@import url('https://fonts.googleapis.com/css2?family=Prata:ital,wght@0,400;0,500;1,400&family=Stoke:wght@300;400;500;600&display=swap');.auth-page {
  min-height: 100vh;
  min-height: 100svh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: calc(2rem + env(safe-area-inset-top)) 1.5rem calc(3rem + env(safe-area-inset-bottom));
  background: radial-gradient(circle at 100% 100%, rgba(201, 167, 240, 0.1), transparent 45%),
    radial-gradient(circle at 80% 0%, rgba(157, 111, 212, 0.1), transparent 55%),
    var(--background);
  position: relative;
  overflow: hidden;
}

html[data-native-platform='ios'] .auth-page {
  min-height: 100dvh;
  height: auto;
  overflow: visible;
  align-items: flex-start;
  padding-top: calc(3rem + env(safe-area-inset-top));
  padding-bottom: calc(3rem + env(safe-area-inset-bottom));
  background: var(--background);
}

.auth-page::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(120deg, rgba(255, 255, 255, 0.1), transparent 45%, rgba(255, 255, 255, 0.03));
  opacity: 0.7;
  pointer-events: none;
}

.auth-container {
  position: relative;
  width: 100%;
  max-width: 440px;
  padding: 2.75rem;
  border-radius: 22px;
  border: 2px solid rgba(255, 255, 255, 0.1);
  background: rgba(89, 101, 170, 0.1);
  backdrop-filter: blur(10px);
  box-shadow: 0 32px 64px rgba(0, 0, 0, 0.5);
  animation: authFade 650ms cubic-bezier(0.2, 1, 0.3, 1);
  z-index: 1;
}

.auth-title {
  font-size: var(--fs-2xl);
  color: var(--text);
  margin-bottom: 0.4rem;
  letter-spacing: -0.02em;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.65rem;
  width: 100%;
}

.auth-title-icon {
  width: 46px;
  height: 46px;
  border-radius: 12px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.auth-title-icon img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: drop-shadow(0px 2px 20px rgba(255, 255, 255, 0.2));
}

.auth-title-text {
  line-height: 1;
}

.auth-subtitle {
  text-align: center;
  color: var(--text-secondary);
  margin-bottom: 1.75rem;
}

.auth-tabs {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.6rem;
  background: rgba(255, 255, 255, 0.02);
  padding: 0.3rem;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.07);
}

.auth-tabs button {
  flex: 1;
  padding: 0.75rem;
  background: transparent;
  color: var(--text-secondary);
  font-weight: 600;
  border-radius: 10px;
  letter-spacing: -0.01em;
  transition: color 0.2s ease, background-color 0.2s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}

.auth-tabs button.active {
  background: rgba(255, 255, 255, 0.08);
  color: var(--text);
  box-shadow: none;
}

@media (hover: hover) and (pointer: fine) {
  .auth-tabs button:not(.active):hover {
    color: var(--text);
    background: rgba(255, 255, 255, 0.04);
  }
}

.auth-tabs button:active {
  transform: scale(0.97);
  transition: transform 80ms ease;
}

.auth-form {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

.auth-field-switch {
  position: relative;
  min-height: 0;
  transition: height 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}

.auth-field-group {
  display: grid;
  gap: 0.75rem;
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: translateY(-12px);
  pointer-events: none;
  transition: opacity 0.35s ease, transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}

.auth-field-group.is-active {
  position: relative;
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

.auth-input {
  padding: 0.9rem 1rem;
  border: 1px solid var(--control-border);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.08);
  color: var(--text);
  font-size: var(--fs-base);
  transition: border-color 0.25s ease, background-color 0.25s ease, transform 0.25s ease;
}

.auth-input:focus {
  border-color: rgba(201, 167, 240, 0.75);
  background: rgba(255, 255, 255, 0.06);
}

.auth-password-wrap {
  position: relative;
}

.auth-password-wrap .auth-input {
  width: 100%;
  padding-right: 3rem;
}

.auth-password-toggle {
  position: absolute;
  right: 0.75rem;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  color: var(--text-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.25rem;
  border-radius: 6px;
  transition: color 0.2s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) and (pointer: fine) {
  .auth-password-toggle:hover {
    color: var(--text);
  }
}

.auth-password-hint {
  font-size: var(--fs-xs);
  color: var(--text-secondary);
  margin: -0.4rem 0 0;
}

.auth-policy-check {
  display: flex;
  /* See TermsGate.css: centred against the box now that it is 24px, rather than
     flex-start with a margin nudging a smaller box onto the first line. */
  align-items: center;
  gap: 0.55rem;
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  line-height: 1.45;
  cursor: pointer;
  min-height: 44px;
  padding: 0.25rem 0;
}

/* These were browser default, roughly 13px, which is half the WCAG 2.5.8
   minimum and well under what a thumb expects to hit. */
.auth-policy-check input[type='checkbox'] {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  accent-color: var(--primary);
  cursor: pointer;
}

.auth-forgot-link {
  background: none;
  border: none;
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  text-align: right;
  align-self: flex-end;
  padding: 0;
  margin-top: -0.25rem;
  transition: color 0.2s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) and (pointer: fine) {
  .auth-forgot-link:hover {
    color: var(--primary);
  }
}

.auth-forgot-desc {
  color: var(--text-secondary);
  font-size: var(--fs-base);
  line-height: 1.5;
  margin: 0;
}

.auth-success {
  color: var(--success);
  font-size: var(--fs-sm);
  margin: -0.5rem 0 0;
}

.auth-back-link {
  background: none;
  border: none;
  color: var(--text-secondary);
  font-size: var(--fs-base);
  text-align: center;
  padding: 0.25rem;
  transition: color 0.2s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) and (pointer: fine) {
  .auth-back-link:hover {
    color: var(--primary);
  }
}

.auth-privacy-message {
  margin: 1.5rem 0 1rem;
  padding: 0.875rem 1rem;
  background: rgba(157, 111, 212, 0.08);
  border: 1px solid rgba(157, 111, 212, 0.2);
  border-radius: 10px;
  font-size: var(--fs-sm);
  color: var(--text-secondary);
  text-align: center;
  line-height: 1.5;
}

.auth-privacy-message p {
  margin: 0;
  color: inherit;
}

.auth-legal-links {
  margin-top: 1rem;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.5rem;
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}

.auth-legal-links a {
  padding: 0.5rem 0.25rem;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

.auth-error {
  color: var(--error);
  font-size: var(--fs-sm);
  margin: -0.5rem 0 0;
}

.auth-submit {
  padding: 1rem;
  background: linear-gradient(120deg, #9d6fd4 0%, #c9a7f0 100%);
  color: #f4fbff;
  border-radius: 14px;
  font-weight: 600;
  font-size: var(--fs-base);
  border: 1px solid rgba(15, 54, 105, 0.8);
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), filter 0.3s ease, opacity 0.2s ease;
  box-shadow: none;
}
@media (hover: hover) and (pointer: fine) {
  .auth-submit:hover:not(:disabled) {
    filter: brightness(1.12);
  }
}

.auth-submit:disabled {
  opacity: 0.6;
  cursor: wait;
}

.auth-divider {
  display: flex;
  align-items: center;
  text-align: center;
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  margin: 0.5rem 0;
  gap: 1rem;
}

.auth-divider::before,
.auth-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: rgba(255, 255, 255, 0.1);
}

.auth-google-btn {
  padding: 0.95rem 1rem;
  background: linear-gradient(135deg, #121826, #1f2a3d);
  color: #f5f7ff;
  border-radius: 12px;
  font-weight: 600;
  font-size: var(--fs-base);
  border: 1px solid rgba(255, 255, 255, 0.08);
  transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.35);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
}
@media (hover: hover) and (pointer: fine) {
  .auth-google-btn:hover:not(:disabled) {
    background: linear-gradient(135deg, #182235, #25324a);
    box-shadow: 0 18px 32px rgba(0, 0, 0, 0.4);
  }
}

.auth-google-btn:disabled {
  opacity: 0.6;
  cursor: wait;
}

.auth-google-btn svg {
  flex-shrink: 0;
}

/* Apple Sign In: black pill per Apple HIG */
.auth-apple-btn {
  padding: 0.95rem 1rem;
  background: #000;
  color: #fff;
  border-radius: 12px;
  font-weight: 600;
  font-size: var(--fs-base);
  border: 1px solid rgba(255, 255, 255, 0.12);
  transition: background-color 0.2s ease, box-shadow 0.2s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  width: 100%;
  margin-top: 0.6rem;
}
@media (hover: hover) and (pointer: fine) {
  .auth-apple-btn:hover:not(:disabled) {
    background: #111;
    box-shadow: 0 18px 32px rgba(0, 0, 0, 0.5);
  }
}

.auth-apple-btn:disabled {
  opacity: 0.6;
  cursor: wait;
}

.auth-apple-btn svg {
  flex-shrink: 0;
}

@keyframes authFade {
  from {
    opacity: 0;
    transform: translateY(18px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes surfaceDrift {
  from {
    transform: translate3d(-2%, -1%, 0);
    opacity: 0.55;
  }

  to {
    transform: translate3d(3%, 2%, 0);
    opacity: 0.85;
  }
}

@media (max-width: 640px) {
  .auth-container {
    padding: 2.25rem 1.75rem;
    border-radius: 18px;
  }

  .auth-tabs button {
    font-size: var(--fs-base);
  }
}

@media (max-width: 400px) {
  .auth-page {
    padding: 1rem 0 calc(2rem + env(safe-area-inset-bottom));
    align-items: flex-end;
  }

  .auth-container {
    width: 100%;
    border-radius: 20px 20px 0 0;
    padding: 2rem 1.25rem calc(1.5rem + env(safe-area-inset-bottom));
    border-bottom: none;
  }
}
.home-page {
  min-height: 100vh;
  min-height: 100svh;
  min-height: 100dvh;
  color: var(--text);
  padding: clamp(1.25rem, 2vw, 2rem);
  max-width: 960px;
  margin: 0 auto;
}

.home-hero {
  margin-top: 4.5rem;
  padding: clamp(1.75rem, 2.2vw, 2.5rem);
  border-radius: 20px;
  border: 1px solid rgba(90, 188, 234, 0.28);
  background:
    radial-gradient(circle at 20% -10%, rgba(90, 188, 234, 0.24), transparent 35%),
    radial-gradient(circle at 90% 110%, rgba(10, 104, 255, 0.2), transparent 40%),
    rgba(18, 24, 48, 0.78);
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}

.home-logo-row {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin-bottom: 1.1rem;
}

.home-logo-icon {
  width: 36px;
  height: 36px;
  border-radius: 10px;
}

.home-logo-text {
  font-size: 1.3rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text);
}

.home-hero h1 {
  margin: 0;
  font-size: clamp(1.9rem, 4vw, 3rem);
  line-height: 1.12;
}

.home-subtitle {
  margin: 1rem 0 0;
  max-width: 68ch;
  color: var(--text-secondary);
  line-height: 1.6;
}

.home-cta-row {
  margin-top: 1.3rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}

.home-cta-primary,
.home-cta-secondary {
  min-height: 44px;
  padding: 0.72rem 1.1rem;
  border-radius: 10px;
  text-decoration: none;
  font-weight: 600;
  transition: transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* Near white on a light purple gradient was 2.62:1 at the light end. Dark type
   on the same fill is 5.2:1 at the dark end and 9.5:1 at the light one, which
   is also how the selected calendar day and every other filled purple surface
   in the app reads. */
.home-cta-primary {
  background: linear-gradient(120deg, #9d6fd4, #c9a7f0);
  color: #120d24;
  transition: filter 0.18s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}

.home-cta-secondary {
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.14);
  color: var(--text);
  transition: background-color 0.18s ease, border-color 0.18s ease, filter 0.18s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}

@media (hover: hover) and (pointer: fine) {
  .home-cta-primary:hover {
    filter: brightness(1.08);
  }

  .home-cta-secondary:hover {
    background: rgba(255, 255, 255, 0.13);
    border-color: rgba(201, 167, 240, 0.45);
  }
}

.home-section {
  margin-top: 1.1rem;
  padding: clamp(1rem, 2vw, 1.4rem);
  border-radius: 16px;
  border: 1px solid var(--border);
  background: var(--surface);
}

.home-section h2 {
  margin: 0 0 0.85rem;
}

.home-feature-grid {
  display: grid;
  gap: 0.8rem;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}

.home-feature-card {
  padding: 0.9rem;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid rgba(255, 255, 255, 0.08);
  /* Stagger off the card's own index so the three arrive in reading order
     rather than all at once. `backwards` fill, never `both`, so no transform
     is left on the card afterwards. */
  animation: slideInUp 0.32s ease-out backwards;
  animation-delay: calc(0.06s * var(--card-index, 0) + 0.08s);
}

.home-feature-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  margin-bottom: 0.6rem;
  border-radius: 10px;
  font-size: 0.95rem;
  color: var(--primary);
  background: rgba(201, 167, 240, 0.12);
  border: 1px solid rgba(201, 167, 240, 0.22);
}

.home-feature-card h3 {
  margin: 0 0 0.4rem;
  font-size: var(--fs-base);
}

.home-feature-card p {
  margin: 0;
  color: var(--text-secondary);
  line-height: 1.5;
}

/* The icons replace the bullets rather than sitting alongside them, so the
   list gains meaning without gaining clutter. */
.home-security ul {
  margin: 0;
  padding-left: 0;
  list-style: none;
  color: var(--text-secondary);
  line-height: 1.6;
  display: grid;
  gap: 0.5rem;
}

.home-security li {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
}

.home-security li > svg {
  flex-shrink: 0;
  width: 15px;
  margin-top: 0.28em;
  color: var(--primary);
  opacity: 0.85;
}

.home-footer {
  margin: 1.15rem 0 0.85rem;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  color: var(--text-secondary);
}

@media (prefers-reduced-motion: reduce) {
  .home-feature-card {
    animation: none;
  }
}
.loading-indicator {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  text-align: center;
  color: var(--text-secondary);
  letter-spacing: 0.02em;
  animation: fadeIn 0.2s ease-out;
}

.loading-indicator-start {
  align-items: flex-start;
  text-align: left;
}

.loading-track {
  position: relative;
  height: 6px;
  width: 160px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
  box-shadow: inset 0 0 12px rgba(0, 0, 0, 0.25);
}

.loading-track-sm {
  width: 120px;
  height: 5px;
}

.loading-track-lg {
  width: 200px;
}

.loading-track::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(201, 167, 240, 0.3), transparent);
  animation: loading-track-glow 1.4s ease-in-out infinite;
}

.loading-orb {
  position: absolute;
  top: 50%;
  width: 14px;
  height: 14px;
  margin-top: -7px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(142, 214, 255, 0.95) 0%, rgba(201, 167, 240, 0.15) 65%);
  box-shadow: 0 0 12px rgba(138, 210, 255, 0.85);
  animation: loading-orbit 1.35s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}

.loading-orb-delay {
  animation-delay: 0.24s;
  opacity: 0.6;
  transform: scale(0.9);
}

.loading-label {
  font-weight: 600;
  font-size: var(--fs-base);
  letter-spacing: 0.01em;
}

@keyframes loading-orbit {
  0% {
    transform: translateX(-35%) scale(0.75);
    opacity: 0;
  }
  15% {
    opacity: 1;
  }
  85% {
    opacity: 1;
  }
  100% {
    transform: translateX(135%) scale(0.75);
    opacity: 0;
  }
}

@keyframes loading-track-glow {
  0% {
    transform: translateX(-40%);
    opacity: 0.3;
  }
  50% {
    transform: translateX(35%);
    opacity: 0.65;
  }
  100% {
    transform: translateX(120%);
    opacity: 0.3;
  }
}
/* ── Shimmer base ────────────────────────────────────────────────────────── */

@keyframes shimmer {
  0%   { background-position: -800px 0; }
  100% { background-position:  800px 0; }
}

.skel {
  display: block;
  background: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0.05) 25%,
    rgba(255, 255, 255, 0.10) 50%,
    rgba(255, 255, 255, 0.05) 75%
  );
  background-size: 800px 100%;
  animation: shimmer 1.8s infinite linear;
  flex-shrink: 0;
}

.sk-mt    { margin-top: 0.65rem; }
.sk-mt-sm { margin-top: 0.4rem;  }

/* ── Journal ─────────────────────────────────────────────────────────────── */
/* Mirrors dreams-list: repeat(auto-fill, minmax(280px, 1fr)).               */
/* 2 columns need ≥560px of grid width (~600px screen); 3 need ~860px.       */

/* Mirrors .dreams-list: one hairline separated column, not a tile grid. */
.sk-journal-grid {
  display: flex;
  flex-direction: column;
}

.sk-journal-card {
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  padding: 0.9rem 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;          /* matches .dream-card gap */
}

.sk-journal-top {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.sk-journal-footer {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.2rem;
}

/* ── Feed ────────────────────────────────────────────────────────────────── */

.sk-feed-list {
  display: flex;
  flex-direction: column;
}

/* Mirrors .feed-post: hairline separated stream row, avatar column, no card. */
.sk-feed-card {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: 0.7rem;
  padding: 0.9rem 1.25rem 0.75rem;
  border-bottom: 1px solid var(--border);
}

.sk-feed-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.1rem;
}

/* 40px circle, matching .feed-avatar */
.sk-feed-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  flex-shrink: 0;
}

.sk-feed-main {
  min-width: 0;
}

.sk-feed-footer {
  display: flex;
  gap: 1.1rem;
  margin-top: 0.7rem;
}

/* ── Profile ─────────────────────────────────────────────────────────────── */

.sk-profile {
  max-width: 900px;
  margin: 0 auto;
}

.sk-profile-header {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: 1rem 1.25rem 0.5rem;
}

.sk-profile-identity {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  width: 100%;
}

.sk-profile-name {
  flex: 1 1 auto;
  min-width: 0;
}

.sk-profile-stats {
  display: flex;
  gap: 0.75rem;
  justify-content: flex-start;
  padding: 0.35rem 1.25rem 0.9rem 0.5rem;
  margin: 0;
}

.sk-stat-item {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  gap: 0.35rem;
  padding: 0.5rem 1rem;
}

.sk-profile-btn {
  display: block;
  margin: 0 1.25rem 1.5rem;
}

/* Mirrors .profile-dreams-head: the heading and the tab pair on one row. */
.sk-profile-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.9rem 1.25rem 0.85rem;
}

.sk-profile-head-top {
  flex: 1 1 auto;
  min-width: 0;
}

.sk-profile-head-tabs {
  display: flex;
  gap: 1.25rem;
  flex-shrink: 0;
}

/* Mirrors .profile-dream-grid: hairline separated column, no tiles. */
.sk-profile-grid {
  display: flex;
  flex-direction: column;
}

.sk-profile-card {
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  padding: 0.9rem 1.25rem;
  display: flex;
  flex-direction: column;
}

/* ── Dream Detail ────────────────────────────────────────────────────────── */
/* Mirrors .detail-card exactly so the skeleton fills the same space.        */

.sk-detail-card {
  margin-top: 0.5rem;
  padding: 0;
  border-radius: 0;
  border: none;
  background: none;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

@media (max-width: 640px) {
  .sk-detail-card {
    padding: 0;
  }
}

.sk-detail-toolbar {
  display: flex;
  align-items: center;
}

.sk-detail-title-row {
  display: flex;
  flex-direction: column;
}

.sk-detail-author {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.sk-detail-content {
  display: flex;
  flex-direction: column;
}

.sk-detail-ai {
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1rem 1.25rem;
  display: flex;
  flex-direction: column;
}

/* ── Insights ────────────────────────────────────────────────────────────── */

.sk-insights {
  max-width: 680px;
  margin: 0 auto;
  padding: 1.5rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.sk-insights-header {
  margin-bottom: 0.75rem;
}

.sk-insights-card {
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 1.1rem 1.25rem;
  display: flex;
  flex-direction: column;
}

.sk-insights-card-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0.75rem;
}

.sk-insights-count {
  margin-left: auto;
  flex-shrink: 0;
}

/* ── Settings ────────────────────────────────────────────────────────────── */

.sk-settings {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

.sk-settings-section {
  border: none;
  border-top: 1px solid var(--border);
  border-radius: 0;
  padding-top: 0.35rem;
}

.sk-settings-head {
  padding: 1.25rem 0 0;
  display: flex;
  flex-direction: column;
}

.sk-settings-body {
  padding: 0.85rem 0 1.25rem;
  display: flex;
  flex-direction: column;
}

.sk-toggle-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  padding: 0.9rem 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}

.sk-toggle-row:last-child {
  border-bottom: none;
  padding-bottom: 0;
}

.sk-toggle-row:first-child {
  padding-top: 0;
}

.sk-toggle-label {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.sk-toggle-switch {
  flex-shrink: 0;
}

/* ── Activity ────────────────────────────────────────────────────────────── */

.sk-activity-list {
  display: flex;
  flex-direction: column;
}

.sk-activity-group {
  margin: 0.75rem 1.25rem 0.6rem;
}

.sk-activity-card {
  display: flex;
  align-items: flex-start;
  gap: 0.85rem;
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  padding: 0.9rem 1.25rem;
}

.sk-activity-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.sk-activity-action {
  flex-shrink: 0;
  align-self: center;
}

/* ── Search ──────────────────────────────────────────────────────────────── */

.sk-people-grid {
  display: flex;
  flex-direction: column;
}

.sk-person-card {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  padding: 0.9rem 1.25rem;
}

.sk-dream-grid {
  display: flex;
  flex-direction: column;
}

.sk-search-dream-card {
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  padding: 0.9rem 1.25rem;
  display: flex;
  flex-direction: column;
}

/* ── Reduced motion ──────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .skel {
    animation: none;
    background: rgba(255, 255, 255, 0.06);
  }
}

/* ── Legacy aliases (keep existing names working) ────────────────────────── */

.dream-card-skeleton { display: none; }
.profile-skeleton    { display: none; }
.list-skeleton       { display: none; }
.toast {
  position: fixed;
  bottom: calc(env(safe-area-inset-bottom) + 5rem);
  left: 50%;
  transform: translateX(-50%);
  background: rgba(20, 26, 36, 0.95);
  border: 1px solid rgba(255, 255, 255, 0.12);
  color: #e8eef8;
  font-size: var(--fs-sm);
  line-height: 1.45;
  padding: 0.7rem 1.2rem;
  border-radius: 14px;
  white-space: normal;
  max-width: min(420px, calc(100vw - 2.5rem));
  text-align: center;
  z-index: 600;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
  cursor: pointer;
  animation: toastIn 220ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}

/* The -50% has to be repeated in every frame: a transform replaces the one on
   the base rule rather than adding to it, so dropping it would snap the toast
   to the right by half its width for the length of the animation. */
@keyframes toastIn {
  from { opacity: 0; transform: translateX(-50%) translateY(12px) scale(0.96); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
}

.toast.is-leaving {
  animation: toastOut 180ms ease-in forwards;
  pointer-events: none;
}

@keyframes toastOut {
  from { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
  to   { opacity: 0; transform: translateX(-50%) translateY(8px) scale(0.98); }
}

@media (prefers-reduced-motion: reduce) {
  .toast,
  .toast.is-leaving {
    animation: none;
  }
}
/* Voice dictation control. Sits inside the bottom-right of a textarea, so the
   idle state is deliberately quiet, a single mic glyph, and only expands into
   the waveform bar while actually listening. */

.voice-input {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  pointer-events: none;
}

.voice-input > * {
  pointer-events: auto;
}

/* ── Trigger ─────────────────────────────────────────────────────────────── */

.voice-input-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  flex-shrink: 0;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: rgba(20, 14, 35, 0.85);
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  cursor: pointer;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  transition: color 0.18s ease, border-color 0.18s ease, background-color 0.18s ease, transform 0.18s ease;
  /* Holding the button is a supported gesture, so suppress the iOS long-press
     callout and text selection that would otherwise fire underneath it. */
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
}

.voice-input-btn:active:not(:disabled) {
  transform: scale(0.92);
}

.voice-input-btn:focus-visible {
  color: var(--primary);
  border-color: rgba(201, 167, 240, 0.5);
}

@media (hover: hover) and (pointer: fine) {
  .voice-input-btn:hover:not(:disabled) {
    color: var(--primary);
    border-color: rgba(201, 167, 240, 0.5);
  }
}

.voice-input-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.voice-input-btn.is-recording {
  color: #ffd9df;
  border-color: rgba(255, 120, 140, 0.7);
  background: rgba(120, 26, 44, 0.55);
  animation: voicePulse 1.9s ease-in-out infinite;
}

.voice-input-btn.is-busy {
  color: var(--primary);
  border-color: rgba(201, 167, 240, 0.45);
}

@keyframes voicePulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 120, 140, 0.32); }
  50%      { box-shadow: 0 0 0 7px rgba(255, 120, 140, 0); }
}

.voice-input-spinner {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 2px solid rgba(201, 167, 240, 0.28);
  border-top-color: var(--primary);
  animation: voiceSpin 0.7s linear infinite;
}

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

/* ── Live recording strip ────────────────────────────────────────────────── */

.voice-input-live {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  height: 36px;
  padding: 0 0.7rem;
  border-radius: 999px;
  border: 1px solid rgba(255, 120, 140, 0.35);
  background: rgba(22, 12, 20, 0.9);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  animation: voiceStripIn 200ms cubic-bezier(0.22, 1, 0.36, 1) backwards;
}

@keyframes voiceStripIn {
  from { opacity: 0; transform: translateX(8px) scaleX(0.9); }
  to   { opacity: 1; transform: translateX(0) scaleX(1); }
}

.voice-input-cancel {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--fs-xs);
  cursor: pointer;
  transition: color 0.16s ease;
}

.voice-input-cancel:focus-visible {
  color: #ff9fb1;
}

.voice-input-cancel:active {
  transform: scale(0.88);
  transition: transform 70ms ease;
}

@media (hover: hover) and (pointer: fine) {
  .voice-input-cancel:hover { color: #ff9fb1; }
}

/* ── Waveform ────────────────────────────────────────────────────────────── */

.voice-wave {
  display: flex;
  align-items: center;
  gap: 2px;
  height: 20px;
  width: 104px;
}

.voice-wave-bar {
  flex: 1;
  min-width: 2px;
  height: 100%;
  border-radius: 2px;
  background: linear-gradient(180deg, #f0a8bb, var(--primary));
  transform: scaleY(0.08);
  transform-origin: center;
  will-change: transform;
}

/* The native engine reports one amplitude per buffer rather than a spectrum, so
   its bars scroll as a history and are sampled well under frame rate. Easing
   between samples is what keeps that from looking like a strobe. */
.voice-input.is-live .voice-wave-bar {
  transition: transform 70ms linear;
}

.voice-input-clock {
  font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
  flex-shrink: 0;
}

.voice-input-clock.is-ending {
  color: #ff9fb1;
}

/* ── Live caption ────────────────────────────────────────────────────────── */

/* Sits above the field while recording. On iOS this fills with words as they
   are spoken; on the upload path there is nothing to show until the recording
   is sent, so it carries the stop affordance instead. */
.voice-caption {
  position: absolute;
  right: 0;
  bottom: calc(100% + 0.6rem);
  width: min(320px, calc(100vw - 3.5rem));
  padding: 0.6rem 0.75rem 0.5rem;
  border-radius: 14px;
  border: 1px solid rgba(201, 167, 240, 0.28);
  background: rgba(18, 12, 30, 0.94);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  /* Purely a readout, so it must never swallow a tap meant for the field. */
  pointer-events: none;
  animation: voiceCaptionIn 180ms cubic-bezier(0.22, 1, 0.36, 1) backwards;
}

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

.voice-caption-text {
  max-height: 5.2em;
  overflow-y: auto;
  font-size: var(--fs-sm);
  line-height: 1.45;
  color: var(--text);
  scrollbar-width: none;
}

.voice-caption-text::-webkit-scrollbar { display: none; }

.voice-caption-idle {
  color: var(--text-secondary);
  font-style: italic;
}

.voice-caption-hint {
  margin: 0.45rem 0 0;
  padding-top: 0.4rem;
  border-top: 1px solid rgba(255, 255, 255, 0.07);
  font-size: var(--fs-micro);
  color: var(--text-secondary);
}

/* ── First-run affordance ────────────────────────────────────────────────── */

/* People reach for a mic expecting to hold it. Holding does work, but tapping
   is the one-handed path, and nothing on screen said so. */
.voice-hint {
  position: absolute;
  right: calc(100% + 0.4rem);
  top: 50%;
  transform: translateY(-50%);
  white-space: nowrap;
  padding: 0.25rem 0.55rem;
  border-radius: 999px;
  border: 1px solid rgba(201, 167, 240, 0.28);
  background: rgba(18, 12, 30, 0.9);
  color: var(--text-secondary);
  font-size: var(--fs-micro);
  pointer-events: none;
  animation: voiceHintIn 2.4s ease-in-out infinite;
}

@keyframes voiceHintIn {
  0%, 100% { opacity: 0.55; }
  50%      { opacity: 1; }
}

/* The bars are a readout of the user's own voice, not decoration, so they keep
   moving under reduced motion, and only the ambient pulse and slide are dropped. */
@media (prefers-reduced-motion: reduce) {
  .voice-input-btn.is-recording { animation: none; }
  .voice-input-live { animation: none; }
  .voice-caption { animation: none; }
  .voice-hint { animation: none; opacity: 1; }
}

/* ── Textarea integration ────────────────────────────────────────────────── */

.voice-field {
  position: relative;
  display: block;
}

.voice-field > textarea {
  display: block;
  width: 100%;
}

.voice-field-affordance {
  position: absolute;
  right: 10px;
  bottom: 10px;
  z-index: 2;
}

/* Keep dictated text clear of the button while it sits over the field. */
.voice-field > textarea {
  padding-bottom: 52px;
}

@media (max-width: 420px) {
  .voice-wave { width: 78px; }
}
/* Both of these are fields, and a field has to look like one. Stripping the
   boxes to match the flat stream left the dream itself as an unmarked area of
   sheet with a placeholder in it: nothing said where the writing surface began
   or ended. They keep the control border rather than the hairline, since here
   the boundary is the whole affordance. */
.dream-title-input {
  width: 100%;
  padding: 0.85rem 1rem;
  border: 1px solid var(--control-border);
  border-radius: 12px;
  background: var(--surface);
  color: var(--text);
  font-size: var(--fs-md);
  font-weight: 600;
  margin-bottom: 0.6rem;
  transition: border-color 0.22s ease, box-shadow 0.22s ease;
}

.dream-title-input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(201, 167, 240, 0.18);
}

.dream-title-input::placeholder {
  color: var(--text-secondary);
  font-weight: 400;
}

.dream-date-section {
  margin-bottom: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.dream-date-section label {
  color: var(--text-secondary);
  font-size: var(--fs-base);
}

.dream-date-input {
  padding: 0.75rem 1rem;
  border-radius: 10px;
  border: 1px solid var(--control-border);
  background: var(--surface);
  color: var(--text);
  font-size: var(--fs-base);
  width: fit-content;
  max-width: 100%;
  transition: border-color 0.22s ease;
}

.dream-date-input:focus {
  border-color: var(--primary);
}

/* Same stream as the feed: one column, hairline separated, gutter on the row
   rather than on the page. It was a responsive tile grid, which is why the
   journal read as a dashboard of the same card repeated. */
.dreams-list {
  display: flex;
  flex-direction: column;
}

/* The list is full bleed now, so the divider needs the gutter the rows carry or
   it sits against the screen edge. It also needs air above it: it separates two
   years, so it should read as a bigger break than the hairline between two
   dreams, and at fs-xs in secondary it was quieter than the rows it divides. */
.dreams-year-header {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  margin: 0;
  padding: 2.25rem 1.25rem 0.7rem;
}

.dreams-year-header:first-child {
  padding-top: 1rem;
}

.dreams-year-header span {
  font-size: var(--fs-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--text);
  white-space: nowrap;
}

.dreams-year-header::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
}

.journal-calendar-shell {
  display: grid;
  gap: 1rem;
}

.journal-calendar {
  border: none;
  border-radius: 0;
  background: none;
  padding: 0.5rem 0.25rem 1rem;
  box-shadow: none;
  /* Left to fill the page the columns stretch to ~150px and the month reads as
     a row of letterboxes. Capping it keeps the cells close to square. */
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
  /* Clips the incoming month to the card, so it enters from the edge instead of
     briefly hanging outside the border. Nothing here needs to overflow. */
  overflow: hidden;
}

.calendar-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
}

.calendar-toolbar h2 {
  margin: 0;
  font-size: var(--fs-md);
}

/* Month arrows. They were ← and → glyphs in a text button: about the size of a
   word, with the standard 0.965 button press that is far too subtle on
   something this small. Now a 44px circle, the iOS minimum, around a centred
   svg chevron, with the chip-tier press and a colour flash so a tap reads. */
.calendar-nav-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  font-size: var(--fs-base);
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease;
}

@media (hover: hover) and (pointer: fine) {
  .calendar-nav-btn:hover {
    background: rgba(201, 167, 240, 0.14);
    border-color: rgba(201, 167, 240, 0.4);
    color: var(--primary);
  }
}

.calendar-nav-btn:active {
  transform: scale(0.93);
  background: rgba(201, 167, 240, 0.22);
  border-color: rgba(201, 167, 240, 0.5);
  color: var(--primary);
  transition: transform 70ms ease, background-color 70ms ease, border-color 70ms ease;
}

.calendar-weekdays {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 0.35rem;
  margin-bottom: 0.35rem;
}

.calendar-weekdays span {
  text-align: center;
  font-size: var(--fs-xs);
  color: var(--text-secondary);
  font-weight: 600;
}

.calendar-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 0.35rem;
}

.calendar-spacer {
  min-height: 54px;
}

.calendar-day {
  min-height: 54px;
  padding: 0;
  border: none;
  background: none;
  border-radius: 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  color: var(--text-secondary);
  -webkit-tap-highlight-color: transparent;
  transition: transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* The dream marker. A night with an entry gets a lit dot rather than a count,
   one dream a night is the norm, and the exact number is in the panel below.
   The dot keeps its slot when empty so numbers never shift between days. */
.calendar-day::after {
  content: '';
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: transparent;
  transition: background-color 0.18s ease, box-shadow 0.18s ease;
}

.calendar-day-number {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  font-size: var(--fs-sm);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}

.calendar-day.has-dreams {
  color: var(--text);
}

.calendar-day.has-dreams .calendar-day-number {
  font-weight: 600;
}

.calendar-day.has-dreams::after {
  background: var(--primary);
  box-shadow: 0 0 6px rgba(201, 167, 240, 0.5);
}

.calendar-day.is-today .calendar-day-number {
  color: var(--primary);
  box-shadow: inset 0 0 0 1px rgba(201, 167, 240, 0.5);
}

/* After is-today on purpose: same specificity, so a selected today fills. */
.calendar-day.is-selected .calendar-day-number {
  background: var(--primary);
  color: var(--background);
  box-shadow: none;
  font-weight: 700;
}

/* Month navigation. The grid used to swap instantly, which read as a flicker
   rather than a move. Sliding it in from the side the month came from gives
   the arrows a direction. `backwards` fill so no transform is left behind.

   The travel and the fade are two separate animations on purpose. Running them
   as one is what made the first attempt invisible: a hard ease-out spends most
   of its distance in the first few frames, so by the time the month was solid
   enough to look at, it had already stopped moving. Splitting them lets the
   fade finish early and long before the slide does, so the part you actually
   watch is the part that still has somewhere to go. */
@keyframes calendarMonthNext {
  from { transform: translate3d(44px, 0, 0); }
  to   { transform: translate3d(0, 0, 0); }
}

@keyframes calendarMonthPrev {
  from { transform: translate3d(-44px, 0, 0); }
  to   { transform: translate3d(0, 0, 0); }
}

/* The label is a fraction of the grid's width, so matching its travel would
   read as the two racing rather than moving together. */
@keyframes calendarMonthLabelNext {
  from { transform: translate3d(16px, 0, 0); }
  to   { transform: translate3d(0, 0, 0); }
}

@keyframes calendarMonthLabelPrev {
  from { transform: translate3d(-16px, 0, 0); }
  to   { transform: translate3d(0, 0, 0); }
}

.calendar-grid[data-month-direction='next'] {
  animation: calendarMonthNext 380ms cubic-bezier(0.33, 1, 0.68, 1) backwards,
             fadeIn 140ms ease-out backwards;
}

.calendar-grid[data-month-direction='prev'] {
  animation: calendarMonthPrev 380ms cubic-bezier(0.33, 1, 0.68, 1) backwards,
             fadeIn 140ms ease-out backwards;
}

.calendar-toolbar h2[data-month-direction='next'] {
  animation: calendarMonthLabelNext 380ms cubic-bezier(0.33, 1, 0.68, 1) backwards,
             fadeIn 140ms ease-out backwards;
}

.calendar-toolbar h2[data-month-direction='prev'] {
  animation: calendarMonthLabelPrev 380ms cubic-bezier(0.33, 1, 0.68, 1) backwards,
             fadeIn 140ms ease-out backwards;
}

/* Switching over from the list view used to drop the calendar in fully formed. */
.journal-calendar-shell {
  animation: fadeIn 0.24s ease-out backwards;
}

.calendar-day-panel {
  border: none;
  border-top: 1px solid var(--border);
  border-radius: 0;
  background: none;
  box-shadow: none;
  padding: 1.1rem 0 0;
}

/* Keyed on the selected date in the JSX, so picking another day replays this
   instead of silently swapping the text underneath the cursor. A fade rather
   than a slide, because the dream cards inside already travel, and stacking the two
   would move them twice as far as anything else on the page. */
.calendar-day-panel-body {
  animation: fadeIn 0.2s ease-out backwards;
}

.calendar-day-panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.8rem;
}

.calendar-day-panel-head h3 {
  margin: 0;
  font-size: var(--fs-base);
}

.calendar-day-panel-head p {
  margin: 0;
  color: var(--text-secondary);
  font-size: var(--fs-sm);
}

.dreams-list.calendar-dreams-list {
  margin-top: 0.25rem;
}

@media (min-width: 1001px) {
  .dreams-list.calendar-dreams-list {
    grid-template-columns: repeat(3, 1fr);
  }
}

.dream-card {
  background: none;
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  box-shadow: none;
  padding: 0.9rem 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  transition: background-color 0.18s ease, border-color 0.18s ease;
  isolation: isolate;
  cursor: pointer;
  position: relative;
}

.dream-card:last-child {
  border-bottom: none;
}
@media (hover: hover) and (pointer: fine) {
  .dream-card:hover {
    background-color: rgba(255, 255, 255, 0.022);
  }
}

.dream-card:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 3px;
}

.dream-card--pending {
  cursor: default;
  opacity: 0.6;
}

.dream-footer-meta {
  margin-top: auto;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.dream-tag-count {
  font-size: var(--fs-xs);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

/* meta row: date + visibility + AI badge */
.dream-meta {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  flex-wrap: nowrap;
  min-width: 0;
  overflow: hidden;
}

/* Same badge as the feed, off the same tokens. */
.dream-vis-pill {
  display: inline-flex;
  align-items: center;
  padding: 0.12rem 0.5rem;
  border-radius: 999px;
  border: none;
  font-size: var(--fs-micro);
  font-weight: 600;
  white-space: nowrap;
  flex-shrink: 0;
}

.dream-vis--public    { color: var(--vis-public);    background: var(--vis-public-soft); }
.dream-vis--followers { color: var(--vis-followers); background: var(--vis-followers-soft); }
.dream-vis--mutuals   { color: var(--vis-mutuals);   background: var(--vis-mutuals-soft); }
.dream-vis--anonymous { color: var(--vis-anonymous); background: var(--vis-anonymous-soft); }
.dream-vis--private   { color: var(--vis-private);   background: var(--vis-private-soft); }

.dream-ai-badge {
  font-size: 1.25rem;
  color: var(--accent);
  opacity: 0.9;
  flex-shrink: 0;
  letter-spacing: 0;
}

.dream-chevron {
  font-size: 1.25rem;
  color: var(--text-secondary);
  transition: transform 0.16s ease;
}
@media (hover: hover) and (pointer: fine) {
  .dream-card:hover .dream-chevron {
    color: var(--primary);
  }
}

.dream-title {
  /* User-authored text: break long unbroken strings instead of overflowing. */
  overflow-wrap: break-word;
  overflow-wrap: anywhere;
  font-weight: 700;
  letter-spacing: -0.015em;
  color: var(--text);
  font-size: var(--fs-lg);
  line-height: 1.25;
  margin: 0.1rem 0 0.15rem;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  line-clamp: 2;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

.dream-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
}

.dream-date {
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  white-space: nowrap;
  flex-shrink: 0;
}

.dream-content {
  /* User-authored text: break long unbroken strings instead of overflowing. */
  overflow-wrap: break-word;
  overflow-wrap: anywhere;
  color: var(--text);
  line-height: 1.5;
  font-size: var(--fs-base);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  line-clamp: 4;
  -webkit-line-clamp: 4;
  overflow: hidden;
  white-space: normal;
}

.dream-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.tags-section {
  display: grid;
  gap: 0.75rem;
}

.tags-input {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

.tags-input input {
  flex: 1;
  padding: 0.8rem 0.9rem;
  border-radius: 10px;
  border: 1px solid var(--control-border);
  background: var(--surface);
  color: var(--text);
}

.tags-input input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(127, 179, 194, 0.14);
}

.add-tag-btn {
  padding: 0.75rem 1rem;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--surface-muted);
  color: var(--text);
  font-weight: 600;
  transition: border-color 0.22s ease, color 0.22s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) and (pointer: fine) {
  .add-tag-btn:hover {
    border-color: var(--primary);
    color: var(--primary);
  }
}

.tags-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.tag .remove-tag {
  background: none;
  border: none;
  color: inherit;
  cursor: pointer;
  font-weight: 600;
  padding: 0 0.25rem;
}

.dream-textarea {
  width: 100%;
  border-radius: 14px;
  border: 1px solid var(--control-border);
  padding: 1rem;
  background: var(--surface);
  color: var(--text);
  font-size: var(--fs-base);
  line-height: 1.5;
  resize: none;
  min-height: 180px;
  transition: border-color 0.22s ease, box-shadow 0.22s ease;
  /* Ensure scrolling happens only within textarea, not the modal */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.dream-textarea:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(201, 167, 240, 0.18);
}

.modal-overlay {
  /* One colour for the sheet and for the fades that sit on top of it. They were
     three separate copies of the same hex, which is how they drift. */
  --modal-surface: #1f1c2b;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  backdrop-filter: blur(8px);
  padding: calc(env(safe-area-inset-top) + 1.25rem) 1.25rem calc(env(safe-area-inset-bottom) + 1.25rem);
  touch-action: none;
}

.modal-content {
  background: var(--modal-surface);
  border-radius: 20px;
  padding: 1.85rem;
  width: 100%;
  max-width: 580px;
  /* Against the overlay's own box, not a viewport unit. dvh does not mean the
     same thing in a web view as it does in a browser, which is how the panel
     ended up taller than the screen it was supposed to fit inside. */
  max-height: 100%;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  box-shadow: var(--raise-highlight), var(--shadow-soft);
  touch-action: pan-y;
}

/* Wrapper that holds the fade gradients outside the scrollable form */
.modal-form-wrap {
  flex: 1 1 auto;
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.modal-form-wrap::before,
.modal-form-wrap::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 3rem;
  pointer-events: none;
  z-index: 1;
  opacity: 1;
  transition: opacity 0.2s ease;
}

/* Two pixels above the scroll box rather than level with it, so the solid end
   of the fade covers the sliver of a line that the rounding leaves showing. */
.modal-form-wrap::before {
  top: -2px;
  height: calc(3rem + 2px);
  background: linear-gradient(to bottom, var(--modal-surface), transparent);
}

.modal-form-wrap::after {
  bottom: 0;
  background: linear-gradient(to top, var(--modal-surface), transparent);
}

.modal-form-wrap.at-top::before {
  opacity: 0;
}

.modal-form-wrap.at-bottom::after {
  opacity: 0;
}

/* The form scrolls internally; the page behind never scrolls while modal is open */
.modal-content form {
  flex: 1 1 auto;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: 0;
  touch-action: pan-y;
  scrollbar-width: none;
}

.modal-content form::-webkit-scrollbar {
  display: none;
}

/* Cancel, the title, and save. Save used to sit at the bottom of the form,
   below visibility, audience and tagging, so posting a dream meant scrolling
   past three sections you had already decided not to touch. */
.modal-header {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.85rem;
}

.modal-header h2 {
  margin: 0;
  font-size: var(--fs-lg);
  text-align: center;
  white-space: nowrap;
}

.modal-header-btn {
  justify-self: start;
  border: none;
  background: none;
  color: var(--text-secondary);
  font-size: var(--fs-base);
  font-weight: 600;
  padding: 0.5rem 0.1rem;
  min-height: 44px;
  cursor: pointer;
  transition: color 0.18s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) and (pointer: fine) {
  .modal-header-btn:hover:not(:disabled) {
    color: var(--text);
  }
}

.modal-header-btn:active:not(:disabled) {
  color: var(--text);
  transform: scale(0.94);
  transition: transform 70ms ease, color 70ms ease;
}

.modal-header-save {
  justify-self: end;
  padding: 0.55rem 1.15rem;
  min-height: 40px;
}

.close-btn {
  background: none;
  border: 1px solid var(--border);
  width: 38px;
  height: 38px;
  border-radius: 12px;
  display: grid;
  place-items: center;
  font-size: 1.1rem;
  color: var(--text-secondary);
  transition: border-color 0.22s ease, color 0.22s ease, background-color 0.22s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) and (pointer: fine) {
  .close-btn:hover {
    border-color: var(--primary);
    color: var(--primary);
    background: rgba(255, 255, 255, 0.04);
  }
}

/* Generating from the sheet, before the dream exists as a row. The button stays
   disabled until there is something to read, and the allowance it spends is
   worth saying out loud before it goes. */
.ai-compose {
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  align-items: flex-start;
}

.ai-compose-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

.ai-compose-mark {
  color: var(--accent);
}

.ai-compose-result {
  width: 100%;
  border-left: 2px solid rgba(232, 200, 122, 0.45);
  padding-left: 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.ai-compose-result-label {
  margin: 0;
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

.ai-compose-result-text {
  margin: 0;
  color: var(--text);
  line-height: 1.5;
  overflow-wrap: anywhere;
}

.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.75rem;
  margin-top: 1.25rem;
}

.visibility-section {
  margin-top: 1rem;
  padding: 1.1rem 0 0;
  border: none;
  border-top: 1px solid var(--border);
  border-radius: 0;
  background: none;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.visibility-section .section-label {
  font-weight: 600;
  margin: 0;
  color: var(--text);
}

.visibility-options {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.visibility-chip {
  border: 1px solid var(--border);
  border-radius: 100px;
  padding: 0.4rem 1rem;
  background: var(--surface);
  color: var(--text-secondary);
  font-weight: 600;
  transition: all 0.16s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}

@media (hover: hover) and (pointer: fine) {
  .visibility-chip:not(.active):hover {
    border-color: rgba(201, 167, 240, 0.45);
    color: var(--text);
  }
}

.visibility-chip.active {
  color: var(--text);
}

.visibility-chip.active[data-vis='public'] {
  color: #34d399;
  border-color: rgba(52, 211, 153, 0.5);
  background: rgba(52, 211, 153, 0.1);
  box-shadow: 0 0 0 2px rgba(52, 211, 153, 0.15);
}

.visibility-chip.active[data-vis='followers'] {
  color: #c9a7f0;
  border-color: rgba(201, 167, 240, 0.5);
  background: rgba(201, 167, 240, 0.1);
  box-shadow: 0 0 0 2px rgba(201, 167, 240, 0.15);
}

.visibility-chip.active[data-vis='mutuals'] {
  color: #f2a8b8;
  border-color: rgba(242, 168, 184, 0.5);
  background: rgba(242, 168, 184, 0.1);
  box-shadow: 0 0 0 2px rgba(242, 168, 184, 0.15);
}

.visibility-chip.active[data-vis='anonymous'] {
  color: #f59e0b;
  border-color: rgba(245, 158, 11, 0.5);
  background: rgba(245, 158, 11, 0.1);
  box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.15);
}

.visibility-chip.active[data-vis='private'] {
  color: #f87171;
  border-color: rgba(248, 113, 113, 0.5);
  background: rgba(248, 113, 113, 0.1);
  box-shadow: 0 0 0 2px rgba(248, 113, 113, 0.15);
}

.visibility-chip:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.visibility-helper {
  margin: 0;
  color: var(--text-secondary);
  font-size: var(--fs-base);
}

/* Hiding people and tagging people are both things most entries never use, so
   they sit behind a disclosure instead of adding two sections to every compose.
   Date and audience stay out in the open: those get set. */
/* Collapsed, this sits between its own rule and the one the ai block draws, and
   the label has to land in the middle of that band. The ai block contributes a
   1rem margin below the summary, so the same 1rem goes above it. It is also
   what every other section leaves under a rule. */
.compose-more {
  margin-top: 1rem;
  border-top: 1px solid var(--border);
  padding-top: 1rem;
}

.compose-more-toggle {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  width: 100%;
  min-height: 44px;
  padding: 0;
  background: none;
  color: var(--text-secondary);
  font-size: inherit;
  font-weight: 600;
  text-align: left;
  transition: color 0.18s ease;
}

.compose-more-toggle::after {
  content: '';
  width: 0.45rem;
  height: 0.45rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1);
}

.compose-more.is-open .compose-more-toggle::after {
  transform: translateY(1px) rotate(-135deg);
}
@media (hover: hover) and (pointer: fine) {
  .compose-more-toggle:hover {
    color: var(--text);
  }
}

.compose-more-toggle:active {
  color: var(--text);
}

/* The height is measured and set in js, because it is the one thing here that
   cannot be written down: the lists inside are any length. All this does is
   keep what has not been revealed yet out of sight. */
.compose-more-panel {
  height: 0;
  overflow: hidden;
  opacity: 0;
  transition: height 0.28s cubic-bezier(0.32, 0.72, 0, 1), opacity 0.2s ease;
}

.compose-more.is-open .compose-more-panel {
  opacity: 1;
}

/* The first section inside draws its own top rule, which would double up with
   the one the disclosure already has. */
.compose-more-panel > .audience-section {
  margin-top: 0;
  border-top: none;
  padding-top: 0.25rem;
}

@media (prefers-reduced-motion: reduce) {
  .compose-more-panel,
  .compose-more-toggle::after {
    transition: none;
  }
}

.audience-section,
.tag-people-section {
  margin-top: 1rem;
  padding: 1.1rem 0 0;
  border: none;
  border-top: 1px solid var(--border);
  border-radius: 0;
  background: none;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.control-headline {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.section-helper {
  margin: 0;
  color: var(--text-secondary);
  font-size: var(--fs-base);
}

.tag-people-input {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.tag-people-input input {
  flex: 1;
  min-width: 220px;
  padding: 0.8rem 0.95rem;
  border-radius: 12px;
  border: 1px solid var(--control-border);
  background: var(--surface);
  color: var(--text);
}

.tag-people-input input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(201, 167, 240, 0.15);
}

.tagged-pill-row {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.tagged-pill {
  display: inline-flex;
  gap: 0.35rem;
  align-items: center;
  padding: 0.35rem 0.8rem;
  border-radius: 100px;
  border: 1px solid var(--border);
  background: var(--surface-muted);
  color: var(--text);
  font-size: var(--fs-sm);
}

.tagged-pill button {
  border: none;
  background: transparent;
  color: inherit;
  cursor: pointer;
  font-size: var(--fs-base);
}

.status-hint {
  color: var(--primary);
}

.ai-hint {
  padding: 10px 0 0 0;
}

.tag-suggestion-list {
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  padding: 0.35rem;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  box-shadow: var(--shadow-light);
  margin-top: 0.35rem;
}

.tag-suggestion-item {
  border: none;
  background: transparent;
  text-align: left;
  padding: 0.5rem 0.75rem;
  border-radius: 10px;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  cursor: pointer;
  transition: background-color 0.15s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1), border-color 0.15s ease;
}
@media (hover: hover) and (pointer: fine) {
  .tag-suggestion-item:hover {
    background: rgba(201, 167, 240, 0.08);
  }
}

.suggestion-name {
  font-weight: 600;
  color: var(--text);
}

.suggestion-username {
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}

.alert-banner {
  margin: 0.5rem 0;
  padding: 0.75rem 1rem;
  border-radius: 12px;
  border: 1px solid rgba(208, 104, 104, 0.35);
  background: rgba(208, 104, 104, 0.08);
  color: #f9d6d6;
  font-weight: 600;
}

.dreams-loading {
  text-align: center;
  color: var(--text-secondary);
  padding: 3rem 1rem;
  font-size: var(--fs-base);
  animation: fadeIn 0.22s ease-out;
}

.journal-header {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0;
  animation: fadeIn 0.22s ease-out;
}

/* Title and the new dream button keep the page gutter; the tab bar underneath
   runs to both edges so its rule lines up with the row dividers below it. */
.journal-header-top {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  /* Explicit, because the mobile .page-header rule sets align-items: flex-start
     on the column above. Without it this row shrinks to its content and
     space-between has no room to push the button to the edge. */
  width: 100%;
  padding: 0 1.25rem 0.85rem;
}

.journal-header h1 {
  animation: slideInUp 0.22s ease-out 0.03s both;
}

.journal-header .page-subtitle {
  animation: slideInUp 0.22s ease-out 0.05s both;
}

.journal-header .action-group {
  animation: fadeIn 0.24s ease-out 0.08s both;
}


.dreams-list {
  animation: fadeIn 0.24s ease-out 0.1s both;
}

.journal-search-row {
  animation: fadeIn 0.22s ease-out 0.09s both;
}

.journal-search-input {
  width: 100%;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--control-border);
  border-radius: 12px;
  padding: 0.7rem 1rem;
  color: var(--text);
  font-size: var(--fs-base);
  transition: border-color 0.18s ease, background-color 0.18s ease;
  box-sizing: border-box;
}

.journal-search-input::placeholder {
  color: var(--text-secondary);
}

.journal-search-input:focus {
  outline: none;
  border-color: rgba(201, 167, 240, 0.45);
  background: rgba(255, 255, 255, 0.07);
}

.dream-card {
  animation: slideInUp 0.22s ease-out both;
}

/* A phone has less room to give away, so the panel keeps its gutter and spends
   the rest on the form. The outer gutter comes from the overlay, which already
   holds back off the status bar and the home indicator. */
@media (max-width: 768px) {
  .modal-overlay {
    padding-left: 1rem;
    padding-right: 1rem;
  }

  .modal-content {
    padding: 1.25rem;
  }

  /* The dream is the point of this screen, so it gets what is left. */
  .dream-textarea {
    min-height: 30dvh;
  }
}

@media (max-width: 640px) {
  .calendar-day,
  .calendar-spacer {
    min-height: 46px;
    transition: transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}

  .calendar-day-panel-head {
    flex-direction: column;
    align-items: flex-start;
  }

  .dream-card {
    padding: 0.85rem;
    gap: 0.45rem;
  }

  .dream-title {
    line-clamp: 2;
    -webkit-line-clamp: 2;
  }

  .dream-content {
    font-size: var(--fs-sm);
    line-clamp: 3;
    -webkit-line-clamp: 3;
  }

  .modal-actions {
    flex-direction: column-reverse;
    align-items: stretch;
  }
}

@media (max-width: 360px) {
  .dreams-list {
    grid-template-columns: 1fr;
  }
}

@media (prefers-reduced-motion: reduce) {
  .calendar-grid[data-month-direction],
  .calendar-toolbar h2[data-month-direction],
  .journal-calendar-shell,
  .calendar-day-panel-body {
    animation: none;
  }
}
.reaction-popover {
  position: fixed;
  z-index: 1200;
  pointer-events: none;
  padding-bottom: env(safe-area-inset-bottom);
}

.reaction-popover-card {
  pointer-events: auto;
  width: min(360px, 90vw);
  max-height: calc(100dvh - 160px);
  border-radius: 18px;
  background: rgba(15, 21, 36, 0.98);
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 0 22px 50px rgba(6, 8, 18, 0.45);
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  position: relative;
}

.reaction-popover-card::after {
  content: '';
  position: absolute;
  width: 16px;
  height: 16px;
  background: rgba(15, 21, 36, 0.98);
  border-left: 1px solid rgba(255, 255, 255, 0.08);
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  transform: translate(-50%, 0) rotate(45deg);
  left: var(--pointer-offset, 50%);
}

.reaction-popover.placement-top .reaction-popover-card::after {
  bottom: -8px;
}

.reaction-popover.placement-bottom .reaction-popover-card::after {
  top: -8px;
}

.reaction-popover-head {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.reaction-popover-title {
  margin: 0;
  color: var(--text, #fff);
  font-weight: 600;
  font-size: var(--fs-base);
}

.reaction-popover-subtitle {
  margin: 0;
  color: var(--text-secondary, rgba(255, 255, 255, 0.75));
  font-size: var(--fs-sm);
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

.reaction-popover-emoji {
  font-size: 1.15rem;
  display: inline-flex;
  align-items: center;
  color: var(--text, #fff);
}

.reaction-popover-emoji-heart {
  color: var(--primary, #5ac8fa);
}

.reaction-popover-empty {
  margin: 0;
  color: var(--text-secondary, rgba(255, 255, 255, 0.75));
}

.reaction-popover-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  overflow-y: auto;
}

.reaction-popover-row {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.45rem;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid rgba(255, 255, 255, 0.05);
}

/* Which one each person left, once the popover covers every emoji on a dream
   rather than one at a time. */
.reaction-popover-row-emoji {
  margin-left: auto;
  font-size: 1.1rem;
  line-height: 1;
}

.reaction-popover-avatar {
  width: 38px;
  height: 38px;
  border-radius: 12px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 1rem;
  box-shadow: var(--shadow-light, 0 4px 16px rgba(0, 0, 0, 0.25));
}

.reaction-popover-avatar svg {
  width: 1em;
  height: 1em;
}

.reaction-popover-meta {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.reaction-popover-name {
  color: var(--text, #fff);
  font-weight: 600;
  font-size: var(--fs-base);
}

.reaction-popover-username {
  color: var(--primary, #5ac8fa);
  font-size: var(--fs-xs);
  font-weight: 600;
}
.confirm-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  z-index: 550;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 0 0 env(safe-area-inset-bottom);
}

@media (min-width: 480px) {
  .confirm-modal-backdrop {
    align-items: center;
    padding: 1.5rem;
  }
}

.confirm-modal {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 20px 20px 0 0;
  width: 100%;
  max-width: 420px;
  padding: 1.75rem 1.5rem calc(1.75rem + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

@media (min-width: 480px) {
  .confirm-modal {
    border-radius: 20px;
    padding: 1.75rem 1.5rem;
  }
}

.confirm-modal-title {
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--text);
  margin: 0;
}

.confirm-modal-message {
  font-size: var(--fs-base);
  color: var(--text-secondary);
  margin: 0;
  line-height: 1.5;
}

.confirm-modal-input-label {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}

.confirm-modal-input {
  background: var(--surface-hover);
  border: 1px solid var(--control-border);
  border-radius: 10px;
  color: var(--text);
  padding: 0.65rem 0.85rem;
  font-size: var(--fs-base);
  outline: none;
  transition: border-color 0.15s ease;
}

.confirm-modal-input:focus {
  border-color: var(--primary);
}

.confirm-modal-actions {
  display: flex;
  gap: 0.75rem;
  justify-content: flex-end;
  margin-top: 0.25rem;
}
.dream-detail-page {
  padding-top: calc(88px + env(safe-area-inset-top) + 0.75rem);
}

.dream-detail-page .detail-card {
  margin-top: 0.75rem;
}

/* This page was boxes inside boxes: a card around the dream, another around the
   text of it, another around each section, another around every comment. Four
   borders deep before you reach a word anyone wrote. Sections are separated by
   rules now and the page itself is the only container. */
.detail-card {
  margin-top: 0.5rem;
  padding: 0;
  border-radius: 0;
  border: none;
  background: none;
  box-shadow: none;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  animation: slideInUp 0.24s ease-out 0.05s both;
}

.detail-toolbar {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  margin-bottom: 0.25rem;
  animation: fadeIn 0.22s ease-out;
}

.detail-head {
  display: flex;
  flex-wrap: nowrap;
  gap: 1rem;
  align-items: flex-start;
}

.detail-title-block {
  flex: 1;
  min-width: 240px;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: 0px;
}

.detail-title {
  /* User-authored text: break long unbroken strings instead of overflowing. */
  overflow-wrap: break-word;
  overflow-wrap: anywhere;
  text-align: left;
  justify-content: flex-start;
  /* It is an h1, so it was inheriting Prata italic 600 and coming out as the
     only dream title in the app set in the display serif. Same face and weight
     as the feed and journal titles now, just at page-heading size. */
  font-family: inherit;
  font-style: normal;
  font-weight: 700;
  font-size: var(--fs-2xl);
  letter-spacing: -0.02em;
  line-height: 1.2;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
}

.detail-title-editable {
  text-align: left;
  justify-content: flex-start;
  cursor: pointer;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: inherit;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  transition: transform 160ms cubic-bezier(0.22, 1, 0.36, 1), color 0.18s ease;
}

.detail-date-block {
  padding: 0;
  width: 100%;
  min-width: 160px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.75rem;
  align-self: stretch;
  flex-wrap: wrap;
}

.detail-edit-content-btn {
  margin-left: auto;
  flex-shrink: 0;
}

.detail-date-pill {
  border: none;
  border-radius: 999px;
  padding: 0.4rem 0.95rem;
  min-height: 40px;
  background: rgba(255, 255, 255, 0.05);
  color: var(--text-secondary);
  font-weight: 600;
  letter-spacing: 0.02em;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: var(--fs-base);
}

.detail-date-pill--interactive {
  position: relative;
  cursor: pointer;
  background: rgba(201, 167, 240, 0.05);
  color: var(--text);
  transition: border-color 0.18s ease, background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) and (pointer: fine) {
  .detail-date-pill--interactive:hover {
    border-color: var(--primary);
    box-shadow: 0 0 0 2px rgba(201, 167, 240, 0.15);
  }
}

/* 44px target without making the badge itself that tall, same overlay the other
   small controls use. */
.detail-date-pill--interactive::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: 44px;
  transform: translateY(-50%);
}

.detail-date-edit--standalone {
  justify-content: flex-end;
}

/* Always there. It used to appear on hover, which on a phone means never, so
   the one mark saying a title can be edited was invisible to almost everyone
   who could act on it. Purple because that is what interactive means here, and
   a size down so it hints rather than competing with the heading. */
.edit-hint {
  font-size: 0.6em;
  color: var(--primary);
  flex-shrink: 0;
}

.detail-title-edit,
.detail-date-edit {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 0.5rem;
}

.detail-title-input {
  padding: 0.6rem 0.9rem;
  border-radius: 10px;
  border: 1px solid var(--control-border);
  background: var(--surface);
  color: var(--text);
  font-size: var(--fs-xl);
  font-weight: 600;
  flex: 1;
  min-width: 200px;
}

.detail-title-input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(201, 167, 240, 0.15);
}

.detail-date-input {
  padding: 0.5rem 0.75rem;
  border-radius: 8px;
  border: 1px solid var(--control-border);
  background: var(--surface);
  color: var(--text);
  font-size: var(--fs-base);
}

.detail-date-input:focus {
  border-color: var(--primary);
}

.detail-visibility {
  border: none;
  border-top: 1px solid var(--border);
  border-radius: 0;
  padding: 1.1rem 0 0;
  background: none;
}

.detail-hint {
    margin: 0;
    color: var(--text-secondary);
    font-size: var(--fs-base);
}

.detail-audience,
.detail-tagged,
.detail-comments {
  border: none;
  border-top: 1px solid var(--border);
  border-radius: 0;
  padding: 1.1rem 0 0;
  background: none;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.detail-share-accordion {
  border: none;
  border-top: 1px solid var(--border);
  border-radius: 0;
  background: none;
  box-shadow: none;
  padding: 1.1rem 0 0;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.detail-share-toggle {
  border: none;
  background: transparent;
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  text-align: left;
  cursor: pointer;
  transition: opacity 0.18s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) and (pointer: fine) {
  .detail-share-toggle:hover {
    opacity: 0.82;
  }
}

/* Was a `›` glyph rotated 90deg to point down. Flex centres a glyph's inline
   box, not its ink, and `›` sits high in its box with uneven side bearings, so
   it never sat in the middle of the circle, and rotating moved that offset to
   a different edge, which is why it looked wrong both open and closed. An svg
   chevron is centred in its own viewBox, so it lands dead centre and stays
   there when the 180deg flip points it back up. */
.share-toggle-icon {
  font-size: 14px;
  color: var(--text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.04);
  transition: transform 0.2s ease;
}

.share-toggle-icon.open {
  transform: rotate(180deg);
}

.detail-share-panel {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.comments-list {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.comment-thread {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.comment-thread.nested {
  gap: 0.4rem;
}

.comment-thread-children {
  margin-left: 1.5rem;
  padding-left: 1rem;
  border-left: 1px solid rgba(255, 255, 255, 0.08);
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
}

.comment-card-reply {
  background: rgba(255, 255, 255, 0.03);
}

.comment-replies-toggle {
  align-self: flex-start;
  border: none;
  background: transparent;
  color: var(--primary);
  font-weight: 600;
  font-size: var(--fs-base);
  cursor: pointer;
  padding: 0.15rem 0;
  transition: transform 160ms cubic-bezier(0.22, 1, 0.36, 1), color 0.18s ease;
}

.comment-card {
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  padding: 0.8rem 0;
  background: none;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

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

.comment-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
}

.comment-meta-identity {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}

.comment-avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  font-size: 0.72rem;
}

.comment-meta-names {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  align-items: baseline;
}

.comment-author {
  font-weight: 600;
  color: var(--text);
}

.comment-handle {
  color: var(--text-secondary);
  font-size: var(--fs-sm);
}

.comment-time {
  color: var(--text-secondary);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
}

.comment-body {
  /* User-authored text: break long unbroken strings instead of overflowing. */
  overflow-wrap: break-word;
  overflow-wrap: anywhere;
  margin: 0;
  color: var(--text);
  white-space: pre-wrap;
}

.comment-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.comment-action-links {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

.comment-heart-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  position: relative;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.05);
  color: var(--text-secondary);
  padding: 0.35rem 0.8rem;
  font-size: var(--fs-base);
  font-weight: 600;
  cursor: pointer;
  transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* Comments are a dense list, so this one takes the 44px target as an overlay
   rather than as height it would have to spend on every row. */
.comment-heart-btn::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: 44px;
  transform: translate(-50%, -50%);
}

.comment-heart-btn svg {
  color: inherit;
}
@media (hover: hover) and (pointer: fine) {
  .comment-heart-btn:hover:not(:disabled) {
    border-color: rgba(201, 167, 240, 0.45);
    color: var(--text);
  }

  .comment-heart-btn.active:hover:not(:disabled) {
    color: var(--primary);
  }
}

.comment-heart-btn.active {
  border-color: rgba(255, 107, 138, 0.55);
  background: rgba(255, 107, 138, 0.12);
}

.comment-heart-btn.active svg {
  color: var(--like);
}

.comment-heart-btn:disabled {
  opacity: 0.6;
  cursor: wait;
}

.comment-heart-count {
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}

.comment-reply-context {
  margin: 0;
  color: var(--text-secondary);
  font-size: var(--fs-xs);
  font-style: italic;
}

.comment-reply-btn,
.comment-report-btn,
.comment-delete-btn {
  border: none;
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  cursor: pointer;
  text-decoration: underline;
  transition: color 0.18s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) and (pointer: fine) {
  .comment-reply-btn:hover:not(:disabled) {
    color: var(--primary);
  }
}
@media (hover: hover) and (pointer: fine) {
  .comment-report-btn:hover:not(:disabled) {
    color: var(--primary);
  }
}
@media (hover: hover) and (pointer: fine) {
  .comment-delete-btn:hover:not(:disabled) {
    color: var(--text);
  }
}

.comment-reply-btn:disabled,
.comment-report-btn:disabled,
.comment-delete-btn:disabled {
  opacity: 0.5;
  cursor: wait;
}

.comment-composer {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

/* Was a bare <button>, so it picked up no hover, no press and none of the
   button classes' transitions. */
.reply-cancel-btn {
  border: none;
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
  padding: 0.25rem 0.35rem;
  transition: color 0.18s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) and (pointer: fine) {
  .reply-cancel-btn:hover {
    color: var(--text);
  }
}

.reply-cancel-btn:active {
  color: var(--text);
  transform: scale(0.94);
  transition: transform 70ms ease, color 70ms ease;
}

.reply-context {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.03);
  padding: 0.45rem 0.75rem;
}

.reply-context span {
  color: var(--text-secondary);
  font-size: var(--fs-sm);
}

.reply-context button {
  border: none;
  background: transparent;
  color: var(--primary);
  font-weight: 600;
  cursor: pointer;
  font-size: var(--fs-sm);
}

.comment-composer textarea {
  width: 100%;
  min-height: 50px;
  border-radius: 12px;
  border: 1px solid var(--control-border);
  background: var(--surface);
  color: var(--text);
  padding: 0.75rem 0.9rem;
  line-height: 1.5;
  resize: none;
}

.comment-composer textarea:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(201, 167, 240, 0.15);
}

.comment-composer-actions {
  display: flex;
  justify-content: flex-end;
}

.composer-status {
  margin: 0;
  color: var(--text-secondary);
}

.detail-section-head {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.tag-people-input {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.tag-people-input input {
  flex: 1;
  min-width: 220px;
  padding: 0.75rem 0.9rem;
  border-radius: 12px;
  border: 1px solid var(--control-border);
  background: var(--surface);
  color: var(--text);
}

.tag-people-input input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(201, 167, 240, 0.15);
}

.tagged-pill-row {
  display: flex;
  gap: 0.45rem;
  flex-wrap: wrap;
}

.tagged-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.35rem 0.8rem;
  border-radius: 100px;
  border: 1px solid var(--border);
  background: var(--surface-muted);
  color: var(--text);
}

.tagged-pill button {
  border: none;
  background: transparent;
  color: inherit;
  cursor: pointer;
}

.tag-suggestion-list {
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  padding: 0.35rem;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  box-shadow: var(--shadow-light);
  margin-top: 0.35rem;
}

.tag-suggestion-item {
  border: none;
  background: transparent;
  text-align: left;
  padding: 0.5rem 0.75rem;
  border-radius: 10px;
  display: flex;
  flex-direction: column;
  gap: 0.12rem;
  cursor: pointer;
  transition: background-color 0.15s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1), border-color 0.15s ease;
}
@media (hover: hover) and (pointer: fine) {
  .tag-suggestion-item:hover {
    background: rgba(201, 167, 240, 0.08);
  }
}

.suggestion-name {
  font-weight: 600;
  color: var(--text);
}

.suggestion-username {
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}

.detail-label {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: var(--fs-xs);
  color: var(--text-secondary);
  margin-bottom: 0.6rem;
}

.detail-visibility-options {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.detail-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.detail-tags-inline {
  margin: 1rem 0 0.5rem;
}

@media (max-width: 640px) {
  .detail-head {
    flex-direction: column;
    gap: 0.85rem;
  }

  .detail-title-block,
  .detail-date-block {
    width: 100%;
  }
}

/* The poster's row also carries the actions you can take on their dream, at the
   right end of it. They used to be two outlined buttons in the footer, level
   with Close, which gave reporting the same weight as leaving the page. */
.detail-author-block {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.65rem 0 0.1rem;
}

/* Alone in the row on an anonymous dream, where there is no author to show. */
.detail-author-menu {
  margin-left: auto;
}

.detail-author-btn {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  background: transparent;
  border: none;
  padding: 0;
  cursor: pointer;
  text-align: left;
  color: inherit;
  transition: opacity 0.18s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) and (pointer: fine) {
  .detail-author-btn:hover {
    opacity: 0.8;
  }
}

.detail-author-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.detail-author-meta {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

.detail-author-name {
  font-weight: 600;
  font-size: var(--fs-base);
  color: var(--text);
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

.detail-author-handle {
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}

.detail-body {
  /* User-authored text: break long unbroken strings instead of overflowing. */
  overflow-wrap: break-word;
  overflow-wrap: anywhere;
  padding: 0;
  border-radius: 0;
  border: none;
  background: none;
  font-size: var(--fs-md);
  line-height: 1.6;
  color: var(--text);
  white-space: pre-wrap;
}

.detail-reactions {
  margin: 0.9rem 0 0.4rem;
  padding-top: 1rem;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}

/* The negative margin pays back the first chip's padding, so the heart lines up
   with the text above it rather than sitting a target's worth of padding in. */
.detail-reactions .reaction-buttons {
  margin-left: -0.7rem;
}

.detail-reactions .custom-emoji-popover {
  margin-top: 0.5rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  align-items: center;
}

.detail-reactions .custom-emoji-popover input {
  min-width: 140px;
  padding: 0.5rem 0.8rem;
  border-radius: 999px;
  border: 1px solid var(--control-border);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
}

.detail-reactions .custom-emoji-popover input:focus {
  border-color: rgba(201, 167, 240, 0.6);
  outline: none;
  box-shadow: 0 0 0 2px rgba(201, 167, 240, 0.2);
}

.detail-textarea {
  width: 100%;
  min-height: 180px;
  border-radius: 12px;
  border: 1px solid var(--control-border);
  background: var(--surface);
  color: var(--text);
  font-size: var(--fs-base);
  padding: 0.9rem 1rem;
  line-height: 1.5;
  margin-bottom: 1rem;
}

.detail-textarea:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(201, 167, 240, 0.15);
}

.detail-tags-editor {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.detail-tag-input-row {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

.detail-tag-input-row input {
  flex: 1;
  padding: 0.7rem 0.9rem;
  border-radius: 10px;
  border: 1px solid var(--control-border);
  background: var(--surface);
  color: var(--text);
}

.detail-tag-input-row input:focus {
  border-color: var(--primary);
}

.detail-tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.detail-tag-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.35rem 0.75rem;
  border-radius: 100px;
  background: var(--surface-muted);
  border: 1px solid var(--border);
}

.detail-tag-remove {
  background: none;
  border: none;
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  cursor: pointer;
  transition: color 0.18s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) and (pointer: fine) {
  .detail-tag-remove:hover {
    color: var(--text);
  }
}

.detail-edit-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.75rem;
}

/* The AI reading gets a rule down its left the way the feed summary does, so it
   reads as a second voice rather than as a tinted panel. */
.detail-summary {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  border: none;
  border-left: 2px solid rgba(232, 200, 122, 0.45);
  border-radius: 0;
  padding: 0.15rem 0 0.15rem 0.9rem;
  background: none;
  margin-top: 0.9rem;
}

.detail-summary-text {
  flex: 1 1 auto;
}

.detail-summary-footer {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.detail-summary-controls {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

@media (min-width: 1000px) {
  .detail-summary-footer {
    flex-direction: row;
    align-items: flex-start;
    gap: 1.25rem;
  }

  .detail-summary-controls {
    flex: 1 1 auto;
  }

  .ai-upgrade-cta {
    flex: 0 0 260px;
    margin-top: 0;
  }
}

.detail-summary h3 {
  margin: 0 0 0.25rem;
}

.detail-insight {
  /* User-authored text: break long unbroken strings instead of overflowing. */
  overflow-wrap: break-word;
  overflow-wrap: anywhere;
  margin: 0;
  color: var(--text);
}

.ai-disclaimer {
  margin: 0.55rem 0 0;
  color: var(--text-secondary);
  font-size: var(--fs-xs);
  line-height: 1.3;
}

.detail-connections {
  margin: 1rem 0 0;
  padding: 0.9rem 0 0;
  background: none;
  border: none;
  border-top: 1px solid var(--border);
  border-radius: 0;
}

.detail-connections-label {
  font-size: var(--fs-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--primary);
  margin: 0 0 0.55rem;
  opacity: 0.85;
}

.detail-connections-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.detail-connections-item {
  font-size: var(--fs-sm);
  color: var(--text-secondary);
  padding-left: 1rem;
  position: relative;
  line-height: 1.4;
}

.detail-connections-item::before {
  content: '↺';
  position: absolute;
  left: 0;
  color: var(--primary);
  opacity: 0.7;
  font-size: var(--fs-xs);
  top: 0.05em;
}

.ai-privacy-notice {
  margin: 0.75rem 0 0;
  padding: 0.75rem 1rem;
  background: rgba(216, 182, 255, 0.05);
  border-left: 3px solid rgba(157, 111, 212, 0.4);
  border-radius: 6px;
  color: var(--text-secondary);
  font-size: var(--fs-xs);
  line-height: 1.3;
}

.detail-insight.muted {
  color: var(--text-secondary);
  font-style: italic;
}

.detail-status-message {
  margin: 0.25rem 0 0.5rem;
  color: var(--primary);
  font-size: var(--fs-base);
  font-weight: 500;
}

.ai-quota-badge {
  margin: 0 0 0.5rem;
  font-size: var(--fs-xs);
  color: var(--text-secondary);
  font-style: italic;
}

.reanalyze-controls {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  position: relative;
}

.reanalyze-controls .ghost-btn {
  transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
@media (hover: hover) and (pointer: fine) {
  .reanalyze-controls .ghost-btn:hover:not(:disabled) {
    background: rgba(255, 255, 255, 0.08);
    border-color: var(--primary);
  }
}

/* First-analysis style picker. These classes had no CSS at all, so the picker
   rendered as unstyled text in the page flow and read as a stray expansion
   rather than as the choice it is.

   It stays inline rather than becoming an overlay on purpose: `.detail-card`
   has both a backdrop-filter and a `both`-filled animation, and either one
   makes it the containing block for `position: fixed` descendants, so a fixed
   overlay in here would size itself against the card, not the viewport. */
.prompt-selector-modal {
  display: block;
}

.prompt-selector-content {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 1rem;
  border-radius: 14px;
  border: 1px solid rgba(201, 167, 240, 0.28);
  background: rgba(201, 167, 240, 0.05);
  animation: slideInUp 0.22s ease-out backwards;
}

.prompt-selector-content h3 {
  margin: 0;
  font-size: var(--fs-base);
}

.prompt-selector-hint {
  margin: 0 0 0.35rem;
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}

@media (prefers-reduced-motion: reduce) {
  .prompt-selector-content {
    animation: none;
  }
}

/* Sits in the flow rather than hanging off the button. Absolutely positioned it
   did not extend the page's scrollable area, so when the button was near the
   bottom the last options were below everything you could ever scroll to.
   Inline it pushes the page down and the list is always reachable, and it now
   matches the first-analysis picker, which is the same choice in the same
   place. */
.prompt-selector-panel {
  margin-top: 0.25rem;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 1rem;
  border-radius: 14px;
  border: 1px solid rgba(201, 167, 240, 0.28);
  background: rgba(201, 167, 240, 0.05);
  animation: slideInUp 0.22s ease-out backwards;
}

@media (prefers-reduced-motion: reduce) {
  .prompt-selector-panel {
    animation: none;
  }
}

/* One column, because each row can open a description underneath it. Two
   columns left the expanded row dragging its neighbour's height with it. */
.prompt-options-list {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.prompt-selector-label {
  margin: 0;
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--text);
}

/* Every option is one row of the same rhythm, so the two panels line up with
   each other and the grid does not sit tighter than the button above it. */
/* Confirm sits right of cancel and carries the filled treatment, so the
   committing action is the one the eye lands on and the two are never mistaken
   for each other. Right aligned because it closes the panel. */
.prompt-selector-actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.35rem;
  flex-wrap: wrap;
}

.prompt-selector-actions .ghost-btn,
.prompt-selector-actions .primary-btn {
  padding: 0.55rem 1.1rem;
  font-size: var(--fs-sm);
}

.prompt-selector-actions .primary-btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.prompt-option-btn {
  width: 100%;
  display: flex;
  flex-direction: column;
  text-align: left;
  padding: 0.6rem 0.85rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  font-size: var(--fs-sm);
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}

.prompt-option-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
}

.prompt-option-label {
  font-weight: 600;
}

/* Collapsed by default and revealed by selection, so only one description is
   ever open. max-height rather than a height animation: the text is two lines
   at most, and auto height cannot be transitioned. */
.prompt-option-desc {
  display: block;
  max-height: 0;
  opacity: 0;
  overflow: hidden;
  color: var(--text-secondary);
  font-size: var(--fs-xs);
  line-height: 1.5;
  transition: max-height 0.22s ease, opacity 0.18s ease, margin-top 0.22s ease;
}

.prompt-option-btn.is-selected .prompt-option-desc {
  max-height: 9rem;
  opacity: 1;
  margin-top: 0.4rem;
}

/* The chosen style. This is a real selected state now. Something is picked and
   nothing has run yet, so it is allowed to look selected. */
.prompt-option-btn.is-selected {
  border-color: var(--primary);
  background: rgba(201, 167, 240, 0.12);
}

@media (prefers-reduced-motion: reduce) {
  .prompt-option-desc { transition: none; }
}
@media (hover: hover) and (pointer: fine) {
  .prompt-option-btn:hover:not(:disabled) {
    background: rgba(255, 255, 255, 0.08);
    border-color: var(--primary);
  }
}

.prompt-option-btn:disabled:not(.locked) {
  opacity: 0.5;
  cursor: wait;
}

.prompt-option-btn.locked {
  border-style: dashed;
  border-color: rgba(251, 191, 36, 0.35);
  opacity: 0.6;
  cursor: not-allowed;
  pointer-events: none;
  user-select: none;
}

.prompt-lock-icon {
  font-size: 0.7rem;
  margin-right: 0.35rem;
  color: rgba(251, 191, 36, 0.8);
  vertical-align: middle;
}

.prompt-pro-badge {
  display: inline-block;
  margin-left: 0.4rem;
  font-size: var(--fs-micro);
  font-weight: 600;
  padding: 0.1rem 0.35rem;
  border-radius: 0.25rem;
  background: rgba(251, 191, 36, 0.15);
  border: 1px solid rgba(251, 191, 36, 0.45);
  color: rgba(251, 191, 36, 0.9);
  vertical-align: middle;
  letter-spacing: 0.03em;
}

/* The style saved in Settings leads the list and spans the full width, so it
   reads as the top of the list rather than as whichever cell it happened to
   land in. The tint is what marks it; the tag is what stops the tint being
   read as "already chosen", which is the trap the old separate button fell
   into, so it looked selected while being the one thing not yet picked. */
.prompt-option-btn.current-prompt {
  border-color: rgba(255, 255, 255, 0.18);
}

.prompt-current-tag {
  margin-left: auto;
  flex-shrink: 0;
  padding: 0.1rem 0.4rem;
  border-radius: 0.25rem;
  border: 1px solid rgba(201, 167, 240, 0.4);
  background: rgba(201, 167, 240, 0.14);
  color: var(--primary);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: 0.03em;
}

.ai-upgrade-cta {
  border: 1px dashed rgba(251, 191, 36, 0.45);
  border-radius: 10px;
  padding: 0.75rem;
  background: rgba(251, 191, 36, 0.08);
}

.ai-upgrade-cta p {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}

.ai-upgrade-cta-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.6rem;
}

.detail-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.75rem;
}

.detail-error,
.detail-placeholder {
  margin-top: 2rem;
  padding: 1.25rem 0;
  border-radius: 0;
  border: none;
  background: none;
  color: var(--text-secondary);
  text-align: center;
}

.danger-btn {
  border-radius: 12px;
  padding: 0.8rem 1.2rem;
  font-weight: 600;
  border: 1px solid rgba(239, 68, 68, 0.4);
  background: rgba(239, 68, 68, 0.15);
  color: #ffe5e5;
  transition: background-color 0.16s ease, transform 0.16s ease;
}
@media (hover: hover) and (pointer: fine) {
  .danger-btn:hover:not(:disabled) {
    background: rgba(239, 68, 68, 0.28);
    border-color: rgba(239, 68, 68, 0.6);
  }
}

.danger-btn:disabled {
  opacity: 0.6;
  cursor: wait;
}

@media (max-width: 768px) {
  /* Nav is sticky on mobile, so it occupies flow space and needs no top padding */
  .dream-detail-page {
    padding-top: 0.75rem;
  }
}

@media (max-width: 640px) {
  .detail-card {
    padding: 1.5rem;
  }

  .comment-thread-children {
    margin-left: 1rem;
    padding-left: 0.75rem;
  }
}

/* ── Report modal ─────────────────────────────────────────────────────────── */
.report-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  z-index: 500;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 0 0 env(safe-area-inset-bottom);
}

@media (min-width: 480px) {
  .report-modal-backdrop {
    align-items: center;
    padding: 1.5rem;
  }
}

.report-modal {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 20px 20px 0 0;
  width: 100%;
  max-width: 480px;
  padding: 1.75rem 1.5rem calc(1.75rem + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

@media (min-width: 480px) {
  .report-modal {
    border-radius: 20px;
    padding: 1.75rem 1.5rem;
  }
}

.report-modal h3 {
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--text);
  text-transform: capitalize;
}

.report-modal-desc {
  font-size: var(--fs-sm);
  color: var(--text-secondary);
  margin: 0;
}

.report-reasons {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.report-reason-btn {
  padding: 0.5rem 0.9rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
  min-height: 44px;
}

.report-reason-btn.selected {
  border-color: #f87171;
  color: #f87171;
  background: rgba(248, 113, 113, 0.08);
}

.report-modal-actions {
  display: flex;
  gap: 0.75rem;
  justify-content: flex-end;
  margin-top: 0.25rem;
}

/* Generation placeholder. A button that only changes its label leaves the rest
   of the page looking exactly as it did, which is what reads as frozen. This
   puts the movement where the answer is going to appear. */
.analysis-pending {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  padding-top: 0.15rem;
}

.analysis-pending-line {
  height: 0.7rem;
  border-radius: 999px;
  background: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0.05) 0%,
    rgba(201, 167, 240, 0.16) 50%,
    rgba(255, 255, 255, 0.05) 100%
  );
  background-size: 200% 100%;
  animation: analysisShimmer 1.35s ease-in-out infinite;
}

.analysis-pending-line:nth-child(2) { width: 92%; animation-delay: 0.12s; }
.analysis-pending-line:nth-child(3) { width: 68%; animation-delay: 0.24s; }

@keyframes analysisShimmer {
  from { background-position: 140% 0; }
  to   { background-position: -40% 0; }
}

.analysis-pending-note {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0.35rem 0 0;
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}

/* The bars carry no information, so under reduced motion the spinner in the
   note is left to do the work alone. */
@media (prefers-reduced-motion: reduce) {
  .analysis-pending-line {
    animation: none;
  }
}
.feed-header {
  display: flex;
  flex-direction: column;
  gap: 0;
  animation: fadeIn 0.22s ease-out;
}

.feed-header h1 {
  animation: slideInUp 0.22s ease-out 0.03s both;
}

.feed-header-top {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  width: 100%;
  padding: 0 1.25rem 0.85rem;
  animation: slideInUp 0.22s ease-out 0.03s both;
}

/* ── Feed tabs ─────────────────────────────────────────────────────────────── */

.feed-tab-count {
  font-size: var(--fs-micro);
  background: rgba(255, 255, 255, 0.08);
  color: var(--text-secondary);
  padding: 0.1rem 0.42rem;
  border-radius: 999px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

.feed-tab.active .feed-tab-count {
  background: rgba(201, 167, 240, 0.18);
  color: var(--primary);
}

/* ── The stream ────────────────────────────────────────────────────────────────
 * A post used to announce itself six ways at once: a border, a gradient fill, a
 * shadow, a 12px blur, a 16px radius and its own padding. None of that is how a
 * feed reads. What separates one post from the next here is a hairline and the
 * avatar column, and what separates title from body is type, not a box.
 *
 * The stream also runs to both screen edges. The page gutter moved onto the
 * post itself, so the divider is a full width rule rather than a line floating
 * between two margins.
 * --------------------------------------------------------------------------- */
.feed-list {
  display: flex;
  flex-direction: column;
  animation: fadeIn 0.24s ease-out 0.1s both;
}

.feed-post {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: 0.7rem;
  padding: 0.9rem 1.25rem 0.75rem;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  animation: slideInUp 0.22s ease-out both;
  transition: background-color 0.18s ease;
}

.feed-post:last-child {
  border-bottom: none;
}
@media (hover: hover) and (pointer: fine) {
  .feed-post:hover {
    background-color: rgba(255, 255, 255, 0.022);
  }
}

/* The byline row. Name first, then everything else in one muted run, which is
   the line every social app opens a post with. */
.feed-post-head {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
  min-height: 30px;
}

.feed-post-byline {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
  min-width: 0;
  padding-top: 0.15rem;
}

.feed-post-meta {
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  white-space: nowrap;
}

.feed-card-actions {
  position: relative;
  display: flex;
  align-items: flex-start;
  margin-left: auto;
  flex-shrink: 0;
}

.feed-card-menu-btn {
  width: 30px;
  height: 30px;
  border-radius: 999px;
  border: none;
  background: none;
  color: var(--text-secondary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: border-color 0.18s ease, background-color 0.18s ease, color 0.18s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) and (pointer: fine) {
  .feed-card-menu-btn:hover {
    background: rgba(255, 255, 255, 0.06);
    color: var(--text);
  }
}

.feed-card-menu {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  z-index: 3;
  min-width: 200px;
  padding: 0.45rem;
  border-radius: 16px;
  border: 1px solid var(--border);
  background: rgba(10, 14, 22, 0.98);
  box-shadow: var(--shadow-soft);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.feed-card-menu-item {
  width: 100%;
  border: none;
  background: transparent;
  color: var(--text);
  text-align: left;
  border-radius: 12px;
  padding: 0.75rem 0.85rem;
  font-weight: 600;
  min-height: 44px;
  transition: background-color 0.18s ease, border-color 0.18s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) and (pointer: fine) {
  .feed-card-menu-item:hover {
    background: rgba(255, 255, 255, 0.06);
  }
}

.feed-card-menu-item-danger {
  color: #ff9fb1;
}
@media (hover: hover) and (pointer: fine) {
  .feed-card-menu-item-danger:hover {
    background: rgba(255, 138, 171, 0.12);
  }
}

/* Circular, and smaller. A 14px rounded square reads as an app icon; a circle
   reads as a person, which is the whole point of the avatar column. */
.feed-avatar {
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1rem;
  flex-shrink: 0;
}

.feed-avatar--anon {
  filter: grayscale(1);
}

.feed-avatar svg {
  width: 1.1em;
  height: 1.1em;
}

/* A person's name is not a tag. It was a tinted pill, which made the author
   read as metadata about the post rather than as the person who wrote it. */
.feed-author-handle {
  color: var(--text);
  font-size: var(--fs-base);
  font-weight: 600;
  background: none;
  border: none;
  padding: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.feed-author-handle.feed-author-link {
  text-decoration: none;
  transition: color 0.18s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) and (pointer: fine) {
  .feed-author-handle.feed-author-link:hover {
    color: var(--primary);
  }
}

/* With the box gone, type has to do the separating. The title takes the app's
   display serif so it is distinct from the body at a glance rather than by
   being one notch larger. */
.feed-title {
  /* User-authored text: break long unbroken strings instead of overflowing. */
  overflow-wrap: break-word;
  overflow-wrap: anywhere;
  /* A serif at 1.1rem against a sans at 0.95rem is only a 1.16 ratio, which the
     eye reads as the same rank in a different face rather than as a heading.
     At lg it is 1.37, and the extra space underneath stops the title and the
     first line of the dream running together as one block. */
  margin: 0.3rem 0 0.4rem;
  /* Not the display serif. Prata ships a single 400 weight, so asking it for
     bold gets a browser-smeared faux bold rather than a real face. The body
     sans has a true 700, which is what actually makes a title carry, and it is
     what every text feed uses for a post title anyway. Size still does half the
     work: lg over a base body is a 1.37 ratio. */
  font-weight: 700;
  font-size: var(--fs-lg);
  line-height: 1.25;
  letter-spacing: -0.015em;
  color: var(--text);
}

/* The AI reading is a second voice, so it is set off by a rule rather than by
   being loud. It used to be primary-coloured and semibold, which made it
   compete with the dream it was describing. */
.feed-summary {
  /* User-authored text: break long unbroken strings instead of overflowing. */
  overflow-wrap: break-word;
  overflow-wrap: anywhere;
  margin: 0.55rem 0 0;
  padding-left: 0.7rem;
  border-left: 2px solid rgba(232, 200, 122, 0.45);
  color: var(--text-secondary);
  font-weight: 400;
  font-size: var(--fs-sm);
  line-height: 1.45;
}

.feed-content {
  /* User-authored text: break long unbroken strings instead of overflowing. */
  overflow-wrap: break-word;
  overflow-wrap: anywhere;
  color: var(--text);
  line-height: 1.5;
  margin: 0;
}

.feed-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.65rem;
  margin-top: 0.55rem;
}

/* Tags were bordered pills, which is three containers deep inside a post. */
.feed-tags .tag {
  background: none;
  border: none;
  padding: 0;
  border-radius: 0;
  color: var(--primary);
  font-size: var(--fs-sm);
  font-weight: 500;
}

/* Bare icons with counts, the way every feed does it. The bordered chip made
   three reactions read as a row of buttons rather than as a footer. */
/* The negative margins pay back the first button's padding and absorb the row's
   own height, so a 44px target does not push the post's bottom edge down. */
.feed-reactions {
  margin: 0.15rem 0 -0.4rem -0.7rem;
}

.feed-reactions .reaction-button {
  margin-right: 0.5rem;
}

/* A quarter smaller than on the dream itself. There the row is the thing you
   came to use, here it repeats down a scrolling list, and at full size it
   carried more weight than the dream above it. The buttons keep their 44px
   targets either way, so only the glyph changes. */
.feed-reactions .reaction-icon {
  font-size: 1.125rem;
}

.feed-reactions .reaction-emoji-text {
  font-size: 1.25rem;
}

/* ── Empty + discovery states ──────────────────────────────────────────────── */
/* A dashed box around "nothing here yet" draws a container around an absence.
   Centred text in open space says it without the outline. */
.feed-empty-card {
  border: none;
  border-radius: 0;
  padding: 3rem 1.25rem;
  text-align: center;
  color: var(--text-secondary);
  background: none;
  box-shadow: none;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  align-items: center;
  animation: fadeIn 0.24s ease-out 0.1s both;
}

.feed-empty-card .primary-btn {
  margin-top: 0.5rem;
}

.feed-discovery-nudge {
  border: none;
  border-top: 1px solid var(--border);
  border-radius: 0;
  padding: 0.95rem 0 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  background: none;
  animation: fadeIn 0.22s ease-out both;
}

.feed-discovery-nudge p {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}

.feed-discovery-nudge button {
  border: none;
  background: transparent;
  color: var(--primary);
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  padding: 0;
  flex-shrink: 0;
}
@media (hover: hover) and (pointer: fine) {
  .feed-discovery-nudge button:hover {
    text-decoration: underline;
  }
}

/* ── Responsive ────────────────────────────────────────────────────────────── */
/* The stream is single column at every width now, so the grid collapse that
   used to live here has nothing left to do. A feed reading as two columns of
   tiles on a wide screen was most of why this looked like a dashboard. */
@media (max-width: 768px) {
  .feed-card-menu {
    min-width: 184px;
  }
}

/* ── Report modal (shared with DreamDetail) ──────────────────────────────── */
.report-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  z-index: 500;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 0 0 env(safe-area-inset-bottom);
}
@media (min-width: 480px) {
  .report-modal-backdrop { align-items: center; padding: 1.5rem; }
}
.report-modal {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 20px 20px 0 0;
  width: 100%;
  max-width: 480px;
  padding: 1.75rem 1.5rem calc(1.75rem + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
@media (min-width: 480px) {
  .report-modal { border-radius: 20px; padding: 1.75rem 1.5rem; }
}
.report-modal h3 { font-size: var(--fs-md); font-weight: 600; color: var(--text); text-transform: capitalize; }
.report-modal-desc { font-size: var(--fs-sm); color: var(--text-secondary); margin: 0; }
.report-reasons { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.report-reason-btn {
  padding: 0.5rem 0.9rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
  min-height: 44px;
}
.report-reason-btn.selected { border-color: #f87171; color: #f87171; background: rgba(248, 113, 113, 0.08); }
.report-modal-actions { display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 0.25rem; }
.navigation {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  padding-top: env(safe-area-inset-top);
  background: rgba(13, 13, 17, 0.7);
  backdrop-filter: blur(14px) saturate(150%);
  border-bottom: 1px solid var(--border);
  z-index: 100;
  height: calc(88px + env(safe-area-inset-top));
  transition: background-color 0.2s ease, border-color 0.2s ease, height 0.34s cubic-bezier(0.22, 1, 0.36, 1);
  transform: translateZ(0);
  will-change: background-color;
}

.navigation-compact {
  height: 60px;
  background: rgba(10, 14, 18, 0.9);
}

.nav-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 1rem;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  transform: translateZ(0);
}

.nav-logo {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 1.75rem;
  font-weight: bold;
  color: var(--text);
  letter-spacing: -0.02em;
  transition: color 0.2s ease, transform 0.3s cubic-bezier(0.22, 1, 0.36, 1), filter 0.2s ease;
  transform: translateZ(0) scale(1);
  transform-origin: left center;
  will-change: transform;
  backface-visibility: hidden;
}

.nav-logo span {
  transition: opacity 0.22s ease, max-width 0.22s ease, transform 0.22s ease;
  max-width: 11rem;
  opacity: 1;
  transform: translateX(0);
  overflow: hidden;
  white-space: nowrap;
  color: var(--text);
  font: 700 1.25rem "Playfair Display", serif;
  margin-bottom:-1px;
}

.nav-logo-icon {
  width: 36px;
  height: 36px;
  border-radius: 10px;
  transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1), border-radius 0.3s ease;
  transform: translateZ(0) scale(1);
  transform-origin: center;
  will-change: transform;
  backface-visibility: hidden;
  margin-right: -5px;
}
@media (hover: hover) and (pointer: fine) {
  .nav-logo:hover {
    color: var(--primary);
  }
}

.nav-links {
  display: flex;
  gap: 2rem;
  position: relative;
  align-items: center;
  padding-bottom: 12px;
  margin-top: 15px;
  overflow: visible;
  transform: translateZ(0);
  transition: margin-top 0.3s cubic-bezier(0.22, 1, 0.36, 1), padding-bottom 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}

.nav-links a {
  color: var(--text-secondary);
  font-weight: 600;
  transition: color 0.18s ease, transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
  padding: 0.35rem 0;
  min-height: 44px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  position: relative;
  transform: translateZ(0);
  will-change: transform;
  backface-visibility: hidden;
}
@media (hover: hover) and (pointer: fine) {
  .nav-links a:hover {
    color: var(--primary);
  }
}

.nav-links a.active {
  color: var(--primary);
}

.navigation:not(.navigation-compact) .nav-links a:not(.active) .nav-icon {
  opacity: 0.5;
}

.nav-icon-wrapper {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  min-width: 1.8rem;
  min-height: 1.8rem;
}

.nav-icon {
  font-size: 1.25rem;
  color: currentColor;
  opacity: 1;
  transition: opacity 0.24s ease, transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
  transform: translateZ(0) scale(1);
  will-change: transform, opacity;
  backface-visibility: hidden;
}

.nav-tab-label {
  display: inline-flex;
  align-items: center;
  line-height: 1;
  color: var(--text);
  transition: opacity 0.22s ease, transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
  transform: translateZ(0) translateY(0);
  will-change: transform, opacity;
  backface-visibility: hidden;
}

.nav-activity-indicator {
  position: absolute;
  top: -0.5rem;
  right: -0.5rem;
  transform: none;
  padding: 0.15rem 0.36rem;
  min-width: 0.95rem;
  border-radius: 999px;
  background: var(--primary);
  color: #05070b;
  font-size: 0.58rem;
  font-weight: 600;
  line-height: 1;
  text-align: center;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
  border: 1px solid rgba(5, 10, 14, 0.4);
  z-index: 2;
  scale: 0.9;
  font-variant-numeric: tabular-nums;
}

.navigation-compact .nav-icon-wrapper {
  min-width: 1.6rem;
  min-height: 1.6rem;
}

.navigation-compact .nav-activity-indicator {
  top: -0.25rem;
  right: -0.25rem;
  padding: 0.12rem 0.3rem;
  min-width: 0.85rem;
  font-size: 0.5rem;
}

.navigation-compact .nav-logo {
  transform: translateZ(0) scale(1.08);
}

.navigation-compact .nav-logo span {
  max-width: 0;
  opacity: 0;
  transform: translateX(-5px);
}

.navigation-compact .nav-logo-icon {
  transform: translateZ(0) scale(1);
  border-radius: 6px;
}

.navigation-compact .nav-links {
  margin-top: 0;
  padding-bottom: 0px;
  align-items: center;
}

.navigation-compact .nav-links a {
  flex-direction: column;
  gap: 0.12rem;
  align-items: center;
}

.navigation-compact .nav-icon {
  opacity: 0.18;
  transform: translateZ(0) translateY(-2px) scale(0.88);
  pointer-events: none;
}

.navigation-compact .nav-tab-label {
  transform: translateZ(0) translateY(-2px);
}

@media (max-width: 768px) {
  /* Keep icons visible on mobile even when compact */
  .navigation-compact .nav-icon {
    opacity: 1;
    transform: translateZ(0) scale(1);
    pointer-events: auto;
  }
  .navigation {
    position: fixed;
    top: 0;
    height: calc(74px + env(safe-area-inset-top));
  }

  .navigation.navigation-compact {
    height: calc(48px + env(safe-area-inset-top));
  }

  .nav-container {
    padding: 0 0.75rem;
  }

  .nav-logo {
    font-size: 1rem;
  }

  .nav-logo-icon {
    width: 30px;
    height: 30px;
  }

  .navigation:not(.navigation-compact) .nav-logo {
    transform: translateZ(0) scale(1);
  }

  .navigation.navigation-compact .nav-logo {
    transform: translateZ(0) scale(1);
  }

  .navigation:not(.navigation-compact) .nav-logo-icon {
    transform: translateZ(0) scale(0.78);
  }

  .navigation.navigation-compact .nav-logo-icon {
    transform: translateZ(0) scale(1);
  }

  .nav-links {
    flex: 1;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0 0.25rem 10px;
    overflow-x: auto;
    overflow-y: visible;
    scrollbar-width: none;
  }

  .nav-links::-webkit-scrollbar {
    display: none;
  }

  .navigation:not(.navigation-compact) .nav-links a {
    padding: 0.65rem 0.6rem;
    flex-direction: column;
    gap: 0.25rem;
  }

  .nav-links a {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 1 1 auto;
    min-width: 0;
    gap: 0.2rem;
    font-size: var(--fs-sm);
    padding: 0.35rem 0.4rem;
    border-radius: 12px;
    white-space: nowrap;
    flex-direction: row;
  }

  .navigation.navigation-compact .nav-links a {
    padding: 0.25rem 0.3rem;
    gap: 0.15rem;
    flex-direction: row;
    align-items: center;
    min-height: 44px;
  }

  .nav-icon {
    display: block;
    font-size: 1rem;
  }

  .navigation:not(.navigation-compact) .nav-icon {
    font-size: 1.3rem;
  }

  .nav-icon-wrapper {
    min-width: 1.4rem;
    min-height: 1.4rem;
  }

  .nav-tab-label {
    display: none;
    font-size: var(--fs-micro);
    line-height: 1;
  }

  .nav-activity-indicator {
    top: -0.35rem;
    right: -0.35rem;
    transform: none;
    padding: 0.13rem 0.28rem;
    min-width: 0.9rem;
    font-size: 0.55rem;
  }

  .navigation:not(.navigation-compact) .nav-tab-label {
    display: block;
  }
}

@media (max-width: 620px) {
  .nav-links {
    gap: 0.2rem;
  }

  .nav-links a {
    padding: 0.4rem 0.2rem;
  }
}

@media (max-width: 480px) {
  .nav-links a {
    gap: 0.2rem;
    font-size: var(--fs-xs);
    padding: 0.3rem 0.2rem;
  }
}

@media (min-width: 769px) {
  .navigation:not(.navigation-compact) .nav-links a {
    gap: 0.2rem;
  }

  .navigation:not(.navigation-compact) .nav-tab-label {
    margin-top: -0.08rem;
  }

  .navigation-compact .nav-links a {
    gap: 0.35rem;
    align-items: center;
  }

  .navigation-compact .nav-icon {
    opacity: 1;
    transform: translateZ(0) scale(0.92);
    pointer-events: auto;
    font-size: 1rem;
  }

  .navigation-compact .nav-tab-label {
    font-size: var(--fs-sm);
  }

  .nav-logo span {
    font-size: 1.5rem;
    margin-left: 5px;
    margin-bottom: 0;
  }
}

/* ── iOS native: top header ─────────────────────────────────────────────── */

.nav-ios-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: calc(52px + env(safe-area-inset-top));
  padding-top: env(safe-area-inset-top);
  display: flex;
  align-items: center;
  background: rgba(13, 13, 17, 0.7);
  backdrop-filter: blur(12px) saturate(140%);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
  box-shadow: 0 1px 12px rgba(0, 0, 0, 0.3);
  z-index: 100;
  transform: translateY(0px);
  will-change: transform;
}

/* Three-column layout: back | logo | spacer */
.nav-ios-header-side {
  flex: 1;
  display: flex;
  align-items: center;
  min-width: 52px;
}

.nav-ios-header-right {
  justify-content: flex-end;
}

.nav-ios-header-center {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
}

.nav-ios-back-btn {
  background: none;
  color: var(--primary);
  font-size: var(--fs-lg);
  padding: 0 0.75rem;
  min-width: 44px;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  -webkit-tap-highlight-color: transparent;
  transition: transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}

.nav-ios-logo-icon {
  width: 26px;
  height: 26px;
  border-radius: 7px;
  margin-bottom: 3px;
}

.nav-ios-logo-text {
  font-size: 1.15rem;
  font-weight: 400;
  letter-spacing: 0.01em;
  color: var(--text);
  font-family: 'Prata', serif;
}

/* ── iOS native: bottom tab bar ─────────────────────────────────────────── */

.nav-ios-tabbar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  height: calc(52px + env(safe-area-inset-bottom));
  padding-top: 15px;
  padding-bottom: env(safe-area-inset-bottom);
  display: flex;
  align-items: flex-start;
  background: rgba(13, 13, 17, 0.7);
  backdrop-filter: blur(10px) saturate(140%);
  -webkit-backdrop-filter: blur(10px) saturate(140%);
  border-top: 1px solid rgba(255, 255, 255, 0.07);
  box-shadow: 0 -1px 14px rgba(0, 0, 0, 0.32);
  z-index: 100;
  transform: translateZ(0);
  transition: transform 0.25s cubic-bezier(0.32, 0.72, 0, 1), opacity 0.18s ease;
}

/* `resize: 'native'` shrinks the web view, which would leave this fixed bar
   perched on top of the keyboard. A real UITabBar is covered by the keyboard,
   so slide it away instead, and give the text the room back. */
html.keyboard-open .nav-ios-tabbar {
  transform: translateZ(0) translateY(100%);
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .nav-ios-tabbar { transition: none; }
}

.nav-ios-tab {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  color: var(--text-secondary);
  -webkit-tap-highlight-color: transparent;
  transition: color 0.15s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}

.nav-ios-tab .nav-icon-wrapper {
  min-width: 1.9rem;
  min-height: 1.9rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

.nav-ios-tab .nav-icon {
  font-size: 1.6rem;
  transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1), color 0.15s ease;
  transform: translateZ(0) scale(1);
}

.nav-ios-tab .nav-tab-label {
  display: none;
}

/* 0.4 put the unselected icons at 2.46:1 against the bar, under the 3:1 that
   WCAG 1.4.11 asks of a control you are meant to be able to find and press. */
.nav-ios-tab:not(.active) {
  opacity: 0.52;
}

/* iOS tints the selected tab with the app's accent and greys the rest. This bar
   was separating them by brightness alone, a white icon against a dimmed grey
   one, which is a difference you only notice by comparing two icons to each
   other. Hue does not need the comparison: purple is the one you are on. */
.nav-ios-tab.active {
  color: var(--primary);
}

.nav-ios-tab.active .nav-icon {
  transform: translateZ(0) scale(1.06);
}
.offline-indicator {
  background: linear-gradient(90deg, #ff5050 0%, #ff6b6b 100%);
  color: white;
  padding: 0.75rem 1rem;
  text-align: center;
  font-size: var(--fs-sm);
  font-weight: 500;
  box-shadow: 0 2px 8px rgba(255, 80, 80, 0.25);
  position: fixed;
  top: calc(88px + env(safe-area-inset-top));
  left: 0;
  right: 0;
  z-index: 90;
}

@media (max-width: 768px) {
  .offline-indicator {
    top: calc(74px + env(safe-area-inset-top));
  }
}

.offline-indicator-text {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
}

.offline-indicator-text::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: white;
  display: inline-block;
  animation: pulse 2s infinite;
}

@keyframes pulse {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

@media (max-width: 600px) {
  .offline-indicator {
    padding: 0.5rem;
    font-size: var(--fs-xs);
  }
}
/* Hidden everywhere by default. The keyboard-open class is only ever set on the
   native iOS shell, so this never appears on the web build. */
.keyboard-dismiss {
  display: none;
}

html[data-native-platform='ios'].keyboard-open .keyboard-dismiss {
  position: fixed;
  /* `resize: 'native'` shrinks the web view to sit on top of the keyboard, so
     the bottom of the viewport is the top of the keyboard. */
  right: 12px;
  bottom: 12px;
  /* Above the composer modal (1000) and the reaction popover (1200): the dream
     composer is itself a modal with a textarea in it. */
  z-index: 1400;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 38px;
  padding: 0 1rem;
  border-radius: 999px;
  /* Filled rather than another dark translucent pill. It sits over whatever the
     user is writing on an all-dark UI, so a tinted surface with light text read
     as one more piece of chrome, the one control that has to be findable
     without looking for it is the one that should not blend in. */
  border: none;
  background: linear-gradient(135deg, #c9a7f0, #9d6fd4);
  box-shadow: 0 6px 20px rgba(157, 111, 212, 0.45), 0 2px 8px rgba(0, 0, 0, 0.4);
  color: #17112b;
  font-size: var(--fs-sm);
  font-weight: 700;
  -webkit-tap-highlight-color: transparent;
  animation: keyboardDismissIn 160ms cubic-bezier(0.32, 0.72, 0, 1) backwards;
}

.keyboard-dismiss svg {
  font-size: 0.72rem;
  opacity: 0.8;
}

.keyboard-dismiss:active {
  transform: scale(0.93);
  transition: transform 70ms ease;
}

@keyframes keyboardDismissIn {
  from { opacity: 0; transform: translate3d(0, 8px, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  html[data-native-platform='ios'].keyboard-open .keyboard-dismiss {
    animation: none;
  }
}
.error-boundary {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100dvh;
  padding: 2rem;
  background: linear-gradient(180deg, #0e0a14 0%, #17112b 55%, #0e0a14 100%);
}

.error-boundary-content {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 3rem;
  max-width: 500px;
  text-align: center;
  backdrop-filter: blur(10px);
}

.error-boundary-content h1 {
  font-size: var(--fs-2xl);
  margin-bottom: 1rem;
  color: var(--error);
}

.error-boundary-content p {
  color: var(--text-secondary);
  margin-bottom: 1.5rem;
  line-height: 1.5;
}

.error-details {
  margin: 1.5rem 0;
  text-align: left;
}

.error-details summary {
  cursor: pointer;
  color: var(--text-secondary);
  padding: 0.5rem;
  user-select: none;
}
@media (hover: hover) and (pointer: fine) {
  .error-details summary:hover {
    color: var(--text);
  }
}

.error-details pre {
  background: rgba(0, 0, 0, 0.5);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 1rem;
  margin-top: 1rem;
  overflow-x: auto;
  font-size: var(--fs-sm);
  color: var(--error);
  white-space: pre-wrap;
  word-break: break-word;
}

.error-boundary-button {
  background: var(--primary-strong);
  color: white;
  border: none;
  padding: 0.75rem 2rem;
  border-radius: 8px;
  font-size: var(--fs-base);
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) and (pointer: fine) {
  .error-boundary-button:hover {
    background: #0052cc;
    transform: translateY(-2px);
  }
}

.error-boundary-button:active {
  transform: translateY(0);
  transition: transform 80ms ease-out;
}
.terms-gate {
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  padding-bottom: calc(1.5rem + env(safe-area-inset-bottom));
  background: var(--bg);
}

.terms-gate-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 2rem 1.75rem;
  width: 100%;
  max-width: 400px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}

.terms-gate-icon img {
  width: 48px;
  height: 48px;
}

.terms-gate-title {
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--text);
  margin: 0;
  text-align: center;
}

.terms-gate-desc {
  font-size: var(--fs-base);
  color: var(--text-secondary);
  margin: 0;
  text-align: center;
  line-height: 1.5;
}

.terms-gate-checks {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  width: 100%;
}

.terms-gate-check {
  display: flex;
  /* Centred, not flex-start. The old 16px box was nudged down with a margin to
     sit level with the first line of text, which is the usual trick when the
     box is smaller than the line box. At 24px it no longer is, so the text
     block gets centred against the box and the nudge goes away. */
  align-items: center;
  gap: 0.65rem;
  font-size: var(--fs-sm);
  color: var(--text-secondary);
  line-height: 1.5;
  cursor: pointer;
  /* The label toggles the box, so the row is the real target. Holding it to 44px
     gives the shorter of the two consents a full-height target instead of one
     line of text. */
  min-height: 44px;
  padding: 0.25rem 0;
}

.terms-gate-check input[type='checkbox'] {
  flex-shrink: 0;
  /* Was 16px. 24px is the WCAG 2.5.8 minimum, and it matters here because people
     aim at the box even though the whole label works. */
  width: 24px;
  height: 24px;
  accent-color: var(--primary);
  cursor: pointer;
}


.terms-gate-btn {
  width: 100%;
  margin-top: 0.25rem;
}


* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:root {
  --safe-area-inset-top: env(safe-area-inset-top);
  --safe-area-inset-bottom: env(safe-area-inset-bottom);
  --primary: #c9a7f0;
  /* Links only. Brighter than --primary so it separates from body copy without
     losing ground against the background: 10.94:1, up from 9.48:1. */
  --link: #d9b3ff;
  --primary-strong: #9d6fd4;
  /* Two colours that are not the interface. Purple is the app talking; gold is
     the AI's voice and marks anything it wrote; red is a heart, which is a heart
     everywhere and the one place this palette should not be inventing its own
     convention. Everything else stays in the greys so these still land. */
  --accent: #e8c87a;
  --like: #ff6b8a;
  --background: #0d0d11;
  --background-secondary: #13121a;
  /* Overlays in violet rather than white. A white veil over a dark page makes
     grey, and grey everywhere is what reads as flat: the surfaces, the
     hairlines and the presses were all the same colourless film. Same lightness,
     carried at a slightly higher alpha because violet is darker than white. */
  --surface: rgba(205, 188, 245, 0.05);
  --surface-muted: rgba(205, 188, 245, 0.028);
  --text: #eeeafa;
  /* Secondary text used to be #cdbded, a 20% saturated purple. That sat only
     10 saturation points off --primary, so a link inside a sentence read as
     "slightly more purple" rather than as a link. Neutralising it widens that
     gap to 23 points and lets purple mean interactive across the whole app. It
     also lifts headings off body copy, 1.50:1 to 1.72:1. */
  --text-secondary: #b9b6c4;
  /* Under body copy, for the counts and labels that describe a thing rather
     than being it. 6.12:1 on the page, so it is quieter by hue and lightness
     without dropping under the floor. */
  --text-meta: #948cae;
  /* What a press paints, at the shape of the target rather than the glyph. */
  --press-surface: rgba(214, 200, 255, 0.09);
  /* Two of these on purpose. --border draws hairlines between rows and around
     panels, which are structure rather than controls, so they stay quiet. A
     text field is a control: its boundary is the only thing saying where to
     type, which puts it under WCAG 1.4.11 and its 3:1 floor. 0.34 over the page
     lands at 3.06:1. */
  --border: rgba(205, 188, 245, 0.09);
  /* This one stays white. Violet at the same lightness measures 2.0:1 against
     the page, well under the 3:1 a control's boundary owes, and no alpha fixes
     it: the hue is darker than white to begin with. A field's edge is the only
     thing saying where to type, so it keeps the contrast and loses the tint. */
  --control-border: rgba(255, 255, 255, 0.34);
  /* Depth, kept to one rule: light comes from above. A raised surface catches a
     hairline of it along its top edge, and a recessed one takes a shadow there
     instead. Both are one pixel and neither is a glow, which is the line
     between depth and tack. Stream rows stay flat on purpose: they are content,
     not objects sitting on the page. */
  /* Light has a colour and so does its absence. The highlight is warm, the
     shadows are a deep violet rather than black, which is the difference
     between a surface that is lit and a surface that is merely lighter. */
  --raise-highlight: inset 0 1px 0 rgba(255, 248, 236, 0.075);
  --sink-shadow: inset 0 1px 2px rgba(9, 5, 24, 0.36);
  --shadow-soft: 0 1px 2px rgba(9, 5, 24, 0.36), 0 14px 38px rgba(9, 5, 24, 0.4);
  --shadow-light: 0 1px 2px rgba(9, 5, 24, 0.3), 0 8px 22px rgba(9, 5, 24, 0.32);
  --glow-primary: 0 0 50px rgba(157, 111, 212, 0.22);
  --error: #ff5050;
  --success: #2cd44e;

  /* Dream visibility. One colour per audience, used by the feed badge and the
     profile pill, which were carrying the same five hex values separately. */
  --vis-public: #34d399;
  --vis-public-soft: rgba(52, 211, 153, 0.13);
  --vis-followers: #c9a7f0;
  --vis-followers-soft: rgba(201, 167, 240, 0.14);
  --vis-mutuals: #f2a8b8;
  --vis-mutuals-soft: rgba(242, 168, 184, 0.13);
  --vis-anonymous: #f59e0b;
  --vis-anonymous-soft: rgba(245, 158, 11, 0.13);
  --vis-private: #f87171;
  --vis-private-soft: rgba(248, 113, 113, 0.13);
  /* A wash of violet behind the top of the page. The app is read at night on a
     near black screen, and a flat one reads as switched off rather than dark. */
  --app-background-gradient:
    radial-gradient(130% 55% at 50% 0%, rgba(157, 111, 212, 0.17), transparent 62%),
    radial-gradient(110% 45% at 50% 100%, rgba(72, 108, 200, 0.11), transparent 72%),
    linear-gradient(180deg, #0d0d11 0%, #141024 48%, #0e0c16 100%);

  /* Type scale. One step per role. If two things are the same size they are
     meant to read as the same rank. Anything not on this scale was drift.
     Note the root shrinks to 14px under 768px, so every step scales with it.

     micro  count and status badges
     xs     timestamps, footnotes, helper text
     sm     secondary text, meta rows, small controls
     base   body copy and standard controls
     md     lead paragraphs, card titles, h3
     lg     sub-section headings
     xl     section headings, h2
     2xl    page titles, h1
     3xl    display

     Sizing a glyph, avatar or emoji is not typography, so those keep their own
     font-size values and are deliberately off this scale. */
  --type-scale: 1;
  --fs-micro: calc(0.7rem * var(--type-scale));
  --fs-xs: calc(0.78rem * var(--type-scale));
  --fs-sm: calc(0.85rem * var(--type-scale));
  --fs-base: calc(0.95rem * var(--type-scale));
  --fs-md: calc(1.1rem * var(--type-scale));
  --fs-lg: calc(1.3rem * var(--type-scale));
  --fs-xl: calc(1.55rem * var(--type-scale));
  --fs-2xl: calc(1.85rem * var(--type-scale));
  --fs-3xl: calc(2.25rem * var(--type-scale));
}

html {
  background: var(--app-background-gradient);
  background-color: #0d0d11;
  min-height: 100%;
  overscroll-behavior: none;
  scroll-behavior: smooth;
  color-scheme: dark;
}

body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-weight: 400;
  background: var(--app-background-gradient);
  background-color: #0e0a14;
  background-attachment: scroll;
  position: relative;
  color: var(--text);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  min-height: 100dvh;
  overscroll-behavior: contain;
}

html[data-native-platform='ios'] {
  overscroll-behavior: auto;
  /* Prevent WKWebView's black background from bleeding through on rubber-band scroll */
  background: var(--background);
  background-color: var(--background);
}

html[data-native-platform='ios'] body {
  overscroll-behavior: auto;
  background-color: var(--background);
}

/* Scrollbars are a pointer affordance. On touch the content already tracks your
   finger, and iOS itself only ever flashes a transient indicator, so a bar
   sitting in the gutter is the single loudest tell that this is a web page
   inside an app. The native build hides every one of them: the document, inner
   overflow panes, modal bodies, textareas. The web build keeps them, where with
   a mouse the bar is the only cue that there is more below.
   The main document's indicator is drawn by WKWebView's own UIScrollView rather
   than by CSS, so it is turned off natively in AppDelegate.swift. */
html[data-native-platform='ios'],
html[data-native-platform='ios'] * {
  scrollbar-width: none;
  -ms-overflow-style: none;
}

html[data-native-platform='ios']::-webkit-scrollbar,
html[data-native-platform='ios'] *::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none;
}

html.no-overscroll,
html.no-overscroll body {
  overscroll-behavior: none !important;
}

.page-title,
h1 {
  font-family: 'Prata', Georgia, serif;
  font-weight: 600;
  font-style: italic;
}

/* Headings carried no font-size at all, so any heading without a page specific
   rule fell through to the browser default of 2em/1.5em/1.17em, in em, which
   compounds with whatever it is nested in. That was the main reason the same
   rank of heading came out different sizes on different pages. */
h1 { font-size: var(--fs-2xl); }
h2 { font-size: var(--fs-xl); }
h3 { font-size: var(--fs-md); }

/* .dream-title was in this list, which is why the journal was the one page
   setting a dream title in the display serif. Dream titles are bold sans
   everywhere now, so it comes out. */
.entry-title,
.card-title {
  font-family: 'Prata', Georgia, serif;
  font-weight: 500;
}

h2, h3, .section-heading {
  font-weight: 500;
  color: #edeef6;
}

p {
  color: #d8d6e8;
}

.pull-refresh-indicator {
  position: fixed;
  top: calc(52px + env(safe-area-inset-top) + 24px);
  left: 50%;
  transform: translate(-50%, calc(-18px + (var(--pull-progress, 0) * 18px)));
  opacity: 0;
  pointer-events: none;
  z-index: 260;
  padding: 0.45rem 0.75rem;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(14, 10, 20, 0.86);
  color: #d7dfed;
  font-size: var(--fs-sm);
  letter-spacing: 0.01em;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  transition: opacity 160ms ease, transform 160ms ease, border-color 160ms ease;
}

.pull-refresh-indicator.is-visible {
  opacity: 1;
}

.pull-refresh-indicator.is-armed {
  border-color: rgba(201, 167, 240, 0.8);
  color: #eef5ff;
}

.pull-refresh-indicator.is-refreshing {
  border-color: rgba(201, 167, 240, 0.8);
  color: #eef5ff;
  animation: pullRefreshPulse 900ms ease-in-out infinite;
}

@keyframes pullRefreshPulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.55; }
}

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

/* Page entrance. `backwards` fill only, because a lingering `transform` on a page root
   would make it the containing block for every `position: fixed` modal and
   toast inside it, so the transform must be gone once the animation ends. */
@keyframes pageEnter {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

.page-container,
.insights-page,
.home-page {
  animation: pageEnter 200ms ease-out backwards;
}

/* iOS: replace the vertical fade with a directional slide, so a push reads as
   coming from the right and a back gesture as returning from the left. It is the
   single strongest cue that this is an app rather than a page. The curve is
   UIKit's navigation ease. Tabs keep the plain fade. */
@keyframes pageEnterForward {
  from { opacity: 0; transform: translate3d(20px, 0, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

@keyframes pageEnterBack {
  from { opacity: 0; transform: translate3d(-20px, 0, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

html[data-native-platform='ios'] [data-nav-direction='forward'] .page-container,
html[data-native-platform='ios'] [data-nav-direction='forward'] .insights-page {
  animation: pageEnterForward 260ms cubic-bezier(0.32, 0.72, 0, 1) backwards;
}

html[data-native-platform='ios'] [data-nav-direction='back'] .page-container,
html[data-native-platform='ios'] [data-nav-direction='back'] .insights-page {
  animation: pageEnterBack 260ms cubic-bezier(0.32, 0.72, 0, 1) backwards;
}

html[data-native-platform='ios'] [data-nav-direction='tab'] .page-container,
html[data-native-platform='ios'] [data-nav-direction='tab'] .insights-page {
  animation: pageEnter 170ms ease-out backwards;
}

@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

/* Inline activity spinner. Sized from the current font-size so it drops into a
   button label without needing its own dimensions. Static text alone ("Working
   …") reads as a frozen screen; something has to actually move. */
.inline-spinner {
  display: inline-block;
  width: 1em;
  height: 1em;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  opacity: 0.75;
  flex-shrink: 0;
  animation: inlineSpin 0.7s linear infinite;
}

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

/* Small round controls sit below the 44px minimum a finger actually needs. The
   fix is an invisible overlay rather than a bigger circle: the design keeps the
   size it was drawn at, the target grows, and nothing on the page shifts. */
.voice-input-btn,
.close-btn,
.feed-card-menu-btn {
  position: relative;
}

.voice-input-btn::after,
.close-btn::after,
.feed-card-menu-btn::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

/* The target you pressed, at the size it really is. These overlays were hit
   area and nothing else: a 44px reach around a 16px glyph that was never drawn,
   so what you were aiming at only became apparent when you missed it. On press
   they take a surface at their own bounds. Fast in because it has to answer the
   finger, slow out because nothing here should snap. */
.voice-input-btn::after,
.close-btn::after,
.feed-card-menu-btn::after,
.overflow-menu-btn::after,
.feed-tab::after,
.toggle-switch::after,
.profile-toggle-switch::after,
.activity-clear-btn::after,
.comment-heart-btn::after,
.detail-date-pill--interactive::after,
.profile-dreams-head .dream-tab::after {
  border-radius: 999px;
  background-color: transparent;
  transition: background-color 280ms ease;
}

.voice-input-btn:active:not(:disabled)::after,
.close-btn:active:not(:disabled)::after,
.feed-card-menu-btn:active:not(:disabled)::after,
.overflow-menu-btn:active:not(:disabled)::after,
.feed-tab:active::after,
.toggle-switch:active::after,
.profile-toggle-switch:active::after,
.activity-clear-btn:active:not(:disabled)::after,
.comment-heart-btn:active:not(:disabled)::after,
.detail-date-pill--interactive:active::after,
.profile-dreams-head .dream-tab:active::after {
  background-color: var(--press-surface);
  transition-duration: 90ms;
}

/* Same idea for controls that are already wide enough and only short: a feed
   tab is 34px tall and a toggle 28px, so the overlay grows the height and
   leaves the width alone. The toggles are the ones that matter, since only the
   switch itself is the target here, not the settings row around it. */
.feed-tab,
.toggle-switch,
.profile-toggle-switch {
  position: relative;
}

.feed-tab::after,
.toggle-switch::after,
.profile-toggle-switch::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 44px;
  transform: translateY(-50%);
}

.btn-loading {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
}

/* Reduced motion slows the spinner rather than stopping it, because a frozen ring is
   exactly the "is this broken?" signal it exists to prevent. */
@media (prefers-reduced-motion: reduce) {
  .inline-spinner { animation-duration: 2.4s; }
}

@keyframes glowSweep {
  0% { transform: translateX(-120%); opacity: 0; }
  45% { opacity: 0.35; }
  100% { transform: translateX(160%); opacity: 0; }
}

#root {
  min-height: 100dvh;
  position: relative;
  z-index: 2;
}

button {
  font-family: inherit;
  cursor: pointer;
  border: none;
  outline: none;
}

button:focus-visible,
a:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

input, textarea, select {
  font-family: inherit;
  outline: none;
}

/* The browser's resize grabber is drawn in the corner of the box, so a rounded
   textarea clips it into a stub. It is also dead weight on a phone, where there
   is no drag to perform. Fields size themselves instead. */
textarea {
  resize: none;
}

a {
  text-decoration: none;
  color: inherit;
}

/* Text fields are the one thing in the app that should read as set into the
   page rather than sitting on it, which is the other half of having a light
   source. One list, because the alternative is the same declaration copied into
   twenty component rules and drifting. */
.dream-title-input,
.dream-textarea,
.detail-textarea,
.detail-title-input,
.comment-composer textarea,
.profile-input,
.profile-textarea,
.auth-input,
.journal-search-input,
.search-input-wrap input,
.settings-time-input,
.custom-prompt-area textarea,
.password-fields input,
.confirm-modal-input {
  box-shadow: var(--sink-shadow);
}

/* Links sitting inside a sentence. Colour cannot carry these on its own here:
   clearing 3:1 against body copy would need a link brighter than white or dark
   enough to fail against the background, so the underline is what actually does
   the work. It is always on, because a hover-only underline never fires on a
   phone, which is where all of these are read. Defined once and nowhere else,
   so no component stylesheet can win the cascade and take it back off. */
.terms-gate-check a,
.auth-policy-check a,
.auth-legal-links a,
.home-footer a,
.settings-footnote a,
.legal-card a {
  color: var(--link);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  text-decoration-color: rgba(217, 179, 255, 0.5);
  transition: color 0.18s ease, text-decoration-color 0.18s ease, opacity 0.18s ease;
}

/* A link is the one control in the app you can tap without anything moving, so
   it needs a press of its own: the underline fills in and the whole thing dims
   for as long as the finger is down. */
@media (hover: hover) and (pointer: fine) {
  .terms-gate-check a:hover,
  .auth-policy-check a:hover,
  .auth-legal-links a:hover,
  .home-footer a:hover,
  .settings-footnote a:hover,
  .legal-card a:hover {
    text-decoration-color: var(--link);
  }
}

.terms-gate-check a:active,
.auth-policy-check a:active,
.auth-legal-links a:active,
.home-footer a:active,
.settings-footnote a:active,
.legal-card a:active {
  opacity: 0.65;
  text-decoration-color: var(--link);
  transition: opacity 60ms ease-out, text-decoration-color 60ms ease-out;
}

.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 1rem;
}

.app-main--with-nav {
  padding-top: calc(88px + env(safe-area-inset-top));
}

html[data-native-platform='ios'] .app-main--with-nav {
  padding-top: calc(52px + env(safe-area-inset-top));
}

/* Reserve the tab bar's height on iOS, the home indicator's everywhere else. */
html[data-native-platform='ios'] .app-main {
  padding-bottom: calc(49px + env(safe-area-inset-bottom));
}

html:not([data-native-platform='ios']) .app-main {
  padding-bottom: env(safe-area-inset-bottom);
}

/* The keyboard hides the tab bar, so stop holding space for it, because otherwise the
   last line of a long entry sits behind a gap that no longer has anything in it. */
html[data-native-platform='ios'].keyboard-open .app-main {
  padding-bottom: 0;
}

.page-container {
  max-width: 1100px;
  margin: 0 auto;
  padding: 24px 1.25rem 50px;
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
}

.page-header h1 {
  font-size: var(--fs-3xl);
  letter-spacing: -0.02em;
  color: #eef2fb;
}

.page-subtitle {
  color: var(--text-secondary);
  font-size: var(--fs-base);
}

.action-group {
  display: flex;
  gap: 12px;
  align-items: center;
}

.primary-btn,
.secondary-btn,
.ghost-btn {
  border-radius: 12px;
  padding: 0.8rem 1.2rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  border: 1px solid transparent;
  transition: filter 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease, color 0.25s ease, opacity 0.22s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
  background: var(--surface);
  color: var(--text);
  backdrop-filter: blur(10px);
}

.primary-btn {
  background: linear-gradient(135deg, #120d24, #1e1535);
  color: #ede8f5;
  border-color: rgba(255, 255, 255, 0.12);
  box-shadow: var(--raise-highlight), 0 10px 30px rgba(14, 10, 20, 0.2);
  position: relative;
  overflow: hidden;
}

.primary-btn::after {
  content: '';
  position: absolute;
  inset: -20%;
  background: linear-gradient(120deg, transparent 0%, rgba(255, 255, 255, 0.28) 45%, transparent 70%);
  transform: translateX(-120%);
  animation: glowSweep 8s ease-in-out infinite;
}

@media (hover: hover) and (pointer: fine) {
  .primary-btn:hover {
    filter: brightness(1.12);
    box-shadow: var(--shadow-soft), 0 0 20px rgba(201, 167, 240, 0.15);
  }
}

.primary-btn:disabled {
  opacity: 0.25;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

.secondary-btn {
  background: var(--surface-muted);
  color: var(--text);
  border-color: var(--border);
  box-shadow: var(--raise-highlight);
}

@media (hover: hover) and (pointer: fine) {
  .secondary-btn:hover {
    border-color: var(--primary);
    color: var(--primary);
  }
}

/* This used to set `background: transparent`, which cancelled the surface the
   shared button rule gives it. With a transparent border as well there was
   nothing left to see: "Regenerate with different prompt" and "Find people"
   were indistinguishable from the text around them. Stripping containers off
   content is the point of the revamp; a control is the opposite case and has to
   look pressable. */
.ghost-btn {
  background: var(--surface);
  border-color: var(--border);
  color: var(--text);
}

/* For buttons that sit inline with something else rather than in a button row. */
.ghost-btn--compact {
  padding: 0.4rem 0.85rem;
  font-size: var(--fs-sm);
  border-radius: 999px;
  min-height: 34px;
}

.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0.25rem 0.6rem;
  border-radius: 100px;
  color: var(--text-secondary);
  font-size: var(--fs-base);
  border: 1px solid var(--border);
  line-height: 1;
  transition: border-color 0.18s ease, color 0.18s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}

.pill-active {
  background: rgba(201, 167, 240, 0.1);
  color: var(--text);
  border-color: var(--primary);
  box-shadow: var(--glow-primary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.25rem 0.8rem;
}

*:not(input):not(textarea) {
  -webkit-tap-highlight-color: transparent;
}

button, a, [role="button"] {
  touch-action: manipulation;
}

.primary-btn, .secondary-btn, .ghost-btn {
  min-height: 44px;
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}


.card-shell {
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 1.5rem;
  box-shadow: var(--shadow-light);
  transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.16s ease, background-color 0.16s ease;
  backdrop-filter: blur(14px);
}

@media (hover: hover) and (pointer: fine) {
  .card-shell:hover {
    box-shadow: var(--shadow-soft);
    background: rgba(255, 255, 255, 0.07);
    border-color: rgba(255, 255, 255, 0.14);
  }
}

.empty-state {
  text-align: center;
  padding: 3rem 2rem;
  border: 1px dashed var(--border);
  border-radius: 14px;
  color: var(--text-secondary);
  background: var(--surface-muted);
  line-height: 1.7;
}

.empty-subtitle {
  color: var(--text-secondary);
  margin-top: 0.35rem;
}

.app-loading-shell {
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
}

.lazy-route-loading {
  min-height: calc(100dvh - 120px - env(safe-area-inset-top) - env(safe-area-inset-bottom));
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem 1rem calc(2rem + env(safe-area-inset-bottom));
}

html[data-native-platform='ios'] .lazy-route-loading {
  padding-top: calc(2rem + env(safe-area-inset-top) + 56px);
}

/* On iOS WebKit, transform is unreliable on flex items and the ✦ glyph has a
   larger bounding box than its visual. Collapsing line-height removes the
   oversized box so the glyph overflows symmetrically, then position:relative
   nudges it without triggering the flex+transform bug. */
html:not([data-native-platform='ios']) .dream-ai-badge {
  font-size: 1.25rem;
  margin-top: -0.1rem;
}

html[data-native-platform='ios'] .dream-ai-badge{
  line-height: 0;
  position: relative;
  top: -3px;
}

html[data-native-platform='ios'] .profile-ai-badge {
  line-height: 0;
  position: relative;
  top: -1px;
}

.loading-slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
}

.loading-inline {
  padding: 0.35rem 0;
}

.pro-badge {
  display: inline-flex;
  align-items: center;
  padding: 0.2rem 0.42rem;
  border-radius: 5px;
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
  padding-top: 0.3rem;
  color: #fbbf24;
  background: rgba(251, 191, 36, 0.12);
  border: 1px solid rgba(251, 191, 36, 0.35);
  vertical-align: middle;
  line-height: 1;
  flex-shrink: 0;
}

/* A page whose main content is a stream runs edge to edge, so the tab bar and
   the row dividers can reach both sides. The gutter moves onto each row. Direct
   children that are not the stream keep the old gutter as a margin, so headers,
   banners and empty states stay inset. */
.stream-page {
  padding-left: 0;
  padding-right: 0;
  gap: 0;
}

/* Written as a base plus overrides rather than one long `:not()` chain. Every
   `:not(.foo)` carries the specificity of its argument, so nine of them made
   that selector (0,10,0) and it beat every attempt to opt an element out. This
   way the default is (0,1,0) and each exemption is (0,2,0), which is the order
   you would expect. */
.stream-page > * {
  margin: 1rem 1.25rem 0;
}

/* Full bleed: these run to both screen edges and each row pays for its own
   gutter, so the dividers reach the sides. Headers are here too because they
   carry their own padded row, which keeps the tab bar underneath full width. */
.stream-page > .feed-list,
.stream-page > .dreams-list,
.stream-page > .activity-list,
.stream-page > .connections-list,
.stream-page > .connection-list,
.stream-page > .search-results,
.stream-page > .activity-section,
.stream-page > .search-box,
.stream-page > .page-header,
.stream-page > .activity-head,
.stream-page > .connections-header,
.stream-page > .connections-tab-group,
.stream-page > .profile-dreams {
  margin: 0;
}

/* ── Tab bars ──────────────────────────────────────────────────────────────────
 * One pattern for every set of tabs in the app. They were four different
 * controls doing the same job: a pill inside a bordered track on the feed, a
 * segmented control on the journal, a rounded box on the profile, and a grid of
 * outlined buttons on connections. An underline says "you are here" without any
 * of them, and the tabs sit at their label width so the bar lines up with the
 * heading above rather than stretching across the screen.
 *
 * Listed by page selector rather than a shared class so the markup does not
 * have to change on five pages. New tab sets should use `.tab-bar`.
 * --------------------------------------------------------------------------- */
.tab-bar,
.feed-tabs,
.view-toggle,
.dream-tab-group,
.connections-tab-group {
  display: flex;
  gap: 1.5rem;
  width: 100%;
  padding: 0 1.25rem;
  margin: 0;
  background: none;
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  overflow: visible;
  grid-template-columns: none;
}

.tab-bar-btn,
.feed-tab,
.view-toggle-btn,
.dream-tab,
.connections-tab {
  position: relative;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  min-height: 44px;
  padding: 0.6rem 0.1rem;
  border: none;
  border-radius: 0;
  background: none;
  box-shadow: none;
  color: var(--text-secondary);
  font-size: var(--fs-md);
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  width: auto;
  transition: color 0.18s ease, background-color 0.18s ease, border-color 0.18s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* Tabs had no press state at all, so tapping one did nothing until the list
   underneath changed. There is no border or background here to move, so the
   press is the label itself: it lights to full white and takes the same small
   scale the other label-sized controls take. */
.tab-bar-btn:active,
.feed-tab:active,
.view-toggle-btn:active,
.dream-tab:active,
.connections-tab:active {
  color: var(--text);
  transform: scale(0.94);
  transition: transform 70ms ease, color 70ms ease;
}

.tab-bar-btn.active,
.feed-tab.active,
.view-toggle-btn.active,
.dream-tab.active,
.connections-tab.active {
  color: var(--text);
  background: none;
  border: none;
  box-shadow: none;
}

/* ::before, because ::after is the 44px touch overlay. The bar spans the tab,
   and the tab is the width of its label, so it underlines the word. Every tab
   set in the app selects the same way: this bar and a step up in text colour,
   nothing else. .dream-tab was carrying a bold weight instead of the bar, from
   when those two tabs sat directly under the follower counts and a second rule
   that close to them read as a stray line. They live on the dreams heading row
   now, so there is nothing for the bar to collide with. */
.tab-bar-btn.active::before,
.feed-tab.active::before,
.view-toggle-btn.active::before,
.dream-tab.active::before,
.connections-tab.active::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2.5px;
  border-radius: 2px 2px 0 0;
  background: var(--primary);
}

@media (hover: hover) and (pointer: fine) {
  .tab-bar-btn:not(.active):hover,
  .feed-tab:not(.active):hover,
  .view-toggle-btn:not(.active):hover,
  .dream-tab:not(.active):hover,
  .connections-tab:not(.active):hover {
    color: var(--text);
    background: none;
  }
}

/* The heart is the one control that keeps its own colour. Everything else in a
   reaction row is the interface, and the interface is purple. */
.reaction-button--heart.active,
.comment-heart-btn.active {
  color: var(--like);
}

/* ── Reactions ────────────────────────────────────────────────────────────────
 * The heart is an outline until you press it and a fill afterwards, which is
 * what a heart means everywhere else. It is drawn by stroking the solid glyph's
 * own path, so the app does not carry a second icon set for one state.
 * --------------------------------------------------------------------------- */
.reaction-button--heart:not(.active) .reaction-icon path,
.reaction-button--comment .reaction-icon path,
.profile-dream-reaction-icon path,
.comment-heart-btn:not(.active) svg path {
  fill: none;
  stroke: currentColor;
  stroke-width: 38;
}

/* The row and its buttons were defined in Activity.css, which is a route chunk
   that the feed and the dream detail page do not load, so on a cold start they
   were falling back to the browser's own button box and only matched the design
   once you had visited the notifications tab. Shared, so one definition covers
   every surface that draws a reaction.

   44px in both directions, per Apple's minimum: these are icon-sized controls
   sitting next to each other, and the old 0.2rem of padding gave them a target
   about half that. */
.activity-reactions {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.reaction-buttons {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.15rem;
}

.reaction-button {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  min-height: 44px;
  min-width: 44px;
  padding: 0.5rem 0.7rem;
  border: none;
  border-radius: 999px;
  background: none;
  color: var(--text-secondary);
  font-size: var(--fs-sm);
  cursor: pointer;
  transition: background-color 0.18s ease, color 0.18s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}

.reaction-button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.reaction-button.active {
  color: var(--primary);
}

/* Sized to the target rather than a size the target had to be padded out to.
   A 44px button around a 15px glyph reads as a small control with a lot of
   space around it, and the space is invisible until you hover it. */
.reaction-icon {
  font-size: 1.5rem;
  color: inherit;
}

/* Only one of these. There was a second, later copy at 1.1rem further down the
   file, so every size set here was being thrown away and the emoji stayed
   smaller than the icons beside it no matter what this said. */
.reaction-emoji-text {
  font-size: 1.65rem;
  line-height: 1;
}

.reaction-count {
  font-size: var(--fs-base);
  color: inherit;
  font-variant-numeric: tabular-nums;
}

/* Every emoji on a dream, as one control. A chip each meant the row wrapped
   into a block of counts on anything that landed well, and the picker takes any
   emoji a person can type, so there was no ceiling on it. */
.reaction-cluster-glyphs {
  display: inline-flex;
  align-items: center;
  gap: 0.12rem;
}

@media (hover: hover) and (pointer: fine) {
  .reaction-button:hover:not(:disabled) {
    color: var(--text);
    background: rgba(255, 255, 255, 0.06);
  }

  .reaction-button.active:hover:not(:disabled) {
    color: var(--primary);
  }
}

/* An emoji is already full colour, so `color` cannot say whether you are in on
   a reaction: the count beside it was the only thing changing. A tinted pill
   with a ring says it the way every reaction chip does. After the hover block
   on purpose, so hovering a chip you have reacted to does not wash the tint
   back out. */
.reaction-button--emoji.active,
.reaction-button--cluster.active {
  background: rgba(201, 167, 240, 0.16);
  box-shadow: inset 0 0 0 1.5px rgba(201, 167, 240, 0.6);
}

@media (hover: hover) and (pointer: fine) {
  .reaction-button--emoji.active:hover:not(:disabled) {
    background: rgba(201, 167, 240, 0.24);
  }
}

/* ── Overflow menu ────────────────────────────────────────────────────────────
 * The three dots and its dropdown. Block and report live behind one of these on
 * every surface that carries other people's dreams, because moderation should
 * be reachable without sitting at the same weight as the things you came to the
 * page to do. The feed and the profile still carry their own copies of this;
 * new ones should use these classes.
 * --------------------------------------------------------------------------- */
.overflow-menu-root {
  position: relative;
  display: flex;
  align-items: center;
  flex-shrink: 0;
  z-index: 4;
}

.overflow-menu-btn {
  position: relative;
  width: 30px;
  height: 30px;
  border: none;
  border-radius: 999px;
  background: none;
  color: var(--text-secondary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: color 0.18s ease, background-color 0.18s ease;
}

/* 44px target without a 44px glyph. */
.overflow-menu-btn::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}
@media (hover: hover) and (pointer: fine) {
  .overflow-menu-btn:hover {
    background: rgba(255, 255, 255, 0.06);
    color: var(--text);
  }
}

.overflow-menu {
  position: absolute;
  top: calc(100% + 0.35rem);
  right: 0;
  min-width: 190px;
  padding: 0.4rem;
  border-radius: 14px;
  border: 1px solid var(--border);
  background: rgba(10, 14, 22, 0.98);
  box-shadow: var(--raise-highlight), var(--shadow-soft);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  z-index: 5;
}

.overflow-menu-item {
  width: 100%;
  border: none;
  background: transparent;
  color: var(--text);
  text-align: left;
  border-radius: 10px;
  padding: 0.7rem 0.8rem;
  font-weight: 600;
  font-size: var(--fs-base);
  min-height: 44px;
  cursor: pointer;
  transition: background-color 0.18s ease;
}
@media (hover: hover) and (pointer: fine) {
  .overflow-menu-item:hover {
    background: rgba(255, 255, 255, 0.06);
  }
}

.overflow-menu-item:active {
  background: rgba(255, 255, 255, 0.1);
  transition: background-color 60ms ease-out;
}

.overflow-menu-item-danger {
  color: #ff9fb1;
}
@media (hover: hover) and (pointer: fine) {
  .overflow-menu-item-danger:hover {
    background: rgba(255, 138, 171, 0.12);
  }
}

/* Who a dream was shared with. Small enough not to compete with the name it
   sits beside, and the only thing in a post allowed a fill, because the colour
   is the information here rather than decoration. */
.vis-badge {
  display: inline-flex;
  align-items: center;
  padding: 0.12rem 0.5rem;
  border-radius: 999px;
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  flex-shrink: 0;
}

.vis-badge--public    { color: var(--vis-public);    background: var(--vis-public-soft); }
.vis-badge--followers { color: var(--vis-followers); background: var(--vis-followers-soft); }
.vis-badge--mutuals   { color: var(--vis-mutuals);   background: var(--vis-mutuals-soft); }
.vis-badge--anonymous { color: var(--vis-anonymous); background: var(--vis-anonymous-soft); }
.vis-badge--private   { color: var(--vis-private);   background: var(--vis-private-soft); }

.tag,
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--surface-muted);
  color: var(--primary-strong);
  padding: 0.35rem 0.75rem;
  border-radius: 100px;
  font-weight: 600;
  font-size: var(--fs-base);
  border: 1px solid var(--border);
}

.reaction-chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 0.5rem 0;
}

.reaction-popover-list,
.modal-content,
.search-results,
.app-loading-shell {
  -webkit-overflow-scrolling: touch;
}

.reaction-chip {
  border-radius: 999px;
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  padding: 0.3rem 0.85rem;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: var(--fs-base);
  font-weight: 600;
  cursor: pointer;
  transition: border-color 0.2s ease, background-color 0.2s ease, transform 0.2s ease;
}

.reaction-chip.active {
  border-color: var(--primary);
  background: rgba(201, 167, 240, 0.12);
  box-shadow: var(--glow-primary);
}

.reaction-chip:focus-visible {
  border-color: var(--primary);
  background: rgba(201, 167, 240, 0.1);
  outline: none;
}

@media (hover: hover) and (pointer: fine) {
  .reaction-chip:hover {
    border-color: var(--primary);
    background: rgba(201, 167, 240, 0.1);
  }
}

.reaction-emoji-icon {
  font-size: 1rem;
  color: var(--accent);
}

.custom-emoji-popover {
  margin-top: 0.5rem;
  padding: 0.75rem;
  border: 1px solid var(--border);
  border-radius: 16px;
  background: rgba(20, 14, 35, 0.85);
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
}

.emoji-picker-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.emoji-option {
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.05);
  width: 44px;
  height: 44px;
  font-size: 1.7rem;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: border-color 0.2s ease, transform 0.2s ease, background-color 0.2s ease;
}

.emoji-option:focus-visible {
  border-color: var(--primary);
  outline: none;
}

@media (hover: hover) and (pointer: fine) {
  .emoji-option:hover { border-color: var(--primary); }
}

.emoji-option.selected {
  border-color: rgba(201, 167, 240, 0.7);
  background: rgba(201, 167, 240, 0.12);
  box-shadow: 0 0 0 1px rgba(201, 167, 240, 0.2);
}

.emoji-input-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}

.emoji-input-row input {
  min-width: 160px;
  padding: 0.5rem 0.85rem;
  border-radius: 999px;
  border: 1px solid var(--control-border);
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
}

.emoji-input-row input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 2px rgba(201, 167, 240, 0.2);
  outline: none;
}

.reaction-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
}

.reaction-summary-item {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  color: var(--text-secondary);
  font-size: var(--fs-sm);
}

.reaction-summary-item .reaction-emoji-icon {
  font-size: 0.85rem;
}

.emoji-clear-btn {
  width: 100%;
  margin-top: 0.35rem;
  padding: 0.55rem 0.75rem;
  border-radius: 10px;
  border: 1px solid rgba(255, 100, 100, 0.25);
  background: rgba(255, 100, 100, 0.06);
  color: #ff9fb1;
  font-size: 0.85rem;
  font-weight: 600;
  text-align: center;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
}

@media (hover: hover) and (pointer: fine) {
  .emoji-clear-btn:hover {
    background: rgba(255, 100, 100, 0.12);
    border-color: rgba(255, 100, 100, 0.45);
  }
}

.search-highlight {
  background: rgba(232, 200, 122, 0.25);
  color: #f0d58a;
  border-radius: 2px;
  padding: 0 1px;
  font-style: inherit;
  font-weight: inherit;
}

/* Stacked in one grid cell so the box is as tall as the longest wording it can
   hold. `visibility` rather than `display` keeps the hidden ones contributing
   their height while staying out of the accessibility tree, and it is what lets
   the change fade instead of cut. */
.swap-text {
  display: grid;
}

.swap-text-line {
  grid-area: 1 / 1;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.22s ease, visibility 0.22s;
}

.swap-text-line.is-shown {
  opacity: 1;
  visibility: visible;
}

@media (prefers-reduced-motion: reduce) {
  .swap-text-line {
    transition: none;
  }
}

/* Rows arrive a beat apart instead of all at once, which is the difference
   between a list appearing and a list being dealt. The cap matters: past the
   first handful the delay stops growing, so a long list never spends a second
   assembling itself. */
.stream-row-in {
  animation: streamRowIn 380ms cubic-bezier(0.22, 1, 0.36, 1) backwards;
  animation-delay: min(calc(var(--row-index, 0) * 26ms), 240ms);
}

@keyframes streamRowIn {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .stream-row-in {
    animation: none;
  }
}

/* Save lives at the top of a long form, not at the bottom of it. Sticking it
   under the header means the state of your edits and the way to commit them are
   never something you have to scroll to find. Shared, because every screen that
   saves should say the same thing the same way. */
.save-bar {
  position: -webkit-sticky;
  position: sticky;
  top: calc(74px + env(safe-area-inset-top));
  z-index: 200;
  background: rgba(14, 10, 20, 0.88);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 0.75rem 1rem;
  margin-bottom: 0.75rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  flex-wrap: nowrap;
}

/* iOS: stick save bar just below the iOS header (52px, same for all device sizes) */
html[data-native-platform='ios'] .save-bar {
  top: calc(52px + env(safe-area-inset-top));
  /* Remove backdrop-filter on iOS native to prevent GPU compositing promotion.
     The compositing layer it creates causes a blank-box stale-tile artifact
     above it (where settings-header renders) until the user scrolls. */
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  background: rgba(14, 10, 20, 0.97);
}

.save-bar p {
  margin: 0;
  color: var(--text-secondary);
  font-size: var(--fs-base);
  min-width: 0;
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.save-bar .btn-group {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  margin-left: auto;
  flex-shrink: 0;
}

/* The way back off a pushed page. It started on the legal screens and is now on
   connections, edit profile and settings too, so it lives here rather than
   making every page that needs it pull in the legal stylesheet. */
.page-back-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  background: none;
  border: none;
  color: var(--primary);
  font-size: var(--fs-base);
  font-weight: 500;
  padding: 0.5rem 0;
  margin-bottom: 0.75rem;
  cursor: pointer;
  min-height: 44px;
  transition: transform 160ms cubic-bezier(0.22, 1, 0.36, 1), color 0.18s ease, opacity 0.18s ease;
}

/* Drawn rather than imported, so every page gets the same arrow without each
   one choosing its own. Two of them were a literal left arrow character and the
   rest a font icon, at two different weights. */
.page-back-btn::before {
  content: '';
  width: 0.46rem;
  height: 0.46rem;
  border-left: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateX(1px) rotate(45deg);
}
@media (hover: hover) and (pointer: fine) {
  .page-back-btn:hover {
    opacity: 0.8;
  }
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.pending-title {
  color: var(--text-secondary);
  font-size: var(--fs-base);
  font-style: italic;
  margin: 0 0 0.4rem;
}

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

.surface-row {
  display: grid;
  gap: 1rem;
}

.audience-search-input {
  width: 100%;
  margin-bottom: 0.5rem;
}

.audience-search-input input {
  width: 100%;
  padding: 0.75rem 0.95rem;
  border-radius: 12px;
  border: 1px solid var(--control-border);
  background: var(--surface);
  color: var(--text);
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.audience-search-input input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(201, 167, 240, 0.12);
}

.audience-result-list {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}

.audience-result {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 0.75rem 1rem;
  background: var(--surface);
  color: var(--text);
  transition: border-color 0.18s ease, background-color 0.18s ease, box-shadow 0.18s ease, transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
  text-align: left;
}

.audience-result.active {
  border-color: var(--primary);
  background: rgba(201, 167, 240, 0.1);
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.18);
}

.audience-result-meta {
  display: flex;
  flex-direction: column;
  gap: 0.12rem;
}

.result-name {
  font-weight: 600;
  color: var(--text);
}

.result-handle {
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}

.result-status {
  font-size: var(--fs-sm);
  color: var(--text-secondary);
}

.audience-result.active .result-status {
  color: var(--primary);
}

.selected-pill-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  margin-top: 0.65rem;
}

.selected-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.35rem 0.85rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-muted);
  font-size: var(--fs-sm);
  color: var(--text);
}

.selected-pill button {
  border: none;
  background: transparent;
  color: inherit;
  cursor: pointer;
  font-size: var(--fs-base);
  line-height: 1;
}

@media (hover: hover) and (pointer: fine) {
  .selected-pill button:hover { color: var(--primary); }
}

@media (max-width: 768px) {
  /* Text alone comes down on a phone, which is held closer and has a fraction
     of the width to read across. This used to be a 14px root font size, and
     that shrank the icons, the padding and every gap in the app along with the
     words. One knob on the type scale moves only what is typography. */
  :root {
    --type-scale: 0.875;
  }

  /* Prevent iOS auto-zoom on input focus (triggered when font-size < 16px) */
  input,
  textarea,
  select {
    font-size: 16px;
  }

  .app-main.app-main--with-nav {
    padding-top: calc(74px + env(safe-area-inset-top));
  }

  .app-main.app-main--no-nav {
    padding-top: 0;
  }

  .page-container {
    padding: 10px 1.25rem calc(50px + env(safe-area-inset-bottom));
  }

  .page-header {
    flex-direction: column;
    align-items: flex-start;
  }

  .action-group {
    width: 100%;
    justify-content: flex-start;
    flex-wrap: wrap;
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── iOS native: suppress active/hover highlights while scrolling ─────────────
 * `is-scrolling` is added to <html> by src/utils/iosTapFix.js as soon as a
 * touch moves more than 4 px. Removed on touchend so intentional taps still
 * receive full visual feedback. Scoped to data-native-platform='ios' so web
 * is completely unaffected.
 *
 * Strategy:
 *   1. Kill all transitions so mid-flight hover animations can't linger.
 *   2. Strip only the visual "highlight" properties (filter, transform,
 *      box-shadow) that change on hover/active. Intentionally leave
 *      background-color and color alone, and resetting those causes cards to
 *      flash transparent which looks worse.
 *   3. Restore semantic .active states (selected chips, pills) which are
 *      driven by JS state, not pointer events.
 * --------------------------------------------------------------------------- */

/* 1. Kill transitions on everything during scroll so no highlights linger */
html[data-native-platform='ios'].is-scrolling * {
  transition: none !important;
}

/* 2. Strip pointer-driven visual changes. Split in two now that hover is gated
      behind `hover: hover`: the :active half still matters on every iOS device
      (it stops a press state firing mid flick-scroll), while the :hover half
      only has anything to undo on an iPad driven by a trackpad or mouse. */
html[data-native-platform='ios'].is-scrolling *:active {
  filter: none !important;
  transform: none !important;
  box-shadow: none !important;
  outline: none !important;
  border-color: var(--border) !important;
  text-decoration-color: inherit !important;
}


@media (hover: hover) and (pointer: fine) {
  html[data-native-platform='ios'].is-scrolling *:hover {
    filter: none !important;
    transform: none !important;
    box-shadow: none !important;
    outline: none !important;
    border-color: var(--border) !important;
    text-decoration-color: inherit !important;
  }
}

/* 3. Restore semantic selected states (driven by JS, not pointer) */
html[data-native-platform='ios'].is-scrolling .reaction-chip.active, html[data-native-platform='ios'].is-scrolling .reaction-chip.active:active {
  border-color: var(--primary) !important;
  background-color: rgba(183, 183, 240, 0.12) !important;
  box-shadow: var(--glow-primary) !important;
}


@media (hover: hover) and (pointer: fine) {
  html[data-native-platform='ios'].is-scrolling .reaction-chip.active:hover {
    border-color: var(--primary) !important;
    background-color: rgba(183, 183, 240, 0.12) !important;
    box-shadow: var(--glow-primary) !important;
  }
}
html[data-native-platform='ios'].is-scrolling .audience-result.active, html[data-native-platform='ios'].is-scrolling .audience-result.active:active {
  border-color: var(--primary) !important;
  background-color: rgba(183, 183, 240, 0.1) !important;
}


@media (hover: hover) and (pointer: fine) {
  html[data-native-platform='ios'].is-scrolling .audience-result.active:hover {
    border-color: var(--primary) !important;
    background-color: rgba(183, 183, 240, 0.1) !important;
  }
}
html[data-native-platform='ios'].is-scrolling .pill-active, html[data-native-platform='ios'].is-scrolling .pill-active:active {
  border-color: var(--primary) !important;
  background-color: rgba(183, 183, 240, 0.1) !important;
  box-shadow: var(--glow-primary) !important;
}


@media (hover: hover) and (pointer: fine) {
  html[data-native-platform='ios'].is-scrolling .pill-active:hover {
    border-color: var(--primary) !important;
    background-color: rgba(183, 183, 240, 0.1) !important;
    box-shadow: var(--glow-primary) !important;
  }
}

/* ── Interaction feedback ────────────────────────────────────────────────────
 * Two separate concerns, deliberately kept apart:
 *
 *   Press: applies on every device. `-webkit-tap-highlight-color` is cleared
 *            globally above, so without these a tap on a phone gives no
 *            feedback at all. Two durations, on purpose: the short one in each
 *            `:active` rule drives the press, and the 160ms `transform` on the
 *            resting rule drives the release. The release used to have no
 *            transition at all, because the only declaration lived inside
 *            `:active` and went away with it, so the button snapped back to
 *            full size the instant a finger lifted.
 *   Hover: gated behind `hover: hover` + `pointer: fine`. Ungated `:hover`
 *            sticks to whatever a finger last touched until something else is
 *            tapped, which is what `iosTapFix.js` exists to paper over.
 * ------------------------------------------------------------------------- */

.primary-btn:active,
.secondary-btn:active,
.ghost-btn:active,
.danger-btn:active,
.auth-submit:active,
.auth-google-btn:active,
.auth-apple-btn:active,
.request-btn:active,
.follow-action-btn:active,
.moderation-action-btn:active,
.edit-profile-btn:active,
.settings-btn:active,
.add-tag-btn:active,
.close-btn:active,
.report-reason-btn:active,
.feed-card-menu-item:active,
.activity-clear-btn:active,
.prompt-option-btn:active,
.prompt-option:active,
.more-options-toggle:active,
.detail-share-toggle:active,
.voice-input-btn:active {
  transform: scale(0.965);
  transition: transform 80ms ease;
}

/* Cards are large, so they take a much smaller scale, since anything more reads as
   the whole page lurching rather than the card acknowledging the tap. */
.card-shell:active,
.stat-button:active {
  transform: scale(0.99);
  transition: transform 80ms ease;
}

.pill:active,
.chip:active,
.reaction-chip:active,
.emoji-option:active,
.visibility-chip:active,
.filter-toggle .chip:active,
.reaction-button:active,
.comment-heart-btn:active,
.comment-reply-btn:active,
.comment-report-btn:active,
.comment-delete-btn:active,
.selected-pill button:active,
.tagged-pill button:active,
.detail-tag-remove:active,
.avatar-option:active,
.color-swatch:active,
.calendar-day:active,
.nav-ios-tab:active,
.nav-ios-back-btn:active {
  transform: scale(0.93);
  transition: transform 70ms ease;
}

/* A stream row is the full width of the screen, so scaling it reads as the page
   flinching. Social feeds flash the row instead, which is also the only press
   feedback that survives having no border or shadow to change. */
.feed-post:active,
.dream-card:active,
.profile-dream-card:active,
.tagged-dream-card:active,
.search-dream-card:active,
.person-card:active,
.connection-card:active,
.connections-card:active,
.connections-card-main:active,
.activity-card:active,
.activity-card-main:active {
  background-color: rgba(255, 255, 255, 0.08);
  transition: background-color 60ms ease-out;
}

/* Disabled controls must not look pressable. */
.primary-btn:disabled:active,
.secondary-btn:disabled:active,
.ghost-btn:disabled:active,
.danger-btn:disabled:active,
button:disabled:active {
  transform: none;
}

@media (hover: hover) and (pointer: fine) {
  .ghost-btn:hover:not(:disabled),
  .danger-btn:hover:not(:disabled) {
    color: var(--text);
    filter: brightness(1.15);
  }

  .pill:hover,
  .chip:hover,
  .view-toggle-btn:hover,
  .feed-tab:hover,
  .dream-tab:hover,
  .connections-tab:hover {
    border-color: rgba(201, 167, 240, 0.45);
    color: var(--text);
  }

  .dream-card:hover,
  .profile-dream-card:hover,
  .tagged-dream-card:hover,
  .search-dream-card:hover,
  .person-card:hover,
  .connections-card:hover,
  .activity-card:hover {
    border-color: rgba(201, 167, 240, 0.28);
    background-color: rgba(255, 255, 255, 0.055);
  }

  .calendar-day:hover:not(.is-selected) .calendar-day-number {
    background: rgba(201, 167, 240, 0.16);
  }

  .avatar-option:hover,
  .color-swatch:hover,
  .report-reason-btn:hover,
  .feed-card-menu-item:hover,
  .prompt-option-btn:hover,
  .audience-result:hover {
    border-color: rgba(201, 167, 240, 0.45);
  }

  .activity-clear-btn:hover,
  .comment-reply-btn:hover,
  .comment-report-btn:hover {
    color: var(--primary);
  }

  .comment-delete-btn:hover {
    color: #ff9fb1;
  }
}

/* Presses are feedback, not decoration, but they still respect the global
   reduced-motion rule above, which zeroes out every transition duration. */
@media (prefers-reduced-motion: reduce) {
  .page-container,
  .insights-page,
  .home-page,
  [data-nav-direction] .page-container,
  [data-nav-direction] .insights-page {
    animation: none;
  }
}

/* Remaining standalone controls and links. Grouped separately from the block
   above only because these are not variants of the shared button classes. */
.auth-back-link:active,
.auth-forgot-link:active,
.auth-password-toggle:active,
.comment-replies-toggle:active,
.detail-author-btn:active,
.detail-title-editable:active,
.detail-date-pill--interactive:active,
.feed-author-link:active,
.feed-card-menu-btn:active,
.page-back-btn:active,
.memory-patterns-link:active,
.nav-logo:active,
.rc-restore-btn:active,
.tag-suggestion-item:active,
.tagged-view-profile:active,
.emoji-clear-btn:active,
.navigation .nav-links a:active {
  transform: scale(0.94);
  transition: transform 70ms ease;
}

.home-cta-primary:active,
.home-cta-secondary:active {
  transform: scale(0.965);
  transition: transform 80ms ease;
}

@media (hover: hover) and (pointer: fine) {
  .detail-title-editable:hover,
  .detail-date-pill--interactive:hover,
  .feed-author-link:hover,
  .tagged-view-profile:hover,
  .memory-patterns-link:hover,
  .auth-forgot-link:hover,
  .auth-back-link:hover,
  .comment-replies-toggle:hover,
  .rc-restore-btn:hover {
    color: var(--primary);
  }

  .feed-card-menu-btn:hover,
  .auth-password-toggle:hover,
  .page-back-btn:hover {
    color: var(--text);
  }

  .tag-suggestion-item:hover {
    border-color: rgba(201, 167, 240, 0.45);
  }

  .nav-logo:hover {
    filter: brightness(1.15);
  }
}

/* ── Native touch behaviour (iOS shell only) ─────────────────────────────────
 * Without these, a long press anywhere pops WebKit's Copy / Look Up / Share
 * callout and drags start selecting UI text, the clearest tell that an app is
 * a web view. Chrome is locked down; anything the user might genuinely want to
 * copy is opted back in below, and text entry is restored explicitly because
 * inheriting `user-select: none` into a field breaks selection in WKWebView.
 * ------------------------------------------------------------------------- */

html[data-native-platform='ios'] body {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

html[data-native-platform='ios'] input,
html[data-native-platform='ios'] textarea,
html[data-native-platform='ios'] [contenteditable='true'] {
  -webkit-touch-callout: default;
  -webkit-user-select: text;
  user-select: text;
}

/* Real content, meaning a dream, a comment or an analysis, stays long-pressable so it
   can be copied, the way selectable text behaves in a native app. */
html[data-native-platform='ios'] .detail-body,
html[data-native-platform='ios'] .detail-insight,
html[data-native-platform='ios'] .detail-connections-item,
html[data-native-platform='ios'] .comment-body,
html[data-native-platform='ios'] .feed-content,
html[data-native-platform='ios'] .feed-summary,
html[data-native-platform='ios'] .insights-entry-text,
html[data-native-platform='ios'] .profile-bio,
html[data-native-platform='ios'] .legal-page {
  -webkit-touch-callout: default;
  -webkit-user-select: text;
  user-select: text;
}

/* ── Modal motion ────────────────────────────────────────────────────────────
 * Nothing in the app animated on open. Sheets and dialogs simply appeared,
 * which is the other big "web view" tell after the long-press callout. The
 * class names are shared across the three stylesheets that define report
 * modals, so styling them once here covers every caller.
 *
 * `backwards` fill again: these end at a transform, and leaving one applied
 * would make the panel a containing block for anything fixed inside it.
 * ------------------------------------------------------------------------- */

@keyframes modalBackdropIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes modalSheetIn {
  from { opacity: 0; transform: translate3d(0, 100%, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

@keyframes modalDialogIn {
  from { opacity: 0; transform: scale(0.96); }
  to   { opacity: 1; transform: scale(1); }
}

.confirm-modal-backdrop,
.report-modal-backdrop,
.modal-overlay,
.crop-modal-backdrop {
  animation: modalBackdropIn 180ms ease-out backwards;
}

.report-modal,
.modal-content,
.crop-modal-card {
  animation: modalDialogIn 220ms cubic-bezier(0.32, 0.72, 0, 1) backwards;
}

/* The confirm modal is a bottom sheet below 480px and a centred dialog above,
   so its entrance has to follow the same breakpoint. */
.confirm-modal {
  animation: modalSheetIn 260ms cubic-bezier(0.32, 0.72, 0, 1) backwards;
}

@media (min-width: 480px) {
  .confirm-modal {
    animation: modalDialogIn 220ms cubic-bezier(0.32, 0.72, 0, 1) backwards;
  }
}

/* Scrolling inside a sheet must not chain to the page behind it. */
.confirm-modal,
.report-modal,
.modal-content,
.crop-modal-card {
  overscroll-behavior: contain;
}

@media (prefers-reduced-motion: reduce) {
  .confirm-modal-backdrop,
  .report-modal-backdrop,
  .modal-overlay,
  .crop-modal-backdrop,
  .confirm-modal,
  .report-modal,
  .modal-content,
  .crop-modal-card {
    animation: none;
  }
}

/* ---------------------------------------------------------------------------
 * Popovers. These appeared fully formed the instant they mounted, which is the
 * most jarring version of the problem, because they arrive right under the finger that
 * asked for them. Growing them a little out of their anchor corner ties the
 * panel to the control that opened it.
 * `backwards` fill again, for the same containing-block reason as the modals.
 * ------------------------------------------------------------------------- */

@keyframes popoverIn {
  from { opacity: 0; transform: scale(0.96); }
  to   { opacity: 1; transform: scale(1); }
}

.feed-card-menu {
  transform-origin: top right;
  animation: popoverIn 150ms cubic-bezier(0.32, 0.72, 0, 1) backwards;
}

/* Opacity only, deliberately. This card is measured with getBoundingClientRect
   in a layout effect to place itself next to its anchor, and a scale would be
   folded into that measurement and put the popover in the wrong spot. */
.reaction-popover-card {
  animation: fadeIn 150ms ease-out backwards;
}

@media (prefers-reduced-motion: reduce) {
  .feed-card-menu,
  .reaction-popover-card {
    animation: none;
  }
}
