/* ============================================================================
   components.css — reusable pieces: header, buttons, cards, vote, avatar,
   fields, toasts, the inline confirm, skeletons.
   ========================================================================== */

/* — header ------------------------------------------------------------------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(var(--blur)) saturate(var(--saturate));
  backdrop-filter: blur(var(--blur)) saturate(var(--saturate));
  border-bottom: 1px solid var(--border);
}
.site-header::before {
  /* hairline gradient sitting on the very top edge */
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: var(--hairline);
}
/* — reading progress ------------------------------------------------------------
   A 2px amber hairline lying along the header's bottom edge, filling as you
   read. It exists because long serif posts are the reading experience the
   whole type system was built for, and a reader forty percent into one has no
   other way to know it.

   No JavaScript and no scroll listener: animation-timeline hands the animation
   the document's own scroll progress, so it is exact by construction, cannot
   drift out of step with the page, and costs nothing per frame — it animates a
   transform, which never leaves the compositor.

   It measures the page rather than the post element, comments included. That's
   the honest reading: the thread is part of what you came for, and a bar that
   filled up two thirds of the way down the screen would be telling you you had
   finished something you hadn't.

   Everything is inside @supports, and the element is display:none outside it,
   so a browser without scroll-driven animations gets an empty span and no
   stray amber rule. That is the entire fallback story.
   -------------------------------------------------------------------------- */
.reading {
  display: none;
}
@supports (animation-timeline: scroll()) {
  body:has(#view .detail) .reading {
    display: block;
    position: absolute;
    /* -1px so it lies *on* the header's bottom border rather than under it:
       the line the eye already reads as "the chrome ends here" is the one
       that fills. */
    inset: auto 0 -1px 0;
    height: 2px;
    z-index: 1;
    background: var(--accent);
    transform: scaleX(0);
    transform-origin: 0 50%;
    animation: reading linear both;
    animation-timeline: scroll(root block);
  }

  /* base.css collapses every animation-duration to 0.01ms under reduced
     motion, which is right for anything that plays at you and wrong here: with
     a progress-based timeline it would snap the bar to full at the first
     pixel. Nothing about this moves unless the reader moves the page, so it
     keeps its progress-based duration and stays truthful. */
  @media (prefers-reduced-motion: reduce) {
    body:has(#view .detail) .reading {
      animation-duration: auto !important;
    }
  }
}
@keyframes reading {
  to {
    transform: scaleX(1);
  }
}

.site-header__inner {
  max-width: var(--page-max);
  margin-inline: auto;
  min-height: var(--header-h);
  /* The top inset keeps the row clear of the status bar / notch, which
     viewport-fit=cover deliberately lets us paint under. */
  padding: calc(var(--s-2) + env(safe-area-inset-top))
    max(var(--s-4), env(safe-area-inset-right)) var(--s-2)
    max(var(--s-4), env(safe-area-inset-left));
  display: grid;
  /* minmax(0, …) on the middle track, not a bare 1fr: a bare 1fr refuses to go
     below the search field's min-content, so once the brand and the account
     cluster stop fitting the whole row overflows sideways instead. The search
     is the one thing here that can afford to give ground, so let it. */
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s-3) var(--s-4);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 44px;
  justify-self: start;
  color: var(--text);
  text-decoration: none;
  font-family: var(--font-serif);
  font-size: var(--fs-brand);
  letter-spacing: 0.01em;
}
.brand__mark {
  color: var(--text-dim);
  transition: color var(--t-mid) var(--ease), transform var(--t-mid) var(--ease);
}
@media (hover: hover) {
  .brand:hover .brand__mark {
    color: var(--accent);
    transform: rotate(-8deg);
  }
}
.brand:active .brand__mark {
  color: var(--accent);
}
.brand__word {
  font-weight: 500;
}

/* — search --------------------------------------------------------------------- */
.search {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  justify-self: stretch;
  max-width: 30rem;
  padding-inline: var(--s-3);
  height: 40px;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--surface-solid) 60%, transparent);
  color: var(--text-dim);
  transition: border-color var(--t-mid) var(--ease),
    background-color var(--t-mid) var(--ease);
}
.search:focus-within {
  border-color: var(--border-strong);
  background: var(--surface-solid);
}
.search__icon {
  flex: none;
}
.search__input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  outline: none;
  color: var(--text);
  font-size: var(--fs-field);
}
.search__input::placeholder {
  color: var(--text-faint);
}
/* the native clear affordance clashes with the pill; keep it tidy */
.search__input::-webkit-search-cancel-button {
  -webkit-appearance: none;
}

/* — account cluster --------------------------------------------------------- */
.account {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  justify-self: end;
  min-width: 0;
}
/* Write / Sign out: icon + word on a roomy screen, icon alone when the row
   gets tight (see the narrow-screen rules in views.css). The accessible name
   comes from aria-label either way, so hiding the word costs nothing. */
.btn--action {
  min-height: 44px;
}
/* Nice to have, first to go: from roughly 640 to 900px the signed-in cluster
   and the search stop fitting on one row, and the email is the only part of
   the cluster that isn't an action. */
@media (max-width: 900px) {
  .account__email {
    display: none;
  }
}
.account__email {
  max-width: 15ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-dim);
  font-size: var(--fs-meta);
  text-decoration: none;
}
@media (hover: hover) {
  .account__email:hover {
    color: var(--text);
  }
}

/* — buttons ------------------------------------------------------------------ */
.btn {
  --btn-bg: transparent;
  --btn-fg: var(--text);
  --btn-bd: var(--border-strong);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: 44px;
  padding: 0 var(--s-4);
  border: 1px solid var(--btn-bd);
  border-radius: var(--r-sm);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-family: var(--font-ui);
  font-size: var(--fs-ui);
  font-weight: 500;
  line-height: 1;
  cursor: pointer;
  white-space: nowrap;
  transition: transform var(--t-fast) var(--ease),
    background-color var(--t-mid) var(--ease),
    border-color var(--t-mid) var(--ease), opacity var(--t-mid) var(--ease);
}
.btn:active {
  transform: scale(0.97);
}
.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.55;
  cursor: not-allowed;
  transform: none;
}
.btn--primary {
  --btn-bg: var(--accent);
  --btn-fg: var(--accent-ink);
  --btn-bd: transparent;
  font-weight: 600;
}
/* Hover is gated so a tap on a touch screen doesn't leave the button stuck in
   its hover skin until you tap somewhere else. :active is what a finger gets,
   and it's what replaces the tap-highlight flash we turned off in base.css. */
@media (hover: hover) {
  .btn--primary:hover:not([disabled]) {
    --btn-bg: color-mix(in srgb, var(--accent) 88%, white);
  }
  .btn--ghost:hover:not([disabled]) {
    --btn-bd: var(--text-faint);
    background: color-mix(in srgb, var(--text) 6%, transparent);
  }
}
.btn--primary:active:not([disabled]) {
  --btn-bg: color-mix(in srgb, var(--accent) 84%, black);
}
.btn--ghost:active:not([disabled]) {
  --btn-bd: var(--text-faint);
  background: color-mix(in srgb, var(--text) 10%, transparent);
}
.btn--quiet {
  --btn-bd: transparent;
  min-height: 40px;
  padding-inline: var(--s-3);
  color: var(--text-dim);
}
@media (hover: hover) {
  .btn--quiet:hover:not([disabled]) {
    color: var(--text);
    background: color-mix(in srgb, var(--text) 6%, transparent);
  }
}
.btn--quiet:active:not([disabled]) {
  color: var(--text);
  background: color-mix(in srgb, var(--text) 11%, transparent);
}
.btn--danger {
  --btn-bd: transparent;
  --btn-bg: var(--danger);
  --btn-fg: var(--danger-ink);
  font-weight: 600;
}
@media (hover: hover) {
  .btn--danger:hover:not([disabled]) {
    --btn-bg: color-mix(in srgb, var(--danger) 88%, black);
  }
}
.btn--danger:active:not([disabled]) {
  --btn-bg: color-mix(in srgb, var(--danger) 80%, black);
}
.btn--icon {
  min-width: 44px;
  padding: 0;
}
.btn--block {
  width: 100%;
}
.btn--quiet.danger {
  color: var(--danger);
}
@media (hover: hover) {
  .btn--quiet.danger:hover:not([disabled]) {
    color: var(--danger);
    background: var(--danger-soft);
  }
}
.btn--quiet.danger:active:not([disabled]) {
  color: var(--danger);
  background: color-mix(in srgb, var(--danger) 22%, transparent);
}

.icon {
  display: block;
  flex: none;
}

/* tiny inline spinner for pending states */
.spinner {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  animation: spin 0.7s linear infinite;
}
@keyframes spin {
  to {
    transform: rotate(1turn);
  }
}

/* — avatar (initials, deterministic quiet tint, no image fetch) ---------- */
.avatar {
  --h: 180;
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: var(--r-pill);
  background: hsl(var(--h) 32% 42% / 0.28);
  color: color-mix(in srgb, var(--text) 88%, hsl(var(--h) 40% 60%));
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  user-select: none;
}
.avatar--sm {
  width: 22px;
  height: 22px;
  font-size: 0.68rem;
}
.avatar--lg {
  width: 40px;
  height: 40px;
  font-size: var(--fs-ui);
}

/* — post card ------------------------------------------------------------------ */
.card {
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s-4);
  padding: var(--s-4) var(--s-5) var(--s-4) var(--s-4);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--card-bg);
  transition: transform var(--t-mid) var(--ease),
    border-color var(--t-mid) var(--ease), box-shadow var(--t-mid) var(--ease),
    background-color var(--t-mid) var(--ease);
  content-visibility: auto;
  contain-intrinsic-size: 0 168px;
}
.card + .card {
  margin-top: var(--s-3);
}
/* j / k focus the card's link, and scrollIntoView({block:"nearest"}) honours
   this — so a card the cursor steps onto never arrives tucked under the
   sticky header. */
.card {
  scroll-margin-top: calc(var(--header-h) + var(--s-4));
}
/* The keyboard cursor *is* focus (see js/components/feedkeys.js), so it needs
   no mark of its own — but the ring belongs around the card, not around the
   title-and-preview block that happens to be the link. Otherwise the cursor
   reads as a rectangle drawn through a card rather than as the card. */
.card:has(.card__link:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-color: var(--border-strong);
}
.card__link:focus-visible {
  outline: none;
}

/* — warmth ---------------------------------------------------------------------
   The one signature detail on the card, and deliberately the only one.

   Commons is a hearth: the mark is a lamp, the accent is honey amber, and the
   only live quantity in the whole app is votes. So a post the room has agreed
   on gets a single warm hairline down its leading edge — structure carrying
   information, rather than a badge or a chip placed on top of it. Nothing else
   on the card is allowed to compete with it.

   Not announced to assistive tech, and that's on purpose rather than an
   oversight: the vote button beside it already says "Upvote, now 4 votes" out
   loud. This is the same fact drawn, not a second one; saying it twice would
   make the card noisier to listen to, not clearer.

   The gradient fades out at both ends so it never has to meet the card's 18px
   corner radius, and so it reads as light falling on an edge rather than as a
   drawn rule. --accent, no new colour, no new token. */
.card--warm::before {
  content: "";
  position: absolute;
  inset-block: var(--s-5);
  inset-inline-start: 0;
  width: 2px;
  background: linear-gradient(
    to bottom,
    transparent,
    var(--accent),
    transparent
  );
}
@media (hover: hover) {
  .card:hover {
    transform: translateY(-2px);
    border-color: var(--border-strong);
    background: var(--glass-bg);
    -webkit-backdrop-filter: blur(var(--blur)) saturate(var(--saturate));
    backdrop-filter: blur(var(--blur)) saturate(var(--saturate));
    box-shadow: var(--shadow-hover);
  }
}
/* .card.card--opening, not .card--opening: .card:hover above is two
   compound selectors and would otherwise outweigh it, so on a mouse the
   pressed state was invisible for exactly the people most likely to see it. */
.card:has(.card__link:active),
.card.card--opening {
  border-color: var(--border-strong);
  background: color-mix(in srgb, var(--text) 5%, var(--card-bg));
}

/* The card you just tapped, held pressed until its post is on screen.
   :active stops at the moment the finger lifts, which is precisely when the
   waiting begins — so the one frame that needs to say "heard you" is the one
   frame :active can't reach. js/ui.js adds this on the way out and nothing
   takes it off: the view swap takes the card instead. Hover is suppressed
   underneath it so a mouse doesn't lift the card it is busy opening. */
@media (hover: hover) {
  .card.card--opening:hover {
    transform: none;
    box-shadow: none;
    background: color-mix(in srgb, var(--text) 5%, var(--card-bg));
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}
.card__body {
  min-width: 0;
}
.card__link {
  display: block;
  color: inherit;
  text-decoration: none;
}
.card__title {
  font-family: var(--font-serif);
  font-size: var(--fs-title);
  font-weight: 500;
  line-height: 1.25;
  margin-bottom: var(--s-2);
}
.card__preview {
  color: var(--text-dim);
  font-size: var(--fs-body);
  line-height: 1.5;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}
.card__meta {
  margin-top: var(--s-3);
  display: flex;
  align-items: center;
  gap: var(--s-2);
  color: var(--text-faint);
  font-size: var(--fs-meta);
}
/* The author's name is a link to their posts, but inside a card that is
   itself mostly one big link — so it stays quiet until you point at it. */
.card__author {
  color: var(--text-dim);
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-decoration: none;
}
@media (hover: hover) {
  .card__author:hover {
    color: var(--text);
    text-decoration: underline;
  }
}
.card__author:active {
  color: var(--text);
}

/* — vote control ------------------------------------------------------------- */
.vote {
  flex: none;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-width: 44px;
  min-height: 44px;
  padding: var(--s-1) var(--s-2);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  background: none;
  color: var(--text-dim);
  cursor: pointer;
  transition: color var(--t-mid) var(--ease),
    background-color var(--t-mid) var(--ease),
    border-color var(--t-mid) var(--ease);
}
@media (hover: hover) {
  .vote:hover:not([disabled]) {
    color: var(--text);
    background: color-mix(in srgb, var(--text) 6%, transparent);
  }
}
.vote:active:not([disabled]) {
  color: var(--text);
  background: color-mix(in srgb, var(--text) 12%, transparent);
}
.vote__caret {
  transition: transform var(--t-mid) var(--ease);
}
.vote__caret path {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linejoin: round;
  transition: fill var(--t-fast) var(--ease);
}
.vote__count {
  font-size: var(--fs-meta);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
  line-height: 1;
}
.vote--on,
.vote--on:hover:not([disabled]),
.vote--on:active:not([disabled]) {
  color: var(--accent);
}
.vote--on .vote__caret path {
  fill: currentColor;
}
.vote--on .vote__count {
  font-weight: 600;
}
.vote--pulse .vote__caret {
  animation: vote-pop var(--t-slow) var(--ease);
}
@keyframes vote-pop {
  0% {
    transform: scale(1);
  }
  38% {
    transform: scale(1.32) translateY(-1px);
  }
  70% {
    transform: scale(0.94);
  }
  100% {
    transform: scale(1);
  }
}
/* horizontal variant for the detail view */
.vote--inline {
  flex-direction: row;
  gap: var(--s-2);
  min-height: 44px;
  padding-inline: var(--s-3);
  border-color: var(--border);
  border-radius: var(--r-pill);
}

/* — fields --------------------------------------------------------------------- */
.field {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}
.field__label {
  font-size: var(--fs-meta);
  color: var(--text-dim);
}
.input,
.textarea {
  width: 100%;
  padding: var(--s-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--surface-solid);
  color: var(--text);
  font-size: var(--fs-field);
  transition: border-color var(--t-mid) var(--ease),
    box-shadow var(--t-mid) var(--ease);
}
.input:focus,
.textarea:focus {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 28%, transparent);
}
.textarea {
  min-height: 9rem;
  resize: vertical;
  line-height: 1.55;
}
.input[aria-invalid="true"],
.textarea[aria-invalid="true"] {
  border-color: var(--danger);
}
.field__error {
  font-size: var(--fs-meta);
  color: var(--danger);
  min-height: 1em;
}
.input-wrap {
  position: relative;
  display: flex;
}
.input-wrap .input {
  padding-right: 3rem;
}
.reveal {
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  color: var(--text-faint);
  cursor: pointer;
}
@media (hover: hover) {
  .reveal:hover {
    color: var(--text-dim);
  }
}
.reveal:active {
  color: var(--text);
  background: color-mix(in srgb, var(--text) 9%, transparent);
}

/* — toggle (publish switch) ---------------------------------------------------- */
.switch {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  cursor: pointer;
  font-size: var(--fs-ui);
  color: var(--text-dim);
}
.switch input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}
.switch__track {
  width: 40px;
  height: 24px;
  border-radius: var(--r-pill);
  border: 1px solid var(--border-strong);
  background: color-mix(in srgb, var(--text) 8%, transparent);
  transition: background-color var(--t-mid) var(--ease),
    border-color var(--t-mid) var(--ease);
  flex: none;
}
.switch__thumb {
  display: block;
  width: 18px;
  height: 18px;
  margin: 2px;
  border-radius: 50%;
  background: var(--text-dim);
  transition: transform var(--t-mid) var(--ease),
    background-color var(--t-mid) var(--ease);
}
.switch input:checked + .switch__track {
  background: var(--accent-soft);
  border-color: var(--accent);
}
.switch input:checked + .switch__track .switch__thumb {
  transform: translateX(16px);
  background: var(--accent);
}
.switch input:focus-visible + .switch__track {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* — tags --------------------------------------------------------------------- */
.tag {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--s-2);
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--text) 8%, transparent);
  color: var(--text-dim);
  font-size: var(--fs-micro);
  letter-spacing: 0.02em;
}

/* — toasts ----------------------------------------------------------------------- */
.toasts {
  position: fixed;
  left: 50%;
  bottom: max(var(--s-5), env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-2);
  /* % on a fixed element is the viewport minus the scrollbar — 100vw isn't,
     and would push the toast a scrollbar's width off-centre on the desktop. */
  width: min(28rem, calc(100% - 2 * var(--s-4)));
  pointer-events: none;
}
.toast {
  width: 100%;
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--panel-bg);
  -webkit-backdrop-filter: blur(var(--blur-strong)) saturate(var(--saturate));
  backdrop-filter: blur(var(--blur-strong)) saturate(var(--saturate));
  box-shadow: var(--shadow-panel);
  color: var(--text);
  font-size: var(--fs-ui);
  pointer-events: auto;
  animation: toast-in var(--t-mid) var(--ease) both;
}
.toast--leaving {
  animation: toast-out var(--t-mid) var(--ease-in) both;
}
.toast::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  border-radius: inherit;
  background: var(--hairline);
}
@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes toast-out {
  to {
    opacity: 0;
    transform: translateY(6px);
  }
}

/* — inline confirm (delete) ------------------------------------------------- */
.confirm {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--danger);
  border-radius: var(--r-md);
  background: var(--danger-soft);
  animation: route-in var(--t-fast) var(--ease) both;
}
.confirm__text {
  flex: 1;
  min-width: 12ch;
  font-size: var(--fs-ui);
}

/* — skeletons ----------------------------------------------------------------- */
.card--skeleton {
  content-visibility: visible;
}
.sk {
  display: block;
  border-radius: var(--r-sm);
  background: linear-gradient(
    100deg,
    color-mix(in srgb, var(--text) 6%, transparent) 30%,
    color-mix(in srgb, var(--text) 12%, transparent) 50%,
    color-mix(in srgb, var(--text) 6%, transparent) 70%
  );
  background-size: 220% 100%;
  animation: shimmer 1.4s linear infinite;
}
@keyframes shimmer {
  to {
    background-position: -220% 0;
  }
}
.sk--caret {
  width: 16px;
  height: 16px;
}
.sk--count {
  width: 18px;
  height: 10px;
  margin-top: 6px;
}
.sk--title {
  width: 70%;
  height: 18px;
  margin-bottom: var(--s-3);
}
.sk--line {
  width: 100%;
  height: 12px;
  margin-bottom: var(--s-2);
}
.sk--short {
  width: 45%;
}
.sk--meta {
  width: 30%;
  height: 11px;
  margin-top: var(--s-3);
}

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

/* — narrow screens ---------------------------------------------------------
   Glass is the first thing to go. A backdrop-filter makes the browser snapshot
   and blur everything behind the element on every frame it scrolls over; on a
   phone GPU that's what turns a sticky header into a smear. Below 640px the
   header, panels and toasts use their opaque fallbacks instead — same reading
   surface, none of the per-frame cost.
   -------------------------------------------------------------------------- */
@media (max-width: 640px) {
  /* 40px is a fine mouse target and a poor thumb one */
  .btn--quiet {
    min-height: 44px;
  }
  .site-header {
    background: var(--glass-bg-fallback);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .toast {
    background: var(--panel-bg-fallback);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  /* Write / Sign out shed their words so the whole cluster — including the
     theme toggle, which used to be pushed clean off the screen at 320px —
     fits on one row. aria-label still carries the name. */
  .btn--action {
    min-width: 44px;
    padding-inline: 0;
  }
  .btn--action .btn__label {
    display: none;
  }
  .account {
    gap: var(--s-1);
  }
  .site-header__inner {
    gap: var(--s-2) var(--s-3);
  }

  /* 24px on the right of every card is a lot of nothing when the screen is
     320px wide. Even it up and give the words the space back. */
  .card {
    gap: var(--s-3);
    padding: var(--s-4);
  }
  .card__title {
    font-size: 1.2rem;
  }

  /* The confirm's buttons drop onto their own full-width row rather than
     squeezing next to twelve characters of question. */
  .confirm {
    gap: var(--s-2);
  }
  .confirm__text {
    flex: 1 0 100%;
  }
  .confirm .btn {
    flex: 1;
  }
}

/* — demo notice ---------------------------------------------------------------
   Only ever present on the published (static-host) build, where the app is
   answering its own API calls. Two placements, never both at once: a band
   under the header, or folded into the masthead when that's on screen. See
   syncDemoStrip in main.js.

   Either way it scrolls with the page — this is context, not a control, and
   pinning it would be shouting.
   -------------------------------------------------------------------------- */
/* shared by both placements */
.demo {
  color: var(--text-dim);
  font-size: var(--fs-meta);
}
.demo--band {
  border-bottom: 1px solid var(--border);
  /* A neutral band, not a tinted one. The accent is reserved for live things —
     your identity, an active upvote, a link — and a standing notice is not one
     of those. An amber wash here also pulled the light theme toward the warm
     cream it deliberately isn't. Only the label is accented. */
  background: color-mix(in srgb, var(--text) 5%, var(--bg-2));
}
.demo__inner {
  max-width: var(--page-max);
  margin-inline: auto;
  display: flex;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-2) max(var(--s-4), env(safe-area-inset-right)) var(--s-2)
    max(var(--s-4), env(safe-area-inset-left));
}

/* Inside the masthead it isn't a band: no full-bleed background, no border, no
   page padding — it's the third line of a block that already has all three. */
.demo--inline {
  margin-top: var(--s-4);
}
.demo--inline .demo__inner {
  padding: 0;
  gap: var(--s-3);
  align-items: baseline;
}
.demo__text {
  flex: 1;
  min-width: 0;
  line-height: 1.5;
}
.demo__label {
  /* --accent-text, not --accent: this is 13px type, and the raw amber is a
     fill colour that can't carry small text on the light theme's band. */
  color: var(--accent-text);
  font-weight: 600;
}
.demo__text a {
  color: inherit;
  text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
}
@media (hover: hover) {
  .demo__text a:hover {
    color: var(--text);
    text-decoration-color: currentColor;
  }
}
.demo__actions {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
}
.demo__action {
  min-height: 36px;
  font-size: var(--fs-meta);
}
/* the fold chevron points down when open, up when folded */
.demo__fold {
  min-height: 36px;
  min-width: 36px;
  transform: rotate(-90deg);
  transition: transform var(--t-mid) var(--ease);
}
.demo--folded .demo__fold {
  transform: rotate(90deg);
}
.demo--folded .demo__text {
  /* keep the label, drop the explanation — the strip stays visible either way */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.demo--folded .demo__action {
  display: none;
}

@media (max-width: 640px) {
  .demo--band .demo__inner {
    align-items: flex-start;
    gap: var(--s-2);
    padding-block: var(--s-3);
  }
  .demo--inline .demo__inner {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s-2);
  }
  .demo__action {
    min-height: 44px;
  }
  .demo__fold {
    min-height: 44px;
    min-width: 44px;
  }
  .demo--folded .demo__inner {
    align-items: center;
  }
}

/* — command palette ----------------------------------------------------------
   A panel over a dimmed page, near the top rather than centred — the list grows
   downwards and a centred panel would jump about as it does.
   -------------------------------------------------------------------------- */
.palette {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: grid;
  justify-items: center;
  align-content: start;
  /* dvh, not vh: vh is frozen at the largest viewport, so on a phone with
     the URL bar showing a vh-sized panel is taller than the screen. */
  padding: max(10dvh, var(--s-7)) var(--s-4) var(--s-4);
  background: color-mix(in srgb, var(--bg) 62%, transparent);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  animation: palette-in var(--t-fast) var(--ease) both;
}
@keyframes palette-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
.palette__panel {
  width: min(34rem, 100%);
  max-height: min(60dvh, 28rem);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  background: var(--panel-bg);
  -webkit-backdrop-filter: blur(var(--blur-strong)) saturate(var(--saturate));
  backdrop-filter: blur(var(--blur-strong)) saturate(var(--saturate));
  box-shadow: var(--shadow-panel);
  position: relative;
}
.palette__panel::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  border-radius: inherit;
  background: var(--hairline);
}
.palette__field {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--border);
  color: var(--text-faint);
  flex: none;
}
.palette__glyph {
  flex: none;
}
.palette__input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  outline: none;
  color: var(--text);
  font-size: var(--fs-field);
}
.palette__input::placeholder {
  color: var(--text-faint);
}
.palette__list {
  margin: 0;
  padding: var(--s-2);
  list-style: none;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.palette__row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  min-height: 40px;
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  color: var(--text-dim);
  cursor: pointer;
}
/* Selection is driven by aria-selected, so the keyboard and the pointer can't
   disagree about which row is live. */
.palette__row[aria-selected="true"] {
  background: color-mix(in srgb, var(--text) 8%, transparent);
  color: var(--text);
}
.palette__label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-ui);
}
.palette__key {
  flex: none;
  min-width: 1.6em;
  padding: 2px var(--s-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  background: color-mix(in srgb, var(--text) 6%, transparent);
  color: var(--text-faint);
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  text-align: center;
}
.palette__row[aria-selected="true"] .palette__key {
  border-color: var(--border-strong);
  color: var(--text-dim);
}
.palette__empty {
  padding: var(--s-5) var(--s-4) var(--s-6);
  text-align: center;
  color: var(--text-faint);
  font-family: var(--font-serif);
  font-size: 1.05rem;
}

/* The hint that the palette exists at all. Pointer-fine only: there's no ⌘K on
   a phone, and advertising one there would be a lie. */
.search__hint {
  flex: none;
  display: inline-flex;
  align-items: center;
  padding: 2px var(--s-2);
  border: 1px solid var(--border);
  border-radius: 6px;
  background: none;
  color: var(--text-faint);
  font-family: var(--font-ui);
  font-size: var(--fs-micro);
  line-height: 1.5;
  white-space: nowrap;
  cursor: pointer;
}
.search__hint kbd {
  font: inherit;
}
@media (hover: hover) {
  .search__hint:hover {
    border-color: var(--border-strong);
    color: var(--text-dim);
  }
}
@media (max-width: 900px), (pointer: coarse) {
  .search__hint {
    display: none;
  }
}

@media (max-width: 640px) {
  .palette {
    padding-top: var(--s-6);
  }
  .palette__panel {
    background: var(--panel-bg-fallback);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .palette__row {
    min-height: 44px;
  }
}
