/* ---------- Fonts ----------
   Self-hosted rather than pulled from the Google Fonts CDN so the PWA's
   service worker can cache them with the rest of the app shell (it only
   ever intercepts same-origin requests) and so there's no third-party
   request on every page load. Both files are the variable font, split
   into the two subsets this UI actually needs — latin-ext carries the
   Polish diacritics. */
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url('/fonts/jetbrains-mono-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
    U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url('/fonts/jetbrains-mono-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Sold in the shop as the 'name-oxygene' nickname effect — only ever
   applied to a display name, never to body text. */
@font-face {
  font-family: 'Oxygene 1';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/oxygene-1.ttf') format('truetype');
}

/* ---------- Cross-document view transitions ----------
   This is a multi-page site: every link is a real document load, and that
   is what produced the white flash and the navigation visibly rebuilding
   on each tap. Opting into cross-document view transitions lets the
   browser hold the old page on screen and cross-fade into the new one, so
   navigation reads as continuous even though a full load still happens
   underneath. Chrome/Edge and Safari support it; anywhere else this block
   is simply ignored and navigation behaves exactly as before.

   Naming the navbar and bottom bar keeps them *out* of the cross-fade —
   the browser matches them across the two documents and holds them still
   instead of dissolving and redrawing them. */
@view-transition {
  navigation: auto;
}

.navbar {
  view-transition-name: site-navbar;
}

.bottom-nav {
  view-transition-name: site-bottom-nav;
}

/* The global reduced-motion rule below can't reach these — they're
   pseudo-elements, which `*` doesn't match. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

/* ---------- Design tokens ---------- */
:root {
  /* Tells the browser which palette native controls (checkboxes, select
     dropdown popups, scrollbars) should render in — without this they stay
     light-themed even when the page itself is dark. */
  color-scheme: light;

  /* Ink on paper, with one signal colour.
     The palette used to be indigo→cyan on cool grey, which is the default
     look of every dashboard template on the internet and sat oddly under a
     monospace face. Warm paper and near-black ink give the type something
     to be typeset on, and a single vermilion accent means an accent
     actually marks something instead of decorating everything. */
  --color-bg: #f2efe9;
  --color-surface: #fbf9f5;
  --color-border: #ddd7cb;
  --color-border-strong: #c3bcac;
  --color-text: #191713;
  --color-text-muted: #5d594e;
  --color-text-faint: #8b867a;

  --color-primary: #c03a17;
  --color-primary-hover: #9c2f12;
  --color-primary-soft: #f6e6dd;
  --color-accent: #16645c;

  --color-danger: #b3261e;
  --color-danger-soft: #f8e3e0;
  --color-success: #1f6b3a;
  --color-success-soft: #e3f0e6;
  --color-muted-soft: #e9e5dc;
  --navbar-bg: rgba(242, 239, 233, 0.92);

  /* Squared off. Pill-shaped everything was the other half of the
     template look; corners this tight read as built rather than styled,
     and they suit the mono face. Genuinely round things — avatars, dots,
     progress tracks — set 50%/999px themselves. */
  --radius-sm: 3px;
  --radius-md: 5px;
  --radius-lg: 8px;

  /* Depth is for things that genuinely float (menus, modals, sheets,
     toasts). Cards state their edges with a border instead — a grid of
     drop-shadowed rectangles is what made the page look sprayed on. */
  --shadow-sm: 0 1px 0 rgba(25, 23, 19, 0.05);
  --shadow-md: 0 2px 10px rgba(25, 23, 19, 0.08);
  --shadow-lg: 0 14px 34px rgba(25, 23, 19, 0.16);

  --container: 1120px;
  --font: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* Micro-labels: uppercase, tracked out, small. A monospace face is at its
   best doing this, and it gives every section a quiet header without
   another box or another colour. */
.eyebrow,
.section-head p,
.card-meta,
.stat-label,
.label-caps {
  font-variant-numeric: tabular-nums;
}

/* Dark theme: applied automatically when the OS prefers dark (unless the
   user explicitly picked light), and always when data-theme="dark" is set
   by the theme toggle (js/theme.js). */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --color-bg: #14120f;
    --color-surface: #1c1a16;
    --color-border: #302c26;
    --color-border-strong: #48423a;
    --color-text: #f0ece3;
    --color-text-muted: #a49e91;
    --color-text-faint: #78726a;

    --color-primary: #ef6b3c;
    --color-primary-hover: #ff8558;
    --color-primary-soft: #2e1d14;
    --color-accent: #4fbfae;

    --color-danger: #e5675c;
    --color-danger-soft: #35201d;
    --color-success: #5aa86f;
    --color-success-soft: #1a2b1f;
    --color-muted-soft: #262319;
    --navbar-bg: rgba(20, 18, 15, 0.92);

    --shadow-sm: 0 1px 0 rgba(0, 0, 0, 0.4);
    --shadow-md: 0 2px 10px rgba(0, 0, 0, 0.45);
    --shadow-lg: 0 14px 34px rgba(0, 0, 0, 0.6);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --color-bg: #14120f;
  --color-surface: #1c1a16;
  --color-border: #302c26;
  --color-border-strong: #48423a;
  --color-text: #f0ece3;
  --color-text-muted: #a49e91;
  --color-text-faint: #78726a;

  --color-primary: #ef6b3c;
  --color-primary-hover: #ff8558;
  --color-primary-soft: #2e1d14;
  --color-accent: #4fbfae;

  --color-danger: #e5675c;
  --color-danger-soft: #35201d;
  --color-success: #5aa86f;
  --color-success-soft: #1a2b1f;
  --color-muted-soft: #262319;
  --navbar-bg: rgba(20, 18, 15, 0.92);

  --shadow-sm: 0 1px 0 rgba(0, 0, 0, 0.4);
  --shadow-md: 0 2px 10px rgba(0, 0, 0, 0.45);
  --shadow-lg: 0 14px 34px rgba(0, 0, 0, 0.6);
}

* {
  box-sizing: border-box;
}

/* Form controls don't inherit the document font by default — without this
   every button/input would silently fall back to the browser's own UI
   font, which now reads as a completely different typeface next to the
   monospace body text. */
button,
input,
select,
textarea {
  font-family: inherit;
}

@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;
  }
}

html,
body {
  padding: 0;
  margin: 0;
}

body {
  font-family: var(--font);
  color: var(--color-text);
  background: var(--color-bg);
  line-height: 1.55;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  transition: background-color 0.2s ease, color 0.2s ease;
}

main {
  flex: 1;
}

/* Monospace headings need help: at display sizes the fixed advance width
   makes them look gappy, so they get negative tracking and the weight
   comes down from 700 to 600 — a mono bold at 3rem reads as shouting. */
h1,
h2,
h3,
h4 {
  line-height: 1.15;
  margin: 0 0 0.5em;
  font-weight: 600;
  letter-spacing: -0.035em;
}

h1 {
  letter-spacing: -0.05em;
}

/* Numbers line up in columns — counts, XP, prices, view totals all sit in
   tables and stat rows where a jittering digit width is visible. */
body {
  font-variant-numeric: tabular-nums;
}

p {
  margin: 0 0 1em;
  color: var(--color-text-muted);
}

a {
  color: var(--color-primary);
  text-decoration: none;
}

/* Underline from the baseline rather than through the descenders, and
   only on hover — an offset underline is the one link decoration that
   doesn't fight a monospace face. */
a:hover {
  text-decoration: underline;
  text-underline-offset: 0.22em;
  text-decoration-thickness: 1px;
}

img {
  max-width: 100%;
  display: block;
}

.container {
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 24px;
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 10px 18px;
  font-size: 0.86rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease,
    box-shadow 0.15s ease;
  text-decoration: none;
  line-height: 1;
}

.btn:active {
  transform: translateY(1px);
}

.btn-primary {
  background: var(--color-primary);
  color: #fff;
  border-color: var(--color-primary);
}

.btn-primary:hover {
  background: var(--color-primary-hover);
  border-color: var(--color-primary-hover);
  text-decoration: none;
  transform: translateY(-1px);
}

/* The secondary button is a hairline box, not a second filled shape —
   two solid buttons side by side never say which one is the point. */
.btn-secondary {
  background: transparent;
  color: var(--color-text);
  border-color: var(--color-border-strong);
}

.btn-secondary:hover {
  border-color: var(--color-text);
  background: var(--color-muted-soft);
  color: var(--color-text);
  text-decoration: none;
  transform: translateY(-1px);
}

.btn-ghost {
  background: transparent;
  color: var(--color-text-muted);
}

.btn-ghost:hover {
  background: var(--color-primary-soft);
  color: var(--color-primary);
  text-decoration: none;
}

.btn-danger {
  background: var(--color-danger-soft);
  color: var(--color-danger);
}

.btn-danger:hover {
  background: var(--color-danger);
  color: #fff;
  text-decoration: none;
}

.btn-sm {
  padding: 6px 12px;
  font-size: 0.78rem;
}

.btn-block {
  width: 100%;
}

.btn[disabled] {
  opacity: 0.6;
  cursor: not-allowed;
}

/* ---------- Navbar ---------- */
.navbar {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--navbar-bg);
  /* Light blur only. The heavy saturate(180%) frosted-glass bar is period
     decoration; this keeps text legible over whatever scrolls under it
     without turning the header into an effect. */
  backdrop-filter: blur(6px);
  border-bottom: 1px solid var(--color-border);
}

.navbar .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 60px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 9px;
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: -0.04em;
  color: var(--color-text);
}

.brand:hover {
  text-decoration: none;
}

/* One flat square of the signal colour. It was a rounded indigo→cyan
   gradient tile, which is the single most recognisable stock-template
   mark there is. */
.brand-mark {
  width: 26px;
  height: 26px;
  border-radius: 2px;
  background: var(--color-primary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-weight: 700;
  font-size: 0.82rem;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: 4px;
}

.nav-link {
  position: relative;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  color: var(--color-text-muted);
  font-weight: 500;
  font-size: 0.84rem;
}

.nav-link:hover {
  background: var(--color-muted-soft);
  color: var(--color-text);
  text-decoration: none;
}

/* The current page is marked with a rule under the label rather than by
   recolouring it — colour is reserved for things you can act on. */
.nav-link.active {
  color: var(--color-text);
  font-weight: 600;
}

.nav-link.active::after {
  content: '';
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 2px;
  height: 2px;
  background: var(--color-primary);
}

.nav-right {
  display: flex;
  align-items: center;
}

.nav-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}

.nav-toggle {
  display: none;
  background: none;
  border: none;
  cursor: pointer;
  padding: 8px;
}

.nav-toggle span {
  display: block;
  width: 22px;
  height: 2px;
  background: var(--color-text);
  margin: 4px 0;
  transition: transform 0.25s ease, opacity 0.2s ease;
  transform-origin: center;
}

.nav-toggle.is-open span:nth-child(1) {
  transform: translateY(6px) rotate(45deg);
}

.nav-toggle.is-open span:nth-child(2) {
  opacity: 0;
  transform: scaleX(0);
}

.nav-toggle.is-open span:nth-child(3) {
  transform: translateY(-6px) rotate(-45deg);
}

.user-chip {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 10px 5px 5px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  font-weight: 500;
  font-size: 0.8rem;
}

.user-chip:hover {
  text-decoration: none;
  opacity: 0.85;
}

.avatar {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--color-text);
  color: var(--color-bg);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 0.75rem;
  font-weight: 700;
  flex-shrink: 0;
  overflow: hidden;
}

.avatar-img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.icon {
  display: inline-block;
  vertical-align: middle;
  flex-shrink: 0;
  /* Every icon reacts to interaction; the rules under "Icon animations"
     below decide how. The overshoot curve is what makes it read as a
     physical press rather than a fade. The global prefers-reduced-motion
     block near the top of this file flattens all of it for anyone who's
     asked for less movement. */
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* ---------- Icon animations ---------- */

/* Buttons and links: the icon leads the press. */
.btn:hover .icon,
.card-edit-btn:hover .icon {
  transform: translateY(-1px) scale(1.1);
}

.btn:active .icon,
.card-edit-btn:active .icon {
  transform: scale(0.9);
}

.nav-link .icon,
.type-badge .icon {
  transition: transform 0.18s ease;
}

.type-badge:hover .icon {
  transform: rotate(-8deg) scale(1.12);
}

/* Bottom tab bar: the icon lifts into the active tab and dips on tap. */
.bottom-nav-icon > * {
  transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.bottom-nav-item.active .bottom-nav-icon > * {
  transform: translateY(-2px) scale(1.12);
}

.bottom-nav-item:active .bottom-nav-icon > * {
  transform: scale(0.86);
}

.bottom-nav-add:hover .bottom-nav-icon .icon {
  transform: rotate(90deg);
}

/* Scrolls rail: the whole control pops, since the icons there are the
   primary controls rather than decoration. */
.scroll-rail-btn:hover .scroll-rail-icon .icon {
  transform: scale(1.18);
}

.scroll-rail-btn:active .scroll-rail-icon .icon {
  transform: scale(0.85);
}

/* The bell nudges rather than spins — it's a notification, not a toy. */
.notif-bell:hover .icon {
  animation: bell-swing 0.6s ease;
  transform-origin: 50% 15%;
}

@keyframes bell-swing {
  0%,
  100% {
    transform: rotate(0deg);
  }
  20% {
    transform: rotate(12deg);
  }
  45% {
    transform: rotate(-9deg);
  }
  70% {
    transform: rotate(5deg);
  }
}

.theme-toggle {
  transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), border-color 0.15s ease;
}

.theme-toggle:hover {
  transform: rotate(-18deg) scale(1.06);
}

.theme-toggle:active {
  transform: scale(0.9);
}

/* Achievement tiles are tap targets on mobile (they open the detail
   modal), so their icon reacts too. */
.achievement-badge:hover .achievement-icon .icon {
  transform: rotate(-10deg) scale(1.14);
}

.achievement-badge:active .achievement-icon .icon {
  transform: scale(0.92);
}

.name-badge .icon,
.level-chip {
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.user-chip:hover .name-badge .icon {
  transform: scale(1.15);
}

.leaderboard-row-wrap:hover .level-chip {
  transform: scale(1.08);
}

/* Toast icons land with a bounce so the reward registers. */
.toast-icon .icon {
  animation: toast-icon-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

@keyframes toast-icon-pop {
  0% {
    transform: scale(0.3) rotate(-25deg);
    opacity: 0;
  }
  100% {
    transform: scale(1) rotate(0deg);
    opacity: 1;
  }
}

.notif-item:hover .avatar {
  transform: scale(1.06);
}

.avatar {
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* ---------- Hero ---------- */
/* No coloured glow behind the headline. The hero is set left, on the same
   paper as everything else, and closed with a rule — the page starts by
   saying something rather than by lighting itself. */
.hero {
  padding: 72px 0 56px;
  border-bottom: 1px solid var(--color-border);
}

/* The markup puts .container and .hero-inner on the same element, so this
   must not fight the container's centring — the measure is set on the text
   itself instead, which also keeps the headline from running to a
   ten-word line on a wide screen. */
.hero-inner {
  text-align: left;
}

.hero-inner h1 {
  max-width: 18ch;
}

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--color-primary);
  font-weight: 600;
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  margin-bottom: 18px;
}

.eyebrow::before {
  content: '';
  width: 18px;
  height: 1px;
  background: currentColor;
}

.hero h1 {
  font-size: clamp(2.1rem, 4.5vw, 3.4rem);
  margin-bottom: 18px;
}

.hero p.lead {
  font-size: 1rem;
  line-height: 1.7;
  color: var(--color-text-muted);
  margin-bottom: 30px;
  max-width: 56ch;
}

.hero-actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

/* ---------- Sections ---------- */
.section {
  padding: 56px 0;
}

/* A section header is a label and a rule, not a big centred title. */
.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 24px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--color-border);
  flex-wrap: wrap;
}

.section-head h2 {
  font-size: 1.05rem;
  margin: 0;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.section-head p {
  margin: 0;
  font-size: 0.78rem;
  color: var(--color-text-faint);
}

.features {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}

/* Feature cards lose their boxes: on a page that already has a grid of
   real content cards, three more outlined rectangles just add noise. Each
   one is now a rule, an icon and text. */
.feature-card {
  padding: 20px 0 0;
  border-top: 1px solid var(--color-border-strong);
}

.feature-card h3 {
  font-size: 0.95rem;
}

.feature-icon {
  width: 28px;
  height: 28px;
  border-radius: 0;
  background: none;
  color: var(--color-primary);
  display: flex;
  align-items: center;
  justify-content: flex-start;
  margin-bottom: 12px;
}

/* ---------- Project grid & cards ---------- */
.project-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 22px;
}

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

.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: border-color 0.15s ease, transform 0.15s ease, background 0.15s ease;
  animation: fade-in-up 0.35s ease both;
}

/* Hover firms up the edge instead of casting a shadow. The lift stays —
   it's what makes the grid feel alive under a cursor. */
.card:hover {
  border-color: var(--color-border-strong);
  transform: translateY(-2px);
}

/* The empty thumbnail is a hatched placeholder rather than a pastel
   gradient — it reads as "no image yet" instead of as decoration. */
.card-thumb {
  aspect-ratio: 16 / 9.5;
  background-color: var(--color-muted-soft);
  background-image: repeating-linear-gradient(
    45deg,
    transparent 0 6px,
    rgba(125, 118, 105, 0.14) 6px 7px
  );
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-faint);
  font-size: 1.6rem;
  font-weight: 600;
  overflow: hidden;
}

.card-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.card-body {
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 1;
}

.card-body h3 {
  font-size: 1.08rem;
  margin: 0;
}

.card-body h3 a {
  color: var(--color-text);
}

.card-body h3 a:hover {
  color: var(--color-primary);
  text-decoration: none;
}

.card-summary {
  font-size: 0.92rem;
  margin: 0;
  flex: 1;
}

.card-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 0.8rem;
  color: var(--color-text-faint);
  margin-top: 4px;
}

.card-edit-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--color-text-muted);
}

.card-edit-btn:hover {
  color: var(--color-primary);
  text-decoration: none;
}

.card-author {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.card-author a {
  color: inherit;
  font-weight: 600;
}

.card-author a:hover {
  color: var(--color-primary);
}

.avatar-sm {
  width: 18px;
  height: 18px;
  font-size: 0.6rem;
}

.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

/* Tags read as typed metadata, not as sweets. */
.tag {
  display: inline-block;
  padding: 2px 7px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border-strong);
  background: transparent;
  color: var(--color-text-muted);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.02em;
}

.tag::before {
  content: '#';
  opacity: 0.5;
}

.tag.is-muted {
  border-color: var(--color-border);
  color: var(--color-text-faint);
}

.type-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 7px;
  border-radius: var(--radius-sm);
  background: var(--color-muted-soft);
  color: var(--color-text-muted);
  font-size: 0.68rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* ---------- Forms ---------- */
.auth-shell {
  min-height: calc(100vh - 68px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 48px 20px;
}

.auth-card {
  width: 100%;
  max-width: 400px;
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  padding: 36px 32px;
  animation: auth-card-in 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes auth-card-in {
  from {
    opacity: 0;
    transform: translateY(18px) scale(0.98);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

.auth-card h1 {
  font-size: 1.25rem;
}

.auth-card .sub {
  margin-bottom: 26px;
  font-size: 0.84rem;
}

.form-panel {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 28px;
}

.field {
  margin-bottom: 18px;
}

/* Field labels are the small-caps micro-label used everywhere else, so a
   form reads as part of the same document rather than as a widget. */
.field label {
  display: block;
  font-size: 0.68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin-bottom: 6px;
  color: var(--color-text-muted);
}

.field .hint {
  font-size: 0.78rem;
  color: var(--color-text-faint);
  margin-top: 4px;
}

.field input,
.field textarea,
.field select {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 0.88rem;
  background: var(--color-bg);
  color: var(--color-text);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.field input:focus,
.field textarea:focus,
.field select:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: inset 0 0 0 1px var(--color-primary);
}

.field textarea {
  resize: vertical;
  min-height: 110px;
}

.checkbox-field {
  display: flex;
  align-items: center;
  gap: 8px;
}

.checkbox-field input {
  width: auto;
}

.two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}

.form-footer {
  margin-top: 18px;
  text-align: center;
  font-size: 0.9rem;
  color: var(--color-text-muted);
}

.alert {
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  font-size: 0.88rem;
  margin-bottom: 18px;
  display: none;
}

.alert.is-visible {
  display: block;
  animation: alert-in 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}

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

.alert-error {
  background: var(--color-danger-soft);
  color: var(--color-danger);
}

.alert-success {
  background: var(--color-success-soft);
  color: var(--color-success);
}

/* ---------- Dashboard ---------- */
.dash-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 28px;
}

.dash-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 16px;
  margin-bottom: 32px;
}

/* Numbers read as a ledger: a rule on top, the figure large and plain,
   the label small-caps under it. Four outlined boxes said nothing the
   numbers weren't already saying. */
.stat-card {
  background: none;
  border: none;
  border-top: 1px solid var(--color-border-strong);
  border-radius: 0;
  padding: 14px 0 0;
}

.stat-card .num {
  font-size: 1.7rem;
  font-weight: 500;
  letter-spacing: -0.05em;
  line-height: 1.1;
}

.stat-card .label {
  font-size: 0.68rem;
  color: var(--color-text-faint);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

.dash-charts {
  display: grid;
  grid-template-columns: 1.3fr 1fr;
  gap: 16px;
  margin-bottom: 32px;
}

.chart-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 20px;
}

.chart-card h3 {
  margin: 0 0 14px;
  font-size: 0.95rem;
}

.line-chart-wrap {
  position: relative;
}

.line-chart {
  width: 100%;
  height: auto;
  display: block;
  overflow: visible;
}

.chart-hit {
  cursor: pointer;
}

.chart-tooltip {
  position: absolute;
  transform: translate(-50%, -130%);
  background: var(--color-text);
  color: var(--color-bg);
  font-size: 0.76rem;
  font-weight: 600;
  padding: 5px 9px;
  border-radius: var(--radius-sm);
  white-space: nowrap;
  pointer-events: none;
}

.bar-chart {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.bar-row {
  display: grid;
  grid-template-columns: 100px 1fr 32px;
  align-items: center;
  gap: 10px;
}

.bar-label {
  font-size: 0.82rem;
  color: var(--color-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bar-track {
  background: var(--color-muted-soft);
  border-radius: 999px;
  height: 8px;
  overflow: hidden;
}

.bar-fill {
  background: var(--color-primary);
  height: 100%;
  border-radius: 999px;
}

.bar-value {
  font-size: 0.82rem;
  font-weight: 700;
  text-align: right;
}

@media (max-width: 780px) {
  .dash-charts {
    grid-template-columns: 1fr;
  }
  .bar-row {
    grid-template-columns: 70px 1fr 28px;
  }
}

.table-wrap {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

table {
  width: 100%;
  border-collapse: collapse;
}

thead th {
  text-align: left;
  font-size: 0.76rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-faint);
  padding: 14px 20px;
  border-bottom: 1px solid var(--color-border);
}

tbody td {
  padding: 16px 20px;
  border-bottom: 1px solid var(--color-border);
  font-size: 0.92rem;
  vertical-align: middle;
}

tbody tr:last-child td {
  border-bottom: none;
}

tbody tr {
  animation: fade-in-up 0.3s ease both;
}

.row-title {
  font-weight: 700;
}

.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 8px;
  border-radius: var(--radius-sm);
  font-size: 0.7rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.status-pill::before {
  content: '';
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
}

.status-pill.published {
  background: transparent;
  border: 1px solid var(--color-border-strong);
  color: var(--color-success);
}

.status-pill.draft {
  background: transparent;
  border: 1px solid var(--color-border);
  color: var(--color-text-faint);
}

.row-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}

/* ---------- Modal ---------- */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 17, 30, 0.5);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 40px 20px;
  overflow-y: auto;
  z-index: 100;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.2s ease, visibility 0s linear 0.2s;
}

.modal-overlay.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.2s ease;
}

.modal {
  width: 100%;
  max-width: 560px;
  background: var(--color-surface);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  padding: 28px;
  transform: scale(0.94) translateY(12px);
  transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}

.modal-overlay.is-open .modal {
  transform: scale(1) translateY(0);
}

.modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 20px;
}

.modal-close {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--color-text-faint);
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 4px;
}

.modal-close:hover {
  color: var(--color-text);
}

/* ---------- Project detail ---------- */
.detail-hero {
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  padding: 48px 0;
}

.detail-thumb {
  width: 100%;
  aspect-ratio: 21 / 8;
  border-radius: var(--radius-lg);
  overflow: hidden;
  margin-bottom: 28px;
  background-color: var(--color-muted-soft);
  background-image: repeating-linear-gradient(
    45deg,
    transparent 0 6px,
    rgba(125, 118, 105, 0.14) 6px 7px
  );
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-faint);
  font-size: 2rem;
  font-weight: 600;
}

.detail-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.detail-meta {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 18px;
  color: var(--color-text-faint);
  font-size: 0.9rem;
}

.detail-body {
  white-space: pre-wrap;
  color: var(--color-text);
  font-size: 1rem;
  line-height: 1.75;
}

.detail-links {
  display: flex;
  gap: 10px;
  margin-top: 28px;
  flex-wrap: wrap;
}

/* ---------- Engagement bar (likes, edit) ---------- */
/* Six controls (like, Scrolls, save, share, edit, report) do not fit on a
   phone in one row, and without wrapping the last one — Report — simply
   left the screen and took the whole page's horizontal scroll with it.
   Wrapping is the fix; the row-gap keeps the second line from touching
   the first. */
.engagement-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 10px;
  margin-top: 18px;
}

/* Report is the one destructive-ish action here, so on a wide screen it
   sits apart from the things you actually came to do. On a wrapped phone
   layout the auto margin just right-aligns it on its own line, which is
   the same idea. */
@media (min-width: 720px) {
  .engagement-bar #report-btn {
    margin-left: auto;
  }
}

.like-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 16px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text);
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.05s ease;
}

.like-btn:active {
  transform: translateY(1px);
}

.like-btn:hover {
  border-color: var(--color-danger);
  color: var(--color-danger);
}

.like-btn.is-liked {
  border-color: var(--color-danger);
  color: var(--color-danger);
  background: var(--color-danger-soft);
}

.like-btn.is-liked .icon {
  animation: like-pop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes like-pop {
  0% {
    transform: scale(1);
  }
  35% {
    transform: scale(1.35);
  }
  100% {
    transform: scale(1);
  }
}

.like-btn.is-liked .icon {
  fill: currentColor;
}

.like-btn[disabled] {
  opacity: 0.6;
  cursor: not-allowed;
}

/* ---------- Standalone hint text ---------- */
.hint {
  font-size: 0.85rem;
  color: var(--color-text-faint);
}

/* ---------- Comments ---------- */
.comment-form {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 10px;
  margin-bottom: 32px;
}

.comment-form textarea {
  width: 100%;
  min-height: 90px;
  padding: 11px 13px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 0.95rem;
  background: var(--color-surface);
  color: var(--color-text);
  resize: vertical;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.comment-form textarea:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: inset 0 0 0 1px var(--color-primary);
}

.comments-list {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.comment {
  padding-bottom: 20px;
  border-bottom: 1px solid var(--color-border);
}

.comment:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

.comment-head {
  display: flex;
  align-items: center;
  gap: 10px;
}

.comment-head-text {
  display: flex;
  flex-direction: column;
  line-height: 1.3;
}

/* The name and its level chip share one row — without this wrapper the chip
   becomes another item in the column above and stretches to full width. */
.comment-author-row {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}

.comment-author {
  font-weight: 700;
  font-size: 0.9rem;
}

.comment-time {
  font-size: 0.76rem;
  color: var(--color-text-faint);
}

.comment-delete {
  margin-left: auto;
  background: none;
  border: none;
  color: var(--color-text-faint);
  cursor: pointer;
  padding: 4px;
  display: inline-flex;
  border-radius: var(--radius-sm);
}

.comment-delete:hover {
  color: var(--color-danger, #e5484d);
  background: var(--color-primary-soft);
}

.comment-body {
  margin: 8px 0 0;
  font-size: 0.94rem;
  line-height: 1.6;
  white-space: pre-wrap;
}

/* ---------- Cookie consent banner ---------- */
/* A static bar at the very top of the document (not fixed/floating) so it
   pushes page content down instead of overlapping it — avoids covering
   buttons on short pages like the auth forms. */
.cookie-banner {
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  padding: 14px 20px;
}

.cookie-banner-inner {
  max-width: 1100px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
}

.cookie-banner-inner p {
  flex: 1 1 320px;
  margin: 0;
  font-size: 0.85rem;
  color: var(--color-text-muted);
}

/* ---------- Empty state ---------- */
.empty-state {
  text-align: center;
  padding: 56px 24px;
  background: none;
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-md);
}

.empty-state h3 {
  margin-bottom: 6px;
  font-size: 0.95rem;
}

/* ---------- Filters ---------- */
.filter-bar {
  display: flex;
  gap: 12px;
  margin-bottom: 28px;
  flex-wrap: wrap;
}

.filter-bar input,
.filter-bar select {
  padding: 9px 12px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 0.85rem;
  background: var(--color-bg);
  color: var(--color-text);
}

.filter-bar input {
  flex: 1;
  min-width: 220px;
}

/* ---------- Footer ---------- */
.site-footer {
  border-top: 1px solid var(--color-border);
  padding: 32px 0;
  margin-top: 48px;
}

.site-footer .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--color-text-faint);
}

.site-footer a {
  color: var(--color-text-faint);
}

/* ---------- Utility ---------- */
.text-center {
  text-align: center;
}

.mt-lg {
  margin-top: 32px;
}

.spinner {
  display: inline-block;
  width: 16px;
  height: 16px;
  border: 2px solid rgba(255, 255, 255, 0.4);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* ---------- Responsive ---------- */
@media (max-width: 860px) {
  .features {
    grid-template-columns: 1fr;
  }
  .dash-stats {
    grid-template-columns: 1fr;
  }
  .two-col {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 720px) {
  .nav-links {
    position: absolute;
    top: 68px;
    left: 0;
    right: 0;
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border);
    flex-direction: column;
    align-items: stretch;
    padding: 0 16px;
    display: flex;
    gap: 2px;
    max-height: 0;
    opacity: 0;
    overflow: hidden;
    pointer-events: none;
    visibility: hidden;
    transform: translateY(-6px);
    transition: max-height 0.3s ease, opacity 0.2s ease, transform 0.2s ease, padding 0.3s ease,
      visibility 0s linear 0.3s;
  }
  .nav-links.is-open {
    max-height: 400px;
    opacity: 1;
    padding: 10px 16px;
    pointer-events: auto;
    visibility: visible;
    transform: translateY(0);
    transition: max-height 0.3s ease, opacity 0.2s ease, transform 0.2s ease, padding 0.3s ease;
  }
  .nav-toggle {
    display: block;
  }
  /* With nav-actions hidden below, .nav-right (just the theme toggle) was
     the lone middle child of a 3-item space-between row, landing visually
     centered instead of next to the hamburger. Push it flush right instead. */
  .navbar .container {
    justify-content: flex-start;
  }
  .nav-right {
    margin-left: auto;
  }
  /* The bottom tab bar (js/bottom-nav.js) is the primary nav on mobile —
     it covers everything nav-actions would otherwise show (dashboard,
     profile, log in/out), so hide the top one to avoid it overflowing. */
  .nav-actions {
    display: none;
  }
  /* Chrome on Android already offers its own install affordance; the
     custom button is mainly useful on desktop where there's no automatic
     prompt, and there's no room for it next to the hamburger on mobile. */
  .pwa-install-btn {
    display: none;
  }
  .table-wrap {
    overflow-x: auto;
  }
  table {
    min-width: 640px;
  }
}

.pwa-install-btn {
  margin-right: 10px;
  flex-shrink: 0;
}

/* ---------- Theme toggle ---------- */
.theme-toggle {
  background: none;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  width: 38px;
  height: 38px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font-size: 1rem;
  color: var(--color-text);
  margin-right: 10px;
  flex-shrink: 0;
}

.theme-toggle:hover {
  border-color: var(--color-primary);
}

/* ---------- Bottom mobile nav (injected by js/bottom-nav.js) ---------- */
.bottom-nav {
  display: none;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 60;
  background: var(--color-surface);
  border-top: 1px solid var(--color-border);
  padding: 6px 6px calc(6px + env(safe-area-inset-bottom, 0px));
  align-items: center;
  justify-content: space-around;
}

.bottom-nav-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  color: var(--color-text-faint);
  font-size: 0.65rem;
  font-weight: 700;
  text-decoration: none;
  padding: 4px 6px;
  border-radius: var(--radius-sm);
  flex: 1;
}

.bottom-nav-item:hover,
.bottom-nav-item.active {
  color: var(--color-primary);
  text-decoration: none;
}

.bottom-nav-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 20px;
  line-height: 1;
}

.bottom-nav-avatar {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  object-fit: cover;
}

.bottom-nav-avatar-fallback {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--color-text);
  color: var(--color-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.62rem;
  font-weight: 700;
}

.bottom-nav-add .bottom-nav-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 32px;
  border-radius: var(--radius-sm);
  background: var(--color-primary);
  color: #fff;
  font-size: 1.3rem;
  font-weight: 600;
  margin-top: -14px;
}

@media (max-width: 720px) {
  .bottom-nav {
    display: flex;
  }
  body.has-bottom-nav {
    padding-bottom: 66px;
  }
}

/* ---------- Rolls (swipeable project feed) ---------- */
.scrolls-container {
  height: calc(100vh - 68px);
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
}

body.has-bottom-nav .scrolls-container {
  height: calc(100vh - 68px - 66px);
}

.scroll-card {
  height: calc(100vh - 68px);
  scroll-snap-align: start;
  scroll-snap-stop: always;
  position: relative;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  background: #0d0c0a;
}

body.has-bottom-nav .scroll-card {
  height: calc(100vh - 68px - 66px);
}

.scroll-card-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.scroll-card.has-image::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.2) 45%, transparent 75%);
}

.scroll-content {
  position: relative;
  z-index: 2;
  padding: 28px 12px calc(28px + env(safe-area-inset-bottom, 0px)) 24px;
  flex: 1;
  min-width: 0;
}

.scroll-card.has-image .scroll-content {
  color: #fff;
}

.scroll-card.has-image .scroll-content p {
  color: rgba(255, 255, 255, 0.88);
}

.scroll-content h2 {
  font-size: 1.5rem;
  margin-bottom: 6px;
}

.scroll-content .scroll-author {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-weight: 700;
  text-decoration: none;
  color: inherit;
  margin-bottom: 4px;
}

.scroll-avatar {
  width: 26px;
  height: 26px;
  font-size: 0.72rem;
  border: 2px solid rgba(255, 255, 255, 0.85);
}

.scroll-empty-icon {
  font-size: 3rem;
  font-weight: 800;
  color: var(--color-primary);
}

/* ---------- Scrolls right-side action rail (YT Shorts style) ---------- */
.scroll-rail {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  padding: 0 14px calc(20px + env(safe-area-inset-bottom, 0px));
  flex-shrink: 0;
}

.scroll-card.has-image .scroll-rail {
  color: #fff;
}

.scroll-rail-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  font-size: 0.72rem;
  font-weight: 700;
  text-decoration: none;
}

.scroll-rail-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.35);
}

.scroll-card:not(.has-image) .scroll-rail-icon {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-sm);
}

.scroll-rail-count {
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}

.scroll-card:not(.has-image) .scroll-rail-count {
  text-shadow: none;
  color: var(--color-text-muted);
}

.scroll-rail .like-btn.is-liked .scroll-rail-icon {
  color: #ef4444;
}

.scroll-rail .like-btn.is-liked .icon {
  fill: currentColor;
}

/* Saved reads the same way liked does: the icon fills in rather than the
   button changing shape, so the rail doesn't shift under your thumb. */
.scroll-rail .save-btn.is-saved .scroll-rail-icon {
  color: var(--color-primary);
}

.scroll-rail .save-btn.is-saved .icon {
  fill: currentColor;
  animation: like-pop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.scroll-rail-avatar-img {
  width: 44px;
  height: 44px;
  font-size: 1rem;
  border: 2px solid rgba(255, 255, 255, 0.9);
  margin-bottom: 2px;
}

.scroll-card:not(.has-image) .scroll-rail-avatar-img {
  border-color: var(--color-border);
}

/* Full-bleed suits a phone screen; stretched across a desktop window it
   turns a portrait video into a sliver lost in a sea of black letterboxing.
   YouTube's own desktop Shorts player solves this the same way: cap the
   whole card to a phone-shaped column and centre it, rather than trying to
   fill the width. */
@media (min-width: 721px) {
  .scrolls-container {
    background: var(--color-bg);
  }

  .scroll-card {
    max-width: 460px;
    margin: 0 auto;
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
  }
}

/* ---------- Comments drawer (used on scrolls.html) ---------- */
.comments-drawer-overlay {
  position: fixed;
  inset: 0;
  background: rgba(10, 12, 18, 0.55);
  z-index: 300;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.25s ease, visibility 0s linear 0.25s;
}

.comments-drawer-overlay.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.25s ease;
}

.comments-drawer {
  background: var(--color-surface);
  width: 100%;
  max-width: 620px;
  max-height: 78vh;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow-lg);
  transform: translateY(100%);
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

.comments-drawer-overlay.is-open .comments-drawer {
  transform: translateY(0);
}

.comments-drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px 20px 14px;
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
}

.comments-drawer-body {
  padding: 16px 20px calc(24px + env(safe-area-inset-bottom, 0px));
  overflow-y: auto;
}

/* ---------- Profile page ---------- */
.profile-header {
  display: flex;
  gap: 24px;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 8px;
}

.profile-avatar {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  background: var(--color-text);
  color: var(--color-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.8rem;
  font-weight: 600;
  overflow: hidden;
  flex-shrink: 0;
}

.profile-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.profile-info {
  flex: 1;
  min-width: 200px;
}

.profile-name-row {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

.profile-stats {
  display: flex;
  gap: 28px;
  margin: 14px 0;
}

.profile-stat {
  display: flex;
  flex-direction: column;
}

.profile-stat .num {
  font-size: 1.25rem;
  font-weight: 800;
}

.profile-stat .label {
  font-size: 0.76rem;
  color: var(--color-text-faint);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  font-weight: 700;
}

/* ---------- Name badge (compact achievement indicator) ---------- */
.name-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--color-accent);
  color: #fff;
  vertical-align: middle;
  margin-left: 8px;
}

.user-chip .name-badge {
  width: 20px;
  height: 20px;
  margin-left: 4px;
}

.user-chip .name-badge .icon {
  width: 12px;
  height: 12px;
}

/* Log out inside the burger — mobile only. On desktop the navbar's own
   button covers it, and .nav-links is a horizontal row where a logout
   item would sit oddly among the section links. It's a <button>, so it
   needs the link styling reset onto it. */
.nav-link-logout {
  display: none;
  background: none;
  border: none;
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: var(--color-danger);
}

@media (max-width: 720px) {
  .nav-link-logout {
    display: block;
    margin-top: 4px;
    border-top: 1px solid var(--color-border);
    padding-top: 12px;
    border-radius: 0;
  }
  .nav-link-logout:hover {
    background: var(--color-danger-soft);
    color: var(--color-danger);
  }
}

/* ---------- Embedded media ---------- */
.media-frame {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: #000;
  margin-bottom: 24px;
}

/* Audio-shaped players (Spotify, SoundCloud) are wide and short — 16/9
   would leave most of the box empty. */
.media-frame.is-compact {
  aspect-ratio: auto;
  height: 152px;
  background: transparent;
}

.media-frame iframe,
.media-frame video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

.media-audio {
  margin-bottom: 24px;
}

.media-audio audio {
  width: 100%;
}

.media-image {
  margin-bottom: 24px;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--color-muted-soft);
}

.media-image img {
  width: 100%;
  height: auto;
  display: block;
}

/* In the feed the media fills the card behind the text overlay, so the
   per-embed margins and rounding have to go. */
.scroll-card-media {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.scroll-card-media .media-frame,
.scroll-card-media .media-image,
.scroll-card-media .media-audio {
  margin: 0;
  border-radius: 0;
  width: 100%;
}

.scroll-card-media .media-frame:not(.is-compact) {
  height: 100%;
  aspect-ratio: auto;
}

.scroll-card-media .media-image img {
  max-height: 100vh;
  object-fit: contain;
}

/* Card text sits above the player, but the player still has to be
   clickable — so only the text block blocks pointer events. */
.scroll-card.has-media .scroll-content {
  pointer-events: none;
}

.scroll-card.has-media .scroll-content a,
.scroll-card.has-media .scroll-content button {
  pointer-events: auto;
}

/* ---------- Create bottom sheet + rotating add button ---------- */

/* The + becomes an × by rotating, which reads as the same control changing
   state rather than one button swapping for another. */
.bottom-nav.is-sheet-open .bottom-nav-add .bottom-nav-icon .icon {
  transform: rotate(45deg);
}

.bottom-nav-add {
  /* It's a <button> when signed in and an <a> when not; this keeps the two
     visually identical. */
  background: none;
  border: none;
  font: inherit;
  cursor: pointer;
}

.sheet-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 17, 30, 0.5);
  z-index: 150;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.25s ease, visibility 0s linear 0.25s;
}

.sheet-overlay.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.25s ease;
}

.create-sheet {
  position: fixed;
  left: 0;
  right: 0;
  /* Stops above the tab bar rather than covering it: the + is the control
     that opened the sheet and has to stay visible and tappable to close it
     again — that's the whole point of it rotating. */
  bottom: calc(66px + env(safe-area-inset-bottom, 0px));
  z-index: 160;
  max-height: 74vh;
  display: flex;
  flex-direction: column;
  background: var(--color-surface);
  border-top-left-radius: var(--radius-lg);
  border-top-right-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  transform: translateY(100%);
  /* Translating it out of sight is not the same as taking it away: the box
     still sat over the tab bar and swallowed every tap there, so after one
     close the whole bottom navigation was dead until a reload. visibility
     is what actually removes it, delayed so the slide-out still plays. */
  visibility: hidden;
  pointer-events: none;
  transition: transform 0.32s cubic-bezier(0.16, 1, 0.3, 1), visibility 0s linear 0.32s;
}

.create-sheet.is-open {
  transform: translateY(0);
  visibility: visible;
  pointer-events: auto;
  transition: transform 0.32s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Wider than a phone it stops being a sheet and reads better as a panel. */
@media (min-width: 721px) {
  .create-sheet {
    left: 50%;
    right: auto;
    width: min(520px, calc(100vw - 40px));
    transform: translate(-50%, 100%);
    border-radius: var(--radius-lg);
    /* No tab bar at this width, so nothing to clear. */
    bottom: 20px;
    max-height: 88vh;
  }
  .create-sheet.is-open {
    transform: translate(-50%, 0);
  }
}

.sheet-grabber {
  width: 40px;
  height: 4px;
  border-radius: 999px;
  background: var(--color-border);
  margin: 10px auto 2px;
  flex-shrink: 0;
}

.sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 20px 4px;
  flex-shrink: 0;
}

.sheet-head h2 {
  margin: 0;
  font-size: 1.15rem;
}

.sheet-body {
  overflow-y: auto;
  padding: 8px 20px calc(24px + env(safe-area-inset-bottom, 0px));
}

/* The page behind must not scroll while the sheet is up. */
body.sheet-open {
  overflow: hidden;
}

/* Lifted over the sheet and its overlay only while the sheet is open —
   raising it permanently would put the tab bar on top of the project modal
   and the comments drawer too. */
body.sheet-open .bottom-nav {
  z-index: 170;
}

.sheet-details {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  margin-bottom: 16px;
}

.sheet-details > summary {
  cursor: pointer;
  font-weight: 700;
  font-size: 0.9rem;
  display: flex;
  align-items: center;
  gap: 8px;
}

.sheet-details[open] > summary {
  margin-bottom: 10px;
}

/* ---------- Scrolls appearance presets ---------- */
.scroll-preview {
  height: 120px;
  border-radius: var(--radius-sm);
  background-color: var(--color-muted-soft);
  background-size: cover;
  background-position: center;
  display: flex;
  align-items: flex-end;
  padding: 10px;
  margin-top: 6px;
}

.scroll-preview-label {
  font-size: 0.68rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
}


/* ---------- Notifications bell + panel ---------- */

/* The browser's own `[hidden] { display: none }` comes from the UA
   stylesheet, so any author rule setting `display` beats it — the panel and
   badge below both set one, which left them permanently on screen no matter
   what the `hidden` property said. These restore it at a specificity that
   wins over the class rules. */
.notif-panel[hidden],
.notif-badge[hidden] {
  display: none;
}

.notif-wrap {
  position: relative;
  margin-right: 10px;
  flex-shrink: 0;
}

.notif-bell {
  position: relative;
  background: none;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  width: 38px;
  height: 38px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: var(--color-text);
}

.notif-bell:hover {
  border-color: var(--color-primary);
  color: var(--color-primary);
}

.notif-badge {
  position: absolute;
  top: -3px;
  right: -3px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--color-danger);
  color: #fff;
  font-size: 0.64rem;
  font-weight: 800;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.notif-panel {
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  width: min(340px, calc(100vw - 32px));
  max-height: min(420px, 70vh);
  display: flex;
  flex-direction: column;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  z-index: 120;
  overflow: hidden;
}

.notif-panel-head {
  padding: 12px 16px;
  border-bottom: 1px solid var(--color-border);
  font-size: 0.9rem;
}

.notif-list {
  overflow-y: auto;
  padding: 4px 0;
}

.notif-list .hint {
  padding: 14px 16px;
  margin: 0;
}

.notif-item {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 16px;
  text-decoration: none;
  color: var(--color-text);
}

.notif-item:hover {
  background: var(--color-primary-soft);
  text-decoration: none;
}

.notif-item.is-unread {
  background: var(--color-primary-soft);
}

.notif-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 0.83rem;
  line-height: 1.35;
  min-width: 0;
}

.notif-time {
  font-size: 0.72rem;
  color: var(--color-text-faint);
}

@media (max-width: 720px) {
  /* Anchored to the viewport rather than to the bell. The panel is wider
     than the space to the left of the bell, so `right: 0` against the
     relatively-positioned wrapper hung it off the left edge of the screen.
     72px clears the 68px navbar. */
  .notif-panel {
    position: fixed;
    top: 72px;
    left: 12px;
    right: 12px;
    width: auto;
    max-height: 65vh;
  }
}

/* ---------- Leaderboard ---------- */
.leaderboard {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--color-surface);
}

/* The row is a link to the profile and the Follow button is a control, so
   the button can't live inside the link — this wrapper holds both side by
   side and carries the row's own styling. */
.leaderboard-row-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
  padding-right: 12px;
  border-bottom: 1px solid var(--color-border);
  transition: background 0.15s ease;
}

.leaderboard-row-wrap:last-child {
  border-bottom: none;
}

.leaderboard-row-wrap:hover {
  background: var(--color-muted-soft);
}

.leaderboard-row-wrap.is-me {
  background: var(--color-primary-soft);
  box-shadow: inset 2px 0 0 var(--color-primary);
}

.leaderboard-row {
  display: flex;
  align-items: center;
  gap: 14px;
  flex: 1;
  min-width: 0;
  padding: 12px 16px;
  text-decoration: none;
  color: var(--color-text);
}

.leaderboard-row:hover {
  text-decoration: none;
}

.leaderboard-follow {
  flex-shrink: 0;
}

/* Rank is a figure in a column, not a bubble — the medals are the only
   three that get filled in. */
.leaderboard-rank {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  width: 28px;
  height: 24px;
  border-radius: var(--radius-sm);
  flex-shrink: 0;
  font-size: 0.85rem;
  font-weight: 500;
  background: none;
  color: var(--color-text-faint);
}

.leaderboard-rank-1,
.leaderboard-rank-2,
.leaderboard-rank-3 {
  justify-content: center;
  font-weight: 600;
}

.leaderboard-rank-1 {
  background: #d9a520;
  color: #2b1f00;
}

.leaderboard-rank-2 {
  background: #b6bcc6;
  color: #23272e;
}

.leaderboard-rank-3 {
  background: #b37a52;
  color: #40260f;
}

.leaderboard-name {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
  font-weight: 500;
}

/* The name can be long, and the level chip and XP after it must not get
   pushed off a narrow row. The truncation has to live on the span that
   actually holds the text — text-overflow does nothing on a flex
   container, which just clips the name mid-letter with no ellipsis. */
.leaderboard-name > span:not(.leaderboard-you) {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.leaderboard-you {
  font-size: 0.6rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  padding: 1px 5px;
  border-radius: var(--radius-sm);
  background: var(--color-primary);
  color: #fff;
  flex-shrink: 0;
}

.leaderboard-xp {
  font-size: 0.76rem;
  font-weight: 500;
  color: var(--color-text-faint);
  flex-shrink: 0;
}

.leaderboard-gap {
  text-align: center;
  color: var(--color-text-faint);
  margin: 10px 0;
  letter-spacing: 0.3em;
}

@media (max-width: 560px) {
  .leaderboard-row {
    gap: 10px;
    padding: 11px 12px;
  }
  /* With a Follow button on the row there isn't width for everything, and
     the name is what matters — the raw XP number goes rather than letting
     it get squeezed down to an ellipsis. The level chip still shows
     standing. */
  .leaderboard-xp {
    display: none;
  }
  .leaderboard-follow {
    padding: 7px 11px;
    font-size: 0.74rem;
  }
}

/* ---------- Toasts (reward notifications) ---------- */
.toast-stack {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 200;
  display: flex;
  flex-direction: column-reverse;
  gap: 10px;
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 240px;
  max-width: min(340px, calc(100vw - 40px));
  padding: 13px 16px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  box-shadow: var(--shadow-lg);
  cursor: pointer;
  pointer-events: auto;
  opacity: 0;
  transform: translateY(10px) scale(0.97);
  transition: opacity 0.2s ease, transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}

.toast.is-visible {
  opacity: 1;
  transform: translateY(0) scale(1);
}

.toast.is-leaving {
  opacity: 0;
  transform: translateY(6px) scale(0.97);
}

.toast-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: var(--radius-sm);
  flex-shrink: 0;
  background: var(--color-text);
  color: var(--color-bg);
}

.toast-achievement .toast-icon {
  background: var(--color-accent);
  color: #fff;
}

.toast-level .toast-icon {
  background: var(--color-primary);
  color: #fff;
}

.toast-xp .toast-icon {
  background: #2f6f8f;
  color: #fff;
}

.toast-points .toast-icon {
  background: #c69214;
  color: #1c1400;
}

.toast-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  line-height: 1.3;
  min-width: 0;
}

.toast-text strong {
  font-size: 0.92rem;
}

.toast-text span {
  font-size: 0.78rem;
  color: var(--color-text-muted);
}

@media (max-width: 720px) {
  /* Clear the bottom tab bar (66px + safe area) instead of sitting on it. */
  .toast-stack {
    left: 14px;
    right: 14px;
    bottom: calc(80px + env(safe-area-inset-bottom, 0px));
  }
  .toast {
    min-width: 0;
    max-width: none;
  }
}

/* ---------- Level chip + XP progress ---------- */
.level-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 2px 7px;
  border-radius: var(--radius-sm);
  background: var(--color-text);
  color: var(--color-bg);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1.35;
  vertical-align: middle;
  white-space: nowrap;
  flex-shrink: 0;
}

.level-chip-sm {
  padding: 1px 6px;
  font-size: 0.62rem;
}

/* On a profile that has a background equipped, the header text goes white
   on a saturated gradient — a second gradient chip on top of that reads as
   mud, so it flips to a translucent plate instead. */
.profile-header.has-profile-bg .level-chip {
  background: rgba(0, 0, 0, 0.28);
  backdrop-filter: blur(2px);
}

.level-progress {
  margin-top: 14px;
  max-width: 340px;
}

/* The four numbers both currencies are made of. Wraps rather than
   scrolls — on a narrow phone this sits under the level bar and there is
   nothing here worth hiding off-screen. */
.reach-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 10px;
  max-width: 340px;
}

.reach-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  border-radius: var(--radius-sm);
  font-size: 0.7rem;
  color: var(--color-text-muted);
  background: var(--color-muted-soft);
  cursor: help;
}

.reach-chip strong {
  color: var(--color-text);
  font-weight: 700;
}

.profile-header.has-profile-bg .reach-chip {
  background: rgba(0, 0, 0, 0.28);
  color: rgba(255, 255, 255, 0.82);
}

.profile-header.has-profile-bg .reach-chip strong {
  color: #fff;
}

.level-progress-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  font-size: 0.68rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-text-faint);
  margin-bottom: 6px;
}

.profile-header.has-profile-bg .level-progress-head {
  color: rgba(255, 255, 255, 0.82);
}

.level-progress-track {
  height: 6px;
  border-radius: 0;
  background: var(--color-muted-soft);
  overflow: hidden;
}

.profile-header.has-profile-bg .level-progress-track {
  background: rgba(0, 0, 0, 0.25);
}

.level-progress-fill {
  height: 100%;
  border-radius: 0;
  background: var(--color-primary);
  transition: width 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

/* ---------- Achievements ---------- */
.achievements-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 12px;
  margin-bottom: 40px;
}

.achievement-badge {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 8px;
  padding: 18px 12px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  cursor: pointer;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
  animation: fade-in-up 0.35s ease both;
}

.achievement-badge:hover {
  transform: translateY(-2px);
  border-color: var(--color-border-strong);
}

.achievement-badge .achievement-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
}

.achievement-badge .achievement-label {
  font-size: 0.8rem;
  font-weight: 700;
}

.achievement-badge.is-unlocked {
  border-color: var(--color-border-strong);
  background: var(--color-surface);
}

.achievement-badge.is-unlocked .achievement-icon {
  background: var(--color-accent);
  color: #fff;
}

.achievement-badge.is-unlocked .achievement-label {
  color: var(--color-text);
}

.achievement-badge.is-locked {
  opacity: 0.55;
}

.achievement-badge.is-locked .achievement-icon {
  background: var(--color-muted-soft);
  color: var(--color-text-faint);
}

.achievement-badge.is-locked .achievement-label {
  color: var(--color-text-faint);
}

.follow-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.follow-list-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 4px;
  border-bottom: 1px solid var(--color-border);
  text-decoration: none;
  color: var(--color-text);
}

.follow-list-item:last-child {
  border-bottom: none;
}

.follow-list-item:hover {
  color: var(--color-primary);
  text-decoration: none;
}

.follow-list-item .avatar {
  width: 36px;
  height: 36px;
  font-size: 0.9rem;
}

@media (max-width: 720px) {
  .profile-header {
    flex-direction: column;
    align-items: flex-start;
  }
  .profile-avatar {
    width: 72px;
    height: 72px;
    font-size: 1.5rem;
  }
  .profile-info {
    width: 100%;
  }
  .profile-stats {
    gap: 16px;
  }
  .profile-stat .num {
    font-size: 1.05rem;
  }
  .profile-stat .label {
    font-size: 0.66rem;
  }
}

/* ---------- Profile background (shop item, equipped on .profile-header) ---------- */
.profile-header.has-profile-bg {
  padding: 28px;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}

.profile-header.has-profile-bg .profile-info h1,
.profile-header.has-profile-bg .profile-info > p,
.profile-header.has-profile-bg .profile-stat .num {
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
}

.profile-header.has-profile-bg .profile-stat .label {
  color: rgba(255, 255, 255, 0.78);
}

.shop-bg-sunset {
  background: linear-gradient(135deg, #f97316, #ec4899 60%, #f97316);
}

.shop-bg-ocean {
  background: linear-gradient(135deg, #0ea5e9, #22d3ee 60%, #0369a1);
}

.shop-bg-midnight {
  background: linear-gradient(135deg, #1e1b4b, #4c1d95 60%, #0f172a);
}

.shop-bg-aurora {
  background: linear-gradient(120deg, #22d3ee, #a855f7, #34d399, #22d3ee);
  background-size: 300% 300%;
  animation: shop-aurora-shift 8s ease infinite;
}

@keyframes shop-aurora-shift {
  0%,
  100% {
    background-position: 0% 50%;
  }
  50% {
    background-position: 100% 50%;
  }
}

.shop-bg-blocks {
  background: url('/assets/shop/bg-blocks.webp') center / cover no-repeat;
}

.shop-bg-confetti {
  background-color: #4338ca;
  background-image: radial-gradient(circle at 15% 25%, #fbbf24 0 4px, transparent 5px),
    radial-gradient(circle at 75% 15%, #f472b6 0 4px, transparent 5px),
    radial-gradient(circle at 45% 70%, #34d399 0 4px, transparent 5px),
    radial-gradient(circle at 85% 65%, #60a5fa 0 4px, transparent 5px),
    radial-gradient(circle at 25% 85%, #f87171 0 4px, transparent 5px);
  background-size: 160px 160px;
}

/* ---------- Avatar borders (shop item, equipped on .avatar / .profile-avatar) ---------- */
.shop-border-bronze {
  box-shadow: 0 0 0 3px #b08d57;
}

.shop-border-silver {
  box-shadow: 0 0 0 3px #c7cbd1;
}

.shop-border-gold {
  box-shadow: 0 0 0 3px #f0c419, 0 0 10px 1px rgba(240, 196, 25, 0.5);
}

.shop-border-neon {
  animation: shop-neon-pulse 2.2s ease-in-out infinite;
}

@keyframes shop-neon-pulse {
  0%,
  100% {
    box-shadow: 0 0 0 3px #22d3ee, 0 0 10px 2px rgba(34, 211, 238, 0.5);
  }
  50% {
    box-shadow: 0 0 0 3px #22d3ee, 0 0 20px 6px rgba(34, 211, 238, 0.85);
  }
}

.shop-border-rainbow {
  animation: shop-rainbow-ring 4s linear infinite;
}

@keyframes shop-rainbow-ring {
  0% {
    box-shadow: 0 0 0 3px #ff3b3b, 0 0 14px 1px rgba(255, 59, 59, 0.5);
  }
  16% {
    box-shadow: 0 0 0 3px #ff9d3b, 0 0 14px 1px rgba(255, 157, 59, 0.5);
  }
  33% {
    box-shadow: 0 0 0 3px #f4e04d, 0 0 14px 1px rgba(244, 224, 77, 0.5);
  }
  50% {
    box-shadow: 0 0 0 3px #3ddc84, 0 0 14px 1px rgba(61, 220, 132, 0.5);
  }
  66% {
    box-shadow: 0 0 0 3px #3bb2ff, 0 0 14px 1px rgba(59, 178, 255, 0.5);
  }
  83% {
    box-shadow: 0 0 0 3px #b23bff, 0 0 14px 1px rgba(178, 59, 255, 0.5);
  }
  100% {
    box-shadow: 0 0 0 3px #ff3b3b, 0 0 14px 1px rgba(255, 59, 59, 0.5);
  }
}

/* Image-art borders work differently from the box-shadow rings above: the
   frame is a picture that has to sit *around* the avatar, so it's drawn as
   an oversized pseudo-element. Two consequences handled here:
   - .avatar/.profile-avatar clip their content with overflow:hidden (that's
     what makes the photo circular), which would cut the frame off — so
     framed avatars switch to overflow:visible and get their roundness from
     border-radius on the image itself instead.
   - the pseudo-element is sized and offset in percentages of the avatar, so
     one rule works at every avatar size from the 26px navbar chip up.
   The percentages come from measuring the artwork: its transparent inner
   hole is 377x386 of a 640x584 image, centred at (327, 319.5) — so the
   frame is 169.76% x 151.30% of the hole, shifted to line the hole's centre
   up with the avatar's. */
.shop-border-flame {
  overflow: visible;
  position: relative;
}

.shop-border-flame img {
  border-radius: 50%;
}

.shop-border-flame::after {
  content: '';
  position: absolute;
  left: -36.74%;
  top: -32.77%;
  width: 169.76%;
  height: 151.30%;
  background: url('/assets/shop/border-flame.png') center / 100% 100% no-repeat;
  pointer-events: none;
}

/* The frame overhangs its avatar by roughly a third of the avatar's width on
   each side. That's fine over empty space, but it would sit on top of the
   text that follows the avatar, so framed avatars reserve the room. The
   pseudo-element itself is pointer-events:none, so this is purely visual —
   it never swallows a click on whatever it overlaps. */
.profile-header .shop-border-flame {
  margin-right: 18px;
}

.avatar.shop-border-flame {
  margin: 0 8px;
}

/* ---------- Nickname effects (shop item, wraps a display name) ---------- */
.shop-name-gradient {
  background: linear-gradient(90deg, var(--color-primary), var(--color-accent));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.shop-name-shadow {
  text-shadow: 2px 2px 0 var(--color-accent), 4px 4px 0 rgba(0, 0, 0, 0.18);
}

.shop-name-glow {
  color: #22d3ee;
  text-shadow: 0 0 6px rgba(34, 211, 238, 0.85), 0 0 14px rgba(34, 211, 238, 0.5);
}

.shop-name-rainbow {
  background: linear-gradient(90deg, #ff3b3b, #ff9d3b, #f4e04d, #3ddc84, #3bb2ff, #b23bff, #ff3b3b);
  background-size: 200% auto;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: shop-name-rainbow-move 5s linear infinite;
}

@keyframes shop-name-rainbow-move {
  to {
    background-position: 200% center;
  }
}

/* Swaps the typeface rather than recolouring it, so it stacks visually
   with none of the other name effects — you pick one or the other.
   text-transform is not cosmetic here: this font only draws letterforms in
   its uppercase slots, and its lowercase slots hold decorative dingbats, so
   without forcing uppercase a name like "Peter" renders as unreadable
   symbols. Digits and spaces are unaffected. */
.shop-name-oxygene {
  font-family: 'Oxygene 1', var(--font);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.shop-name-sparkle {
  position: relative;
}

.shop-name-sparkle::after {
  content: '✦';
  position: absolute;
  top: -0.3em;
  right: -0.9em;
  font-size: 0.6em;
  color: #f4e04d;
  animation: shop-sparkle-twinkle 1.6s ease-in-out infinite;
}

@keyframes shop-sparkle-twinkle {
  0%,
  100% {
    opacity: 0.2;
    transform: scale(0.7);
  }
  50% {
    opacity: 1;
    transform: scale(1.15);
  }
}

/* ---------- Shop page ---------- */
.points-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  border-radius: 999px;
  background: var(--color-primary-soft);
  color: var(--color-primary);
  font-weight: 800;
}

.shop-categories {
  display: flex;
  flex-direction: column;
  gap: 40px;
}

.shop-category-hint {
  margin-top: -8px;
}

.shop-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: 16px;
}

.shop-item-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  text-align: center;
  padding: 20px 16px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  animation: fade-in-up 0.35s ease both;
}

.shop-item-card.is-equipped {
  border-color: var(--color-primary);
  background: var(--color-primary-soft);
}

/* No background/color here: every real use combines this with either a
   .shop-bg-* class (whose gradient must show through) or the -avatar/-name
   variants below (which set their own) — a fallback here would compete at
   equal CSS specificity with .shop-bg-*, and since this rule comes later
   in the file it would silently win, which is exactly the bug this avoids. */
.shop-item-preview {
  width: 100%;
  height: 56px;
  border-radius: var(--radius-sm);
}

.shop-item-preview.shop-item-preview-avatar {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
}

.shop-item-preview.shop-item-preview-name {
  height: auto;
  background: none;
  font-size: 1.3rem;
  font-weight: 800;
}

.shop-item-name {
  font-weight: 700;
}

.shop-item-price {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--color-text-muted);
}

.shop-item-card.is-owned .shop-item-price {
  color: var(--color-success);
}

/* ---------- Achievement claim button ---------- */
.achievement-claim-btn {
  padding: 5px 12px;
  font-size: 0.72rem;
}

/* ---------- Achievement detail modal ---------- */
.achievement-modal-icon {
  width: 64px;
  height: 64px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto 16px;
}

.achievement-modal-icon.is-unlocked {
  background: linear-gradient(135deg, #f59e0b, #f97316);
  color: #fff;
}

.achievement-modal-icon.is-locked {
  background: var(--color-muted-soft);
  color: var(--color-text-faint);
}

.achievement-modal-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 700;
  font-size: 0.85rem;
  padding: 4px 10px;
  border-radius: 999px;
  margin-bottom: 12px;
}

.achievement-modal-status.is-unlocked {
  background: var(--color-success-soft);
  color: var(--color-success);
}

.achievement-modal-status.is-locked {
  background: var(--color-muted-soft);
  color: var(--color-text-faint);
}

@media (max-width: 720px) {
  .profile-header.has-profile-bg {
    padding: 20px;
  }
}

/* ============================================================
   Shop, second batch
   ------------------------------------------------------------
   Same three slots as the originals (profile background, avatar
   border, nickname effect), so nothing here needs new markup — an
   item is a price row in shop_item_defs, a line in js/shop-items.js
   and one rule below.

   Everything is CSS: no image downloads, and each one adapts to
   light/dark on its own. The animated ones are all flattened by the
   global prefers-reduced-motion rule near the top of this file.
   ============================================================ */

/* ---------- Profile backgrounds ---------- */

/* The site's own paper, for people who don't want a light show. */
.shop-bg-paper {
  background-color: #efe9dd;
  background-image: repeating-linear-gradient(45deg, transparent 0 7px, rgba(120, 105, 80, 0.09) 7px 8px);
}

.shop-bg-grid {
  background-color: #16283d;
  background-image: linear-gradient(rgba(120, 190, 255, 0.22) 1px, transparent 1px),
    linear-gradient(90deg, rgba(120, 190, 255, 0.22) 1px, transparent 1px);
  background-size: 22px 22px;
}

.shop-bg-sunrise {
  background: linear-gradient(160deg, #7c2d12, #ea580c 55%, #fbbf24);
}

.shop-bg-forest {
  background: linear-gradient(150deg, #052e21, #0f5132 60%, #1a7f4b);
}

/* A prompt, a cursor and scanlines. The stripes are a hard-stop
   repeating gradient rather than an image so they stay crisp at any
   height the header happens to be. */
.shop-bg-terminal {
  background-color: #08120b;
  background-image: repeating-linear-gradient(
      0deg,
      rgba(74, 222, 128, 0.22) 0 1px,
      transparent 1px 4px
    ),
    radial-gradient(120% 90% at 50% 120%, rgba(74, 222, 128, 0.35), transparent 70%);
}

.shop-bg-halftone {
  background-color: #c03a17;
  background-image: radial-gradient(rgba(0, 0, 0, 0.42) 22%, transparent 23%);
  background-size: 14px 14px;
}

.shop-bg-topo {
  background-color: #1c1a16;
  background-image: repeating-radial-gradient(
    circle at 30% 40%,
    rgba(240, 160, 60, 0.35) 0 2px,
    transparent 2px 22px
  );
}

/* Colour bars with a tracking band drifting down them. */
.shop-bg-vhs {
  background: linear-gradient(
    90deg,
    #c0c0c0 0 14.28%,
    #c0c000 14.28% 28.56%,
    #00c0c0 28.56% 42.84%,
    #00c000 42.84% 57.12%,
    #c000c0 57.12% 71.4%,
    #c00000 71.4% 85.68%,
    #0000c0 85.68% 100%
  );
  position: relative;
  overflow: hidden;
}

.shop-bg-vhs::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 14%;
  background: linear-gradient(rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0));
  animation: shop-vhs-band 3.6s linear infinite;
  pointer-events: none;
}

@keyframes shop-vhs-band {
  from {
    top: -14%;
  }
  to {
    top: 100%;
  }
}

.shop-bg-starfield {
  background-color: #070912;
  background-image: radial-gradient(1.6px 1.6px at 12% 22%, #fff, transparent),
    radial-gradient(1.4px 1.4px at 68% 14%, #cbd5ff, transparent),
    radial-gradient(1.8px 1.8px at 38% 68%, #fff, transparent),
    radial-gradient(1.2px 1.2px at 84% 58%, #ffe9c4, transparent),
    radial-gradient(1.5px 1.5px at 24% 86%, #fff, transparent),
    radial-gradient(1.3px 1.3px at 92% 88%, #dbeafe, transparent);
  background-size: 88px 76px;
  animation: shop-star-twinkle 4.5s ease-in-out infinite;
}

@keyframes shop-star-twinkle {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.72;
  }
}

/* Sun, then the horizon grid — the grid is a single gradient skewed by
   its own background-size gradient rather than a 3D transform, which
   would need an extra element and a perspective context. */
.shop-bg-vaporwave {
  background-color: #1a0b2e;
  background-image: repeating-linear-gradient(
      0deg,
      rgba(255, 106, 213, 0.55) 0 1px,
      transparent 1px 18px
    ),
    repeating-linear-gradient(90deg, rgba(255, 106, 213, 0.4) 0 1px, transparent 1px 26px),
    linear-gradient(180deg, #ff2e88 0%, #ff9a3c 34%, #1a0b2e 62%);
}

/* Analogue noise: two dot layers sliding across each other. */
.shop-bg-static {
  background-color: #101010;
  background-image: radial-gradient(rgba(255, 255, 255, 0.45) 1px, transparent 1px),
    radial-gradient(rgba(255, 255, 255, 0.28) 1px, transparent 1px);
  background-size: 5px 5px, 7px 7px;
  animation: shop-static-shift 0.35s steps(4) infinite;
}

@keyframes shop-static-shift {
  0% {
    background-position: 0 0, 3px 4px;
  }
  25% {
    background-position: 2px 3px, 0 2px;
  }
  50% {
    background-position: 4px 1px, 5px 0;
  }
  75% {
    background-position: 1px 4px, 2px 5px;
  }
  100% {
    background-position: 3px 2px, 4px 3px;
  }
}

.shop-bg-lava {
  background-color: #240b02;
  background-image: radial-gradient(closest-side at 28% 30%, #ff7a18 0%, rgba(255, 122, 24, 0) 100%),
    radial-gradient(closest-side at 72% 66%, #e11d48 0%, rgba(225, 29, 72, 0) 100%),
    radial-gradient(closest-side at 52% 88%, #f59e0b 0%, rgba(245, 158, 11, 0) 100%);
  background-size: 60% 70%, 55% 65%, 45% 55%;
  background-repeat: no-repeat;
  animation: shop-lava-drift 12s ease-in-out infinite alternate;
}

@keyframes shop-lava-drift {
  0% {
    background-position: 10% 10%, 80% 70%, 45% 95%;
  }
  50% {
    background-position: 30% 60%, 60% 20%, 70% 40%;
  }
  100% {
    background-position: 5% 80%, 90% 40%, 20% 20%;
  }
}

/* ---------- Avatar borders ----------
   The plain ones are a box-shadow ring: it doesn't affect layout and
   follows the avatar's border-radius for free. The three that need a
   gradient, a moving part or a halo use a pseudo-element instead, and
   those have to opt out of the avatar's overflow:hidden — which is
   there to clip the photo, so the photo gets its own radius back. */
.shop-border-ink {
  box-shadow: 0 0 0 2px var(--color-text);
}

/* outline, not box-shadow: dashes are the whole point and box-shadow
   has no dash. outline-offset keeps it clear of the photo, and like
   box-shadow it takes up no layout space. */
.shop-border-dashed {
  outline: 2px dashed var(--color-primary);
  outline-offset: 2px;
}

.shop-border-double {
  box-shadow: 0 0 0 2px var(--color-surface), 0 0 0 5px var(--color-text);
}

.shop-border-emerald {
  box-shadow: 0 0 0 3px #10b981;
}

.shop-border-violet {
  box-shadow: 0 0 0 3px #8b5cf6;
}

.shop-border-ember {
  animation: shop-ember-breathe 3s ease-in-out infinite;
}

@keyframes shop-ember-breathe {
  0%,
  100% {
    box-shadow: 0 0 0 3px #ea580c, 0 0 8px 1px rgba(234, 88, 12, 0.45);
  }
  50% {
    box-shadow: 0 0 0 3px #f59e0b, 0 0 16px 4px rgba(245, 158, 11, 0.75);
  }
}

.shop-border-orbit,
.shop-border-conic,
.shop-border-halo {
  overflow: visible;
  position: relative;
}

.shop-border-orbit img,
.shop-border-conic img,
.shop-border-halo img {
  border-radius: 50%;
}

.shop-border-orbit {
  box-shadow: 0 0 0 2px var(--color-border-strong);
}

/* A single dot going round. The dot is drawn at the top of a box the
   size of the avatar and the whole box spins, which is far cheaper than
   animating a path and stays centred at any avatar size. */
.shop-border-orbit::after {
  content: '';
  position: absolute;
  inset: -6px;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 0, var(--color-primary) 0 3px, transparent 4px);
  animation: shop-orbit-spin 3.2s linear infinite;
  pointer-events: none;
}

@keyframes shop-orbit-spin {
  to {
    transform: rotate(360deg);
  }
}

/* A conic gradient masked into a ring — a gradient border, which a
   box-shadow cannot do at all. The mask punches out everything inside
   the ring so the avatar underneath still shows through. */
.shop-border-conic::after {
  content: '';
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  background: conic-gradient(#ef4444, #f59e0b, #facc15, #22c55e, #06b6d4, #6366f1, #d946ef, #ef4444);
  -webkit-mask: radial-gradient(circle, transparent calc(50% - 4px), #000 calc(50% - 4px));
  mask: radial-gradient(circle, transparent calc(50% - 4px), #000 calc(50% - 4px));
  animation: shop-orbit-spin 4s linear infinite;
  pointer-events: none;
}

.shop-border-halo::after {
  content: '';
  position: absolute;
  inset: -8px;
  border-radius: 50%;
  /* Transparent through the middle: filled all the way across, the glow
     sat on top of the face instead of around it. */
  background: radial-gradient(
    circle,
    rgba(250, 204, 21, 0) 0 47%,
    rgba(250, 204, 21, 0.6) 53%,
    rgba(250, 204, 21, 0) 76%
  );
  animation: shop-halo-pulse 3.4s ease-in-out infinite;
  pointer-events: none;
}

.shop-border-halo {
  box-shadow: 0 0 0 2px #facc15;
}

@keyframes shop-halo-pulse {
  0%,
  100% {
    transform: scale(0.92);
    opacity: 0.7;
  }
  50% {
    transform: scale(1.08);
    opacity: 1;
  }
}

.shop-border-glitch {
  animation: shop-border-glitch-shift 1.6s steps(2, end) infinite;
}

@keyframes shop-border-glitch-shift {
  0% {
    box-shadow: -3px 0 0 0 #ff003c, 3px 0 0 0 #00e5ff, 0 0 0 2px var(--color-text);
  }
  50% {
    box-shadow: 3px 0 0 0 #ff003c, -3px 0 0 0 #00e5ff, 0 0 0 2px var(--color-text);
  }
  100% {
    box-shadow: -2px 2px 0 0 #ff003c, 2px -2px 0 0 #00e5ff, 0 0 0 2px var(--color-text);
  }
}

/* The pseudo-element borders overhang their avatar, which is fine over
   empty space but would sit on the text that follows one inline. Same
   reservation the Blue Flame frame makes. */
.profile-header .shop-border-orbit,
.profile-header .shop-border-halo {
  margin-right: 10px;
}

.avatar.shop-border-orbit,
.avatar.shop-border-halo {
  margin: 0 5px;
}

/* ---------- Nickname effects ---------- */
.shop-name-caps {
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-size: 0.9em;
  font-weight: 600;
}

.shop-name-underline {
  text-decoration: underline;
  text-decoration-color: var(--color-primary);
  text-decoration-thickness: 2px;
  text-underline-offset: 0.24em;
}

/* Sits behind the bottom half of the text like a marker stroke, and
   because it's a background it survives any colour the name already
   has. */
.shop-name-marker {
  background: linear-gradient(transparent 55%, rgba(250, 204, 21, 0.75) 55%);
  padding: 0 2px;
}

.shop-name-emboss {
  color: var(--color-text);
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.55), 0 -1px 0 rgba(0, 0, 0, 0.35);
}

/* Hollow letters. Needs a colour behind it to read, so the stroke is the
   text colour and the fill is knocked out. */
.shop-name-outline {
  color: transparent;
  -webkit-text-stroke: 1px var(--color-text);
  paint-order: stroke fill;
}

.shop-name-terminal {
  color: #4ade80;
  font-weight: 500;
}

.shop-name-terminal::after {
  content: '█';
  margin-left: 0.15em;
  animation: shop-terminal-blink 1.1s steps(1, end) infinite;
}

@keyframes shop-terminal-blink {
  0%,
  49% {
    opacity: 1;
  }
  50%,
  100% {
    opacity: 0;
  }
}

.shop-name-ice {
  background: linear-gradient(180deg, #e0f2fe, #38bdf8 55%, #0369a1);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  text-shadow: 0 0 10px rgba(56, 189, 248, 0.35);
}

.shop-name-fire {
  background: linear-gradient(0deg, #7c2d12, #ea580c 40%, #facc15 90%);
  background-size: 100% 200%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: shop-name-fire-flicker 2.4s ease-in-out infinite;
}

@keyframes shop-name-fire-flicker {
  0%,
  100% {
    background-position: 50% 0%;
  }
  50% {
    background-position: 50% 60%;
  }
}

.shop-name-gold {
  background: linear-gradient(100deg, #5c410a, #d9b746 28%, #f7e9a8 44%, #b8912a 62%, #5c410a);
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.25));
  background-size: 250% auto;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: shop-name-gold-sheen 6s linear infinite;
}

@keyframes shop-name-gold-sheen {
  to {
    background-position: 250% center;
  }
}

/* RGB split. The two coloured copies are text-shadows rather than
   duplicated elements, so it works on a name of any length without the
   markup knowing anything about it. */
.shop-name-glitch {
  color: var(--color-text);
  animation: shop-name-glitch-split 2.2s steps(2, end) infinite;
}

@keyframes shop-name-glitch-split {
  0% {
    text-shadow: -2px 0 #ff003c, 2px 0 #00e5ff;
  }
  40% {
    text-shadow: 2px 0 #ff003c, -2px 0 #00e5ff;
  }
  60% {
    text-shadow: -1px 1px #ff003c, 1px -1px #00e5ff;
  }
  100% {
    text-shadow: -2px 0 #ff003c, 2px 0 #00e5ff;
  }
}

/* ============================================================
   Set cosmetics
   ------------------------------------------------------------
   The pieces drawn to go together (see SHOP_BUNDLES in
   js/shop-items.js). They're ordinary items — a set is only a list
   of ids — so each one still works on its own.
   ============================================================ */

/* Four toes and a pad, drawn as five radial gradients and tiled. Two
   tiles offset from each other so the prints don't line up in a grid,
   which is what would give away that it's a pattern. */
.shop-bg-paws {
  background-color: #e8d5b7;
  background-image: radial-gradient(ellipse 7px 9px at 22px 30px, #a97142 60%, transparent 62%),
    radial-gradient(ellipse 3.5px 4.5px at 13px 20px, #a97142 60%, transparent 62%),
    radial-gradient(ellipse 3.5px 4.5px at 20px 16px, #a97142 60%, transparent 62%),
    radial-gradient(ellipse 3.5px 4.5px at 27px 16px, #a97142 60%, transparent 62%),
    radial-gradient(ellipse 3.5px 4.5px at 33px 21px, #a97142 60%, transparent 62%);
  background-size: 62px 62px;
}

.shop-bg-beach {
  background: linear-gradient(
    180deg,
    #7dd3fc 0%,
    #38bdf8 34%,
    #0ea5e9 46%,
    #0284c7 58%,
    #fde68a 62%,
    #f5d689 100%
  );
  position: relative;
  overflow: hidden;
}

.shop-bg-beach::after {
  content: '';
  position: absolute;
  top: 10%;
  right: 12%;
  width: 42px;
  height: 42px;
  border-radius: 50%;
  background: radial-gradient(circle, #fffbe6 40%, #fbbf24 70%, rgba(251, 191, 36, 0) 72%);
}

/* Flakes on two layers falling at different speeds — the parallax is what
   stops it reading as a texture sliding down the page in one piece. */
.shop-bg-snow {
  background-color: #1e293b;
  background-image: radial-gradient(2px 2px at 12% 18%, #fff, transparent),
    radial-gradient(1.6px 1.6px at 62% 12%, #e0f2fe, transparent),
    radial-gradient(2.2px 2.2px at 34% 62%, #fff, transparent),
    radial-gradient(1.4px 1.4px at 82% 48%, #fff, transparent),
    radial-gradient(1.8px 1.8px at 48% 86%, #e0f2fe, transparent);
  background-size: 90px 90px, 130px 130px, 90px 90px, 130px 130px, 90px 90px;
  animation: shop-snow-fall 9s linear infinite;
}

@keyframes shop-snow-fall {
  to {
    background-position: 0 90px, 0 130px, 0 90px, 0 130px, 0 90px;
  }
}

/* ---------- Set borders ---------- */

/* The ring is the collar; the tag hangs off the bottom of it. */
.shop-border-collar {
  box-shadow: 0 0 0 4px #b91c1c;
  overflow: visible;
  position: relative;
}

.shop-border-collar img {
  border-radius: 50%;
}

.shop-border-collar::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: -22%;
  width: 30%;
  height: 30%;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(circle at 40% 35%, #fde68a, #d97706 70%);
  box-shadow: 0 0 0 1px rgba(120, 53, 15, 0.6);
  pointer-events: none;
}

/* Rays cut from a repeating conic gradient, masked to a ring so the
   avatar shows through, turning slowly. */
.shop-border-sun,
.shop-border-frost {
  overflow: visible;
  position: relative;
}

.shop-border-sun img,
.shop-border-frost img {
  border-radius: 50%;
}

.shop-border-sun {
  box-shadow: 0 0 0 3px #f59e0b;
}

.shop-border-sun::after {
  content: '';
  position: absolute;
  inset: -9px;
  border-radius: 50%;
  background: repeating-conic-gradient(#fbbf24 0deg 9deg, transparent 9deg 24deg);
  -webkit-mask: radial-gradient(circle, transparent calc(50% - 9px), #000 calc(50% - 9px));
  mask: radial-gradient(circle, transparent calc(50% - 9px), #000 calc(50% - 9px));
  animation: shop-orbit-spin 22s linear infinite;
  pointer-events: none;
}

.shop-border-frost {
  box-shadow: 0 0 0 3px #bae6fd, 0 0 12px 2px rgba(186, 230, 253, 0.55);
}

.shop-border-frost::after {
  content: '';
  position: absolute;
  inset: -7px;
  border-radius: 50%;
  background: repeating-conic-gradient(rgba(224, 242, 254, 0.95) 0deg 3deg, transparent 3deg 30deg);
  -webkit-mask: radial-gradient(circle, transparent calc(50% - 7px), #000 calc(50% - 7px));
  mask: radial-gradient(circle, transparent calc(50% - 7px), #000 calc(50% - 7px));
  animation: shop-frost-shimmer 5s ease-in-out infinite;
  pointer-events: none;
}

@keyframes shop-frost-shimmer {
  0%,
  100% {
    opacity: 0.45;
    transform: rotate(0deg);
  }
  50% {
    opacity: 1;
    transform: rotate(12deg);
  }
}

.profile-header .shop-border-collar,
.profile-header .shop-border-sun,
.profile-header .shop-border-frost {
  margin-right: 12px;
}

.avatar.shop-border-collar,
.avatar.shop-border-sun,
.avatar.shop-border-frost {
  margin: 0 6px;
}

/* ---------- Set nickname effects ---------- */
.shop-name-woof {
  color: #92400e;
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4);
}

.shop-name-woof::after {
  content: '🐾';
  margin-left: 0.3em;
  font-size: 0.7em;
  animation: shop-woof-wag 1.8s ease-in-out infinite;
  display: inline-block;
}

@keyframes shop-woof-wag {
  0%,
  100% {
    transform: rotate(-12deg);
  }
  50% {
    transform: rotate(12deg);
  }
}

.shop-name-tropic {
  background: linear-gradient(92deg, #0e7490, #22d3ee 35%, #fde047 70%, #fb923c);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  /* The teal end of the ramp sits on sky blue in the Summer set, which is
     where this name is most likely to be worn — the shadow is what keeps
     it off the background it was designed to sit on. */
  filter: drop-shadow(0 1px 1px rgba(0, 40, 60, 0.45));
}

/* ---------- Set cards on the shop page ---------- */
.shop-bundle-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 16px;
}

.shop-bundle-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 16px;
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  animation: fade-in-up 0.35s ease both;
}

/* The three members shown as one strip: background, avatar, name — the
   same order they appear on a profile, so the card reads as a preview of
   the profile rather than as three unrelated swatches. */
.shop-bundle-preview {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 62px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.shop-bundle-preview .shop-item-preview-avatar {
  width: 40px;
  height: 40px;
  font-size: 0.78rem;
  flex-shrink: 0;
}

.shop-bundle-preview-name {
  font-size: 1rem;
  font-weight: 600;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.shop-bundle-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.shop-bundle-name {
  font-weight: 600;
}

.shop-bundle-blurb {
  font-size: 0.76rem;
  color: var(--color-text-muted);
  margin: 0;
  min-height: 2.4em;
}

.shop-bundle-price {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 8px;
  font-size: 0.78rem;
  color: var(--color-text-faint);
}

/* Neither figure may break across lines — "215" on one line and "pts" on
   the next is how the partly-owned card fell apart, since the sentence
   after it is long enough to force a wrap. The sentence wraps instead. */
.shop-bundle-price strong,
.shop-bundle-was {
  white-space: nowrap;
}

/* The set price is the number to read; the separate price is context. */
.shop-bundle-price strong {
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--color-text);
}

.shop-bundle-was {
  text-decoration: line-through;
  text-decoration-thickness: 1px;
  color: var(--color-text-faint);
}

.shop-bundle-save {
  color: var(--color-success);
  font-weight: 600;
}

/* Light backgrounds need dark text on them.
   The profile header assumes its background is dark and forces white
   text with a shadow, which is right for fourteen of the sixteen and
   invisible on the two pale ones. These win on specificity (three
   classes to the header rule's two) and also cover the shop's set
   preview, which inherits the page's text colour and would otherwise
   put cream on cream in dark mode. */
.shop-bg-paper,
.shop-bg-beach {
  color: #191713;
}

.profile-header.has-profile-bg.shop-bg-paper .profile-info h1,
.profile-header.has-profile-bg.shop-bg-paper .profile-info > p,
.profile-header.has-profile-bg.shop-bg-paper .profile-stat .num,
.profile-header.has-profile-bg.shop-bg-beach .profile-info h1,
.profile-header.has-profile-bg.shop-bg-beach .profile-info > p,
.profile-header.has-profile-bg.shop-bg-beach .profile-stat .num {
  color: #191713;
  text-shadow: 0 1px 2px rgba(255, 255, 255, 0.55);
}

.profile-header.has-profile-bg.shop-bg-paper .profile-stat .label,
.profile-header.has-profile-bg.shop-bg-paper .level-progress-head,
.profile-header.has-profile-bg.shop-bg-beach .profile-stat .label,
.profile-header.has-profile-bg.shop-bg-beach .level-progress-head {
  color: rgba(25, 23, 19, 0.72);
}

.profile-header.has-profile-bg.shop-bg-paper .reach-chip,
.profile-header.has-profile-bg.shop-bg-beach .reach-chip {
  background: rgba(255, 255, 255, 0.6);
  color: rgba(25, 23, 19, 0.8);
}

.profile-header.has-profile-bg.shop-bg-paper .reach-chip strong,
.profile-header.has-profile-bg.shop-bg-beach .reach-chip strong {
  color: #191713;
}

.profile-header.has-profile-bg.shop-bg-paper .level-progress-track,
.profile-header.has-profile-bg.shop-bg-beach .level-progress-track {
  background: rgba(25, 23, 19, 0.2);
}

/* Saved state on the project page's Save button — filled rather than
   recoloured, the same way the like heart reads as on/off. */
.btn.is-saved {
  border-color: var(--color-primary);
  color: var(--color-primary);
}

.btn.is-saved .icon {
  fill: currentColor;
}
