.btn-text {
  background: none; border: none; color: var(--text-secondary);
  font-size: 14px; font-weight: 600; cursor: pointer;
}

.login-header-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-3) var(--space-6);
  border-radius: var(--radius-pill);
  /* Was cream text on a translucent gold wash, 1.12:1 once the header
     turned to paper. Solid vermilion makes the one sign-in action the
     clearest thing in the bar, which is also where it should be. */
  border: 1px solid var(--vermilion);
  background: var(--vermilion);
  color: var(--paper-raised);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.2px;
  /* Flat, no brand glow. Solid vermilion on paper is already the loudest
     thing in the bar; a halo around it is the "paper does not glow" line. */
  box-shadow: none;
  transition: transform var(--duration-fast) ease, border-color var(--duration-fast) ease, background var(--duration-fast) ease;
}

.login-header-btn:hover {
  border-color: var(--vermilion-deep);
  background: var(--vermilion-deep);
}

.login-header-btn:active {
  transform: scale(0.97);
}

/* Used in exactly three places, all of them the back control in a titled
   header. It was a bare 24px chevron floating in the bar with no affordance;
   it now reads as a control, matching .back-fab's language so "back" looks
   the same whether it sits in a header or over a hero photo. */
.btn-icon {
  width: 40px;
  height: 40px;
  flex: 0 0 auto;
  background: var(--paper-raised);
  border: 1px solid var(--hairline-strong);
  border-radius: 50%;
  color: var(--text-primary);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background var(--duration-fast) ease, border-color var(--duration-fast) ease, transform var(--duration-fast) ease;
}

.btn-icon:hover {
  border-color: var(--vermilion);
  color: var(--vermilion);
}

.btn-icon:active {
  transform: scale(0.94);
}

/* ── Rail card ────────────────────────────────────────────────────────────
   An oval frame around the cafe photo, the arch motif closed at the bottom
   as well as the top. Softer than the arch and it reads as a printed vignette
   rather than a doorway. */
.rail-card {
  flex: 0 0 auto;
  width: 132px;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: 0;
  border: none;
  background: none;
  text-align: left;
  cursor: pointer;
}

.rail-card-frame {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  border-radius: 999px;
  background: var(--paper-sunk);
  border: 1px solid var(--hairline);
  transition: border-color var(--duration-fast) ease;
}

.rail-card:hover .rail-card-frame {
  border-color: var(--vermilion);
}

.rail-card-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* The placeholder is the wordmark, not a photo, contain it and knock it back
   so it reads as "no picture yet" rather than as the cafe's actual frontage. */
.rail-card-frame.is-placeholder .rail-card-img {
  object-fit: contain;
  padding: var(--space-8);
  opacity: 0.3;
}

.rail-card-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-body);
  letter-spacing: var(--tracking-display);
  line-height: 1.2;
  color: var(--ink);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}

.rail-card-meta {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-label);
  color: var(--ink-soft);
}

.rail-card-score {
  font-weight: 700;
  color: var(--vermilion-text);
  font-variant-numeric: tabular-nums;
}

.activity-item {
  background: var(--bg-surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  overflow: hidden;
  height: auto;
  padding: var(--space-5) var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  cursor: pointer;
  transition: transform 0.18s ease, border-color 0.18s ease;
}

.activity-item:hover {
  border-color: rgba(206, 64, 26, 0.25);
}

.activity-item:active {
  transform: scale(0.99);
}

.activity-header {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

/* Circular, like a profile picture anywhere else; the arch is for cafe
   photography, not for people. Flat rather than the old vermilion gradient:
   this repeats once per card, and fourteen solid accents down a column is
   exactly how an accent stops being rare. */
.activity-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--paper-sunk);
  border: 1px solid var(--hairline);
  color: var(--vermilion-deep);
  font-family: var(--font-logo);
  font-weight: 700;
  font-size: 14px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.activity-meta {
  flex: 1;
  min-width: 0;
}

.activity-line {
  font-size: 14px;
  line-height: 1.35;
  color: var(--text-primary);
  word-wrap: break-word;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 8px;
  row-gap: 6px;
}

.activity-line strong {
  font-weight: 700;
}

.activity-reviewer-row {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 8px;
  row-gap: 6px;
}

.activity-reviewed-word {
  font-weight: 500;
  color: var(--text-primary);
}

.activity-cafe-link {
  color: var(--vermilion-text);
  font-weight: 700;
  text-decoration: none;
}

.activity-cafe-link:hover {
  text-decoration: underline;
}

.activity-time {
  font-size: 11px;
  color: var(--text-secondary);
  margin-top: 2px;
  font-weight: 500;
}

.activity-scores {
  display: flex;
  flex-direction: column;
  gap: 3px;
  flex-shrink: 0;
}

.activity-score-pill {
  font-size: 10px;
  font-weight: 700;
  padding: 2px 7px;
  border-radius: var(--radius-pill);
  /* Was a white wash on a white hairline, both invisible on paper, so the
     pills read as loose text rather than as pills. */
  background: var(--paper-sunk);
  border: 1px solid var(--hairline);
  color: var(--text-primary);
  white-space: nowrap;
  letter-spacing: 0.2px;
}

/* The verdict is the point of the card, somebody's actual opinion, so it
   reads at body size in ink, on the direction's 1.55 leading, rather than as
   greyed-out secondary metadata. */
.activity-verdict {
  font-size: var(--text-body);
  line-height: 1.55;
  color: var(--ink);
  font-style: italic;
  margin: 0;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
  word-break: break-word;
  text-overflow: ellipsis;
}

/* One shape, filled. Was object-fit: contain inside a fixed-height box, which
   is what produced the bars either side of a portrait shot, the box and the
   photo disagreed and the ground showed through. Giving the box the photo's
   job instead (fixed aspect, cover) means there is nothing left to letterbox.
   The trade, chosen deliberately: off-shape photos lose their edges. */
.activity-photo {
  width: 100%;
  max-width: 100%;
  aspect-ratio: var(--photo-aspect);
  height: auto;
  max-height: none;
  object-fit: cover;
  object-position: center;
  border-radius: var(--radius-sm);
  border: 1px solid var(--hairline);
  display: block;
}

.review-photo-carousel {
  position: relative;
  width: 100%;
  max-width: 100%;
  margin-top: var(--space-2);
}

.review-photo-scroll {
  display: flex;
  width: 100%;
  max-width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-behavior: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  /* Must allow vertical. This was `pan-x`, which does not mean "make sideways
     swiping work"; it means "this element consumes horizontal panning and
     nothing else", so a vertical drag starting on it was never passed up to
     the feed. The photo is full-width and ~355px tall on a phone, so a finger
     landing almost anywhere on a multi-photo post could not scroll the page.
     Single-photo posts render a bare <img> and were unaffected, which is
     exactly why the bug looked like "posts with multiple pics trap you".

     `auto` lets the browser route by gesture direction: horizontal to this
     scroller, vertical to the feed. Sideways swiping needs `overflow-x` and
     `scroll-snap-type`, both above, it never needed touch-action.

     Invisible on desktop: a mouse wheel does not consult touch-action, and
     the arrow buttons only render under `(hover: hover)`. It has to be
     checked at a touch-capable size, which is what the smoke check does. */
  touch-action: auto;
}

.review-photo-scroll::-webkit-scrollbar {
  display: none;
}

.review-photo-nav-btn {
  display: none;
  position: absolute;
  top: 50%;
  z-index: 2;
  width: 32px;
  height: 32px;
  margin: 0;
  padding: 0;
  /* Was a near-black glass puck with a cream chevron. It sits over a photo,
     so it keeps its own ground, but paper, matching .back-fab. */
  border: 1px solid var(--hairline-strong);
  border-radius: 50%;
  background: var(--paper-raised);
  color: var(--ink);
  font-size: 22px;
  line-height: 1;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transform: translateY(calc(-50% - 12px));
  box-shadow: 0 4px 12px rgba(90, 74, 58, 0.24);
}

.review-photo-nav-prev {
  left: 8px;
}

.review-photo-nav-next {
  right: 8px;
}

.review-photo-nav-btn:disabled {
  opacity: 0.35;
  cursor: default;
}

.review-photo-nav-btn:focus-visible {
  outline: 2px solid rgba(206, 64, 26, 0.95);
  outline-offset: 2px;
}

@media (hover: hover) and (pointer: fine) {
  .review-photo-nav-btn {
    display: inline-flex;
  }

  .review-photo-scroll {
    overflow-x: hidden;
    cursor: default;
  }
}

.review-photo-slide {
  flex: 0 0 100%;
  width: 100%;
  scroll-snap-align: center;
  scroll-snap-stop: always;
}

.activity-photo-slide-img,
.review-photo-slide .activity-photo {
  width: 100%;
  aspect-ratio: var(--photo-aspect);
  max-height: none;
  object-fit: cover;
  object-position: center;
  border-radius: var(--radius-sm);
  border: 1px solid var(--hairline);
  display: block;
  user-select: none;
  -webkit-user-drag: none;
}

.review-photo-dots {
  display: flex;
  justify-content: center;
  align-items: center;
  /* The dots each carry their own padding now, so the visual gap is the sum
     of two paddings; this closes the extra space that would otherwise open. */
  gap: 0;
  margin-top: 2px;
}

/* A 6px dot is a 6px tap target, which is under WCAG 2.5.8 by a factor of
   four and hard to hit on a phone regardless of the guideline. The dot stays
   6px; it is a position indicator, and a 24px dot would read as a button , 
   and the target grows around it instead: transparent padding to 24×24 with
   the visible circle drawn on the content box only. */
.review-photo-dot {
  width: 6px;
  height: 6px;
  box-sizing: content-box;
  padding: 9px;
  border: none;
  border-radius: 50%;
  background: var(--hairline-strong);
  background-clip: content-box;
  cursor: pointer;
  transition: transform 0.15s ease, background-color 0.15s ease;
}

.review-photo-dot.is-active {
  background: var(--brand);
  background-clip: content-box;
  transform: scale(1.2);
}

.review-photo-dot:focus-visible {
  /* Ring the dot, not the invisible 24px pad around it. */
  outline: 2px solid rgba(206, 64, 26, 0.95);
  outline-offset: -7px;
}

@media (max-width: 480px) {
  .activity-verdict {
    display: block;
    -webkit-line-clamp: unset;
    -webkit-box-orient: unset;
    overflow: visible;
    text-overflow: initial;
  }

  /* The photo is a fixed shape now, so capping its height would reintroduce
     exactly the mismatch the bars came from. Width drives it instead. */
}

.activity-reactions {
  display: flex;
  align-items: center;
  gap: 6px;
  padding-top: var(--space-3);
  border-top: 1px solid var(--hairline);
  flex-wrap: wrap;
}

.reaction-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 5px 10px;
  border-radius: var(--radius-pill);
  background: var(--paper-sunk);
  border: 1px solid var(--hairline);
  color: var(--text-secondary);
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 13px;
  line-height: 1;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}

.reaction-btn:hover {
  background: var(--wash-strong);
  border-color: var(--hairline-strong);
}

.reaction-btn:active {
  transform: scale(0.91);
}

.reaction-btn.active {
  background: rgba(206, 64, 26, 0.14);
  border-color: rgba(206, 64, 26, 0.5);
  color: var(--vermilion-deep);
  box-shadow: none;
}

.reaction-btn.active:hover {
  background: rgba(206, 64, 26, 0.22);
  border-color: rgba(206, 64, 26, 0.65);
}

.reaction-emoji {
  font-size: 14px;
  line-height: 1;
}

.reaction-count {
  font-size: 11px;
  font-weight: 700;
  min-width: 0;
  line-height: 1;
}

.load-more-btn {
  width: 100%;
  padding: var(--space-5);
  margin-top: 4px;
  background: rgba(206, 64, 26, 0.08);
  color: var(--brand);
  border: 1px solid rgba(206, 64, 26, 0.3);
  border-radius: var(--radius-pill);
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.18s ease, transform 0.18s ease;
  font-family: var(--font-body);
}

.load-more-btn:hover {
  background: rgba(206, 64, 26, 0.14);
}

.load-more-btn:active {
  transform: scale(0.98);
}

.load-more-btn:disabled {
  opacity: 0.5;
  cursor: default;
  transform: none;
}

.activity-empty {
  text-align: center;
  padding: 60px var(--space-8);
  color: var(--text-secondary);
}

.activity-empty-title {
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: 700;
  letter-spacing: var(--tracking-display);
  color: var(--text-primary);
  margin-bottom: 6px;
}

.activity-empty-sub {
  font-size: var(--text-meta);
}

.cafe-card {
  background: var(--bg-surface);
  border-radius: var(--radius-md);
  position: relative;
  border: 1px solid var(--hairline);
  box-shadow: var(--shadow-card);
  cursor: pointer;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
  display: flex;
  flex-direction: column;
  padding: var(--space-6);
  gap: var(--space-4);
  width: 100%;
  max-width: 320px;
  margin: 0 auto;
  height: fit-content;
}

.cafe-card:active {
  transform: scale(0.98);
  box-shadow: var(--shadow-card-press);
}

.cafes-search-wrap {
  padding: 10px 20px 8px;
  background: var(--paper);
  border-bottom: 1px solid var(--hairline);
}

/* No longer a boxed hero. Phase 10 flattened its gradients but left it as a
   bordered, tinted panel, and a boxed hero is not a shape this app uses
   anywhere else, which is exactly why it read as foreign. It now opens the
   way the feed does: a display-scale greeting, a supporting line, then the
   search. The vermilion rule under "Logged Cafes" further down does the
   dividing that the box was trying to do. */
.cafes-explore-shell {
  position: relative;
  padding: 0;
  margin: 0 0 var(--space-9);
  border: none;
  background: none;
  box-shadow: none;
}

.cafes-explore-head {
  margin-bottom: var(--space-7);
}

.cafes-explore-title {
  font-family: var(--font-display);
  font-size: var(--text-display);
  font-weight: 700;
  letter-spacing: var(--tracking-display);
  line-height: 1.15;
  color: var(--ink);
  margin: 0;
}

.cafes-explore-sub {
  margin: var(--space-2) 0 0;
  color: var(--ink-soft);
  font-size: var(--text-meta);
}

.cafes-explore-search-wrap {
  position: relative;
}

.cafes-explore-search-wrap::before {
  content: '';
  position: absolute;
  left: 18px;
  top: calc(50% - 10px);
  width: 16px;
  height: 16px;
  transform: translateY(-50%);
  pointer-events: none;
  z-index: 2;
  /* Ink-soft, matching the feed's inline search icon, the grey #969696 was
     picked against the old near-black field.
     Drawn as a *mask* rather than a background-image so the colour comes from
     the token. As a background-image the stroke had to be spelled out inside
     the data URI, where no CSS variable can reach it, so it silently kept
     #6E6255, the pre-Phase-14 --ink-soft, after the palette moved on. Under a
     mask only the SVG's alpha matters and background-color does the colouring,
     which makes that drift impossible rather than merely fixed.
     The 0.65 opacity it used to carry is gone too: the colour is now the
     colour, rather than a token diluted by an arbitrary amount. */
  background-color: var(--ink-soft);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E") center / 16px 16px no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E") center / 16px 16px no-repeat;
}

#cafes-explore-place-suggestions {
  display: none !important;
}

/* A hardcoded #1b1b1b field: the one genuinely near-black input left in the
   app, sitting on the Explore tab where the primary action is to type. */
.cafes-search-input {
  width: 100%;
  border-radius: var(--radius-pill);
  border: 1px solid var(--hairline);
  background: var(--paper-raised);
  color: var(--text-primary);
  font-size: var(--text-body);
  font-family: var(--font-body);
  padding: 12px 16px 12px 46px;
  box-shadow: none;
  transition: border-color 0.18s ease;
}

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

.cafes-search-input:focus {
  outline: none;
  border-color: rgba(206, 64, 26, 0.72);
  box-shadow:
    inset 0 -1px 2px rgba(0, 0, 0, 0.55),
    0 0 16px rgba(206, 64, 26, 0.24),
    0 0 30px rgba(206, 64, 26, 0.14);
  transform: translateY(-1px);
}

.cafes-section-divider {
  padding: var(--space-8) 20px var(--space-2);
}

/* Was a small grey uppercase label. Now the reference's section heading:
   display face, hot accent, sitting on a thin rule. */
.cafes-section-divider span {
  display: block;
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: 700;
  letter-spacing: var(--tracking-display);
  color: var(--vermilion);
  padding-bottom: var(--space-3);
  border-bottom: 1.5px solid var(--vermilion);
}

.cafes-explore-results {
  margin-top: 10px;
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
  justify-items: center;
  max-height: 360px;
  overflow-y: auto;
  padding-right: 2px;
  scrollbar-width: thin;
  scrollbar-color: rgba(206, 64, 26, 0.56) var(--paper-sunk);
}

.cafes-explore-results::-webkit-scrollbar {
  width: 10px;
}

.cafes-explore-results::-webkit-scrollbar-track {
  background: var(--paper-sunk);
  border-radius: var(--radius-pill);
}

.cafes-explore-results::-webkit-scrollbar-thumb {
  background: var(--hairline-strong);
  border: 2px solid var(--paper-sunk);
  border-radius: var(--radius-pill);
}

.cafes-explore-results::-webkit-scrollbar-thumb:hover {
  background: rgba(33, 28, 22, 0.30);
}

.explore-result-card {
  width: 100%;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  background: var(--paper-raised);
  padding: 12px;
  transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}

.explore-result-card:hover {
  transform: translateY(-1px);
  border-color: rgba(206, 64, 26, 0.28);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28), 0 0 0 1px rgba(206, 64, 26, 0.08) inset;
}

.explore-result-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.explore-result-name {
  margin: 0;
  font-size: 15px;
  color: var(--text-primary);
  font-weight: 700;
}

.explore-result-location {
  margin: 3px 0 0;
  font-size: 12px;
  color: var(--text-secondary);
}

.explore-card-footer {
  margin-top: 10px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.explore-phantom-note {
  color: var(--text-secondary);
  font-size: 11px;
}

.explore-save-btn {
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--hairline-strong);
  background: var(--paper-sunk);
  color: var(--text-secondary);
  border-radius: var(--radius-pill);
  padding: 0;
  transition: border-color 0.16s ease, color 0.16s ease, background 0.16s ease, transform 0.16s ease;
}

.explore-save-btn:active {
  transform: scale(0.95);
}

.explore-save-btn:disabled {
  opacity: 0.72;
  cursor: default;
}

.explore-save-icon {
  width: 15px;
  height: 15px;
  stroke: currentColor;
  stroke-width: 2;
  fill: transparent;
}

.explore-save-icon.is-saved {
  fill: currentColor;
  stroke: currentColor;
}

.explore-save-icon.is-saving {
  animation: exploreSavePulse 0.9s ease-in-out infinite;
}

.explore-result-card:hover .explore-save-btn {
  border-color: rgba(206, 64, 26, 0.4);
  color: var(--brand);
  background: rgba(206, 64, 26, 0.1);
}

@keyframes exploreSavePulse {
  0% { opacity: 0.52; }
  50% { opacity: 1; }
  100% { opacity: 0.52; }
}

@media (min-width: 980px) {
  .cafes-explore-results {
    max-width: 720px;
    margin-left: auto;
    margin-right: auto;
    max-height: 400px;
  }

  .explore-result-card {
    max-width: 680px;
  }
}

.cafes-empty {
  grid-column: 1 / -1;
  text-align: center;
  padding: 44px 16px 28px;
  color: var(--text-secondary);
}

.cafes-empty-title {
  font-family: var(--font-logo);
  font-size: 19px;
  font-weight: 700;
  color: var(--text-primary);
  margin-bottom: 6px;
}

.cafes-empty-sub {
  font-size: 13px;
}

.card-header {
  display: flex;
  flex-direction: column;
}

.card-img-wrapper {
  width: 100%;
  aspect-ratio: var(--photo-aspect);
  flex-shrink: 0;
  position: relative;
  border-radius: var(--radius-sm);
  overflow: hidden;
  /* The last of the near-black letterbox gradients, the same one removed
     from feed, detail and leaderboard photos in Phases 6 and 8. */
  background: var(--paper-sunk);
}

/* The placeholder is the wordmark, not a photo. This was two inline style
   attributes in feed.js, one of them a 60%-black slab, which is why no
   restyling phase could reach it. Matches the rail card's placeholder. */
.card-img-wrapper.is-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
}

.card-img-wrapper.is-placeholder .card-img {
  object-fit: contain;
  width: 60%;
  height: auto;
  opacity: 0.3;
}

/* Fills its frame, like every other photo since Phase 6, contain was what
   left the paper bars either side of a portrait shot. The placeholder
   wordmark overrides this back to contain. */
.card-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
}

.card-title {
  font-family: var(--font-display);
  letter-spacing: var(--tracking-display);
  font-size: var(--text-heading);
  font-weight: 700;
  margin-bottom: 2px;
  line-height: 1.2;
}

.card-location {
  font-size: 12px;
  color: var(--text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.card-badges {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* Was rgba(44,44,44,0.4), a 40% black slab, which is a subtle recess on
   near-black and a grey-brown smear on cream. Same class of muddy panel
   Phase 4 found on the review form, surviving here because a dark row with
   light text is internally legible and so passes a contrast check. */
.badge-star-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  background: var(--paper-sunk);
  border: 1px solid var(--hairline);
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  font-size: 12px;
  font-weight: 600;
  color: var(--text-primary);
}
/* Was `.badge-star-row span { opacity: 0.8 }`. Descendant opacity compounds:
   the score's "/5 stars" is nested inside the score span, so it rendered at
   0.64 and a label's contrast depended on how deep in the row it happened to
   sit. Quietness is a colour decision, so it is made in colour. */
.badge-star-label { color: var(--text-secondary); }

.card-meta {
  position: absolute;
  top: 8px;
  right: 8px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
}

/* Sits over the café card photo, so it keeps its own ground, paper with a
   hairline, matching every other control that floats over imagery. */
.date-pill {
  background: var(--paper-raised);
  border: 1px solid var(--hairline-strong);
  padding: 4px 9px;
  border-radius: var(--radius-pill);
  font-size: var(--text-label);
  font-weight: 700;
  color: var(--ink);
}

/* Floating Action Button */
.fab {
  position: absolute;
  bottom: calc(var(--nav-clearance) + var(--space-4));
  right: var(--space-9);
  width: 56px; height: 56px;
  border-radius: 50%;
  background: var(--brand);
  /* Was #121212, the same ink-on-vermilion bug Phase 2 fixed on .btn-primary
     and missed here, because the old gold accent was light enough to carry
     dark text and vermilion is not. */
  color: var(--paper-raised);
  border: none;
  /* Was --shadow-brand-lg, a vermilion glow. Paper does not glow: a warm
     neutral lift reads as "above the page" without haloing the accent. */
  box-shadow: 0 6px 18px rgba(90, 74, 58, 0.22);
  display: flex; justify-content: center; align-items: center;
  cursor: pointer; z-index: var(--z-fab);
  transition: transform var(--duration-fast) ease, background var(--duration-fast) ease;
}

.fab:hover {
  background: var(--brand-hover);
}

.fab:active {
  transform: scale(0.92);
}

/* Form Styles */
/* The photo dropzone is itself a <label> inside a .form-group, so this rule
   was overriding its display:flex with display:block, which is why the
   "Add photos" icon and text sat top-left instead of centred in the box, and
   why they took the field-label colour and weight rather than their own. */
.form-group label:not(.file-upload-btn) {
  display: block;
  font-size: var(--text-meta);
  color: var(--text-secondary);
  margin-bottom: 8px;
  font-weight: 600;
}

.file-upload-btn {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%;
  min-height: 124px;
  padding: 20px 16px;
  /* Was a dark panel with gold dashes; on paper it read as a hole. */
  background: var(--wash);
  color: var(--vermilion-deep);
  border: 2px dashed rgba(206, 64, 26, 0.45);
  border-radius: var(--radius-md);
  cursor: pointer;
  font-weight: 700;
  margin-bottom: 24px;
  box-shadow: none;
  transition: border-color 0.18s ease, background 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
}

.file-upload-btn:hover {
  border-color: rgba(206, 64, 26, 0.9);
  background:
    radial-gradient(120% 100% at 50% -20%, rgba(255, 226, 189, 0.1), rgba(255, 226, 189, 0)),
    rgba(44, 44, 44, 0.76);
  box-shadow:
    inset 0 -1px 0 rgba(0, 0, 0, 0.38),
    0 8px 20px rgba(0, 0, 0, 0.22);
}

.file-upload-btn:active {
  transform: scale(0.99);
}

.file-upload-btn svg {
  width: 30px;
  height: 30px;
  stroke-width: 1.9;
}

/* The -20px pulls the dropdown up over the preceding input's bottom margin so
   the two read as one control. That makes it depend on a sibling's margin,
   which is why it needs overriding in the review form below. */
.place-suggestions-list {
  display: none;
  width: 100%;
  margin-top: -20px;
  margin-bottom: 20px;
  /* Border, row separators and hover were all white-on-dark: on paper the
     dropdown had no edge, no dividers between suggestions, and no hover
     feedback at all, three invisible affordances on the single most
     important field in the app. */
  border: 1px solid var(--hairline-strong);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--bg-elevated);
  box-shadow: var(--shadow-card);
}

/* The review form's inputs carry no bottom margin (the .form-group owns the
   spacing), so the pull-up has nothing to cancel and would sit the dropdown
   20px over the input it belongs to. */
#brew-form .place-suggestions-list {
  margin-top: 0;
  margin-bottom: 0;
}

.place-suggestions-list.open {
  display: block;
}

.place-suggestion-item {
  width: 100%;
  border: 0;
  border-bottom: 1px solid var(--hairline);
  background: transparent;
  color: var(--text-primary);
  padding: 10px 12px;
  text-align: left;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.place-suggestion-item:last-child {
  border-bottom: 0;
}

.place-suggestion-item:hover {
  background: rgba(206, 64, 26, 0.08);
}

.place-suggestion-main {
  font-size: var(--text-meta);
  font-weight: 700;
}

.place-suggestion-sub {
  font-size: var(--text-label);
  color: var(--text-secondary);
}

/* Leaderboard movement ticker */
.lb-ticker {
  position: relative;
  overflow: hidden;
  margin-bottom: var(--space-6);
  padding: var(--space-3) 0;
  /* White hairlines above and below: on paper the strip had no rules at all,
     so it floated between the tabs and the first rank row. */
  border-top: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
  /* Fade the ends so items enter and leave rather than popping at the edge. */
  -webkit-mask-image: linear-gradient(to right, transparent, #000 32px, #000 calc(100% - 32px), transparent);
          mask-image: linear-gradient(to right, transparent, #000 32px, #000 calc(100% - 32px), transparent);
}

.lb-ticker[hidden] { display: none; }

.lb-ticker-track {
  display: flex;
  width: max-content;
  animation: lb-ticker-scroll 45s linear infinite;
}

/* Pause on hover or touch so entries can actually be read. */
.lb-ticker:hover .lb-ticker-track,
.lb-ticker:focus-within .lb-ticker-track,
.lb-ticker:active .lb-ticker-track {
  animation-play-state: paused;
}

.lb-ticker-run {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  padding-right: var(--space-8);
  flex: 0 0 auto;
}

@keyframes lb-ticker-scroll {
  from { transform: translateX(0); }
  /* Exactly one copy's width, so the loop is seamless. */
  to   { transform: translateX(-50%); }
}

.lb-ticker-item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-meta);
  white-space: nowrap;
}

.lb-ticker-move {
  font-weight: 700;
  font-size: var(--text-label);
}

.lb-ticker-name {
  color: var(--text-primary);
  font-weight: 600;
}

.lb-ticker-date {
  color: var(--ink-soft);
  font-size: var(--text-label);
}

.lb-ticker-score {
  color: var(--ink-soft);
  font-size: var(--text-label);
  font-variant-numeric: tabular-nums;
}

/* Motion is decorative here, the same information is on every row, so it is
   safe to stop it entirely and let the strip be scrolled by hand instead. */
@media (prefers-reduced-motion: reduce) {
  .lb-ticker {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
  .lb-ticker-track { animation: none; }
  /* The duplicate exists only to loop the animation. */
  .lb-ticker-run[aria-hidden="true"] { display: none; }
}

.place-suggestions-error {
  padding: 10px 12px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-secondary);
  border-left: 2px solid var(--brand);
}

/* Star Sliders */
.slider-container {
  margin-bottom: 14px;
  padding: 12px 12px 10px;
  border-radius: 14px;
  border: 1px solid var(--hairline);
  background: var(--paper-raised);
  box-shadow: none;
}

.slider-label {
  display: flex;
  justify-content: flex-start;
  margin-bottom: 8px;
  font-size: 14px;
  color: var(--text-secondary);
}

/* Retained for any remaining callers; the form's own section breaks now use
   the .section-head rule device shared with the feed and detail views. */
.form-section-subtitle {
  margin: 14px 0 8px;
  font-size: var(--text-label);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--tracking-label);
  color: var(--text-secondary);
}

/* Section breaks inside the form need more air above them than a heading
   following a paragraph does; they are separating groups of controls. */
#brew-form .section-head {
  margin-top: var(--space-11);
}

#brew-form .section-head:first-child {
  margin-top: 0;
}

/* Every input carries a 20px bottom margin from the base input styles, and
   .form-group adds another 24px on top, so each field in the form was
   double-spaced and the gaps read as section breaks rather than as one field
   following another. The group owns the rhythm; the control does not.
   textarea is left alone because #verdict spaces its own character counter. */
#brew-form .form-group input,
#brew-form .form-group select {
  margin-bottom: 0;
}


/* The counter belongs to the textarea, but the textarea carried the base
   input's 20px bottom margin, so the count floated in the gap below it and
   sat closer to the next field's label than to the box it describes. The
   spacing moves to the counter. */
#verdict {
  resize: none;
  margin-bottom: var(--space-2);
}

.verdict-char-remaining {
  margin-top: 0;
  margin-bottom: var(--space-9);
  font-size: var(--text-label);
  color: var(--ink-soft);
  text-align: right;
  font-weight: 600;
}

/* Secondary, deliberately. Solid vermilion with a drop shadow made "Roast"
   the loudest thing in each score card, louder than the stars, which are the
   actual input. A shortcut to the bottom of the scale should not out-rank
   choosing a score. */
.roast-score-btn {
  border: 1px solid rgba(206, 64, 26, 0.45);
  background: rgba(206, 64, 26, 0.08);
  color: var(--vermilion-deep);
  border-radius: var(--radius-pill);
  padding: 5px 10px;
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.2px;
  cursor: pointer;
  box-shadow: none;
  transition: transform 0.16s ease, background 0.16s ease, border-color 0.16s ease;
}

.roast-score-btn:hover {
  background: rgba(206, 64, 26, 0.16);
  border-color: var(--vermilion);
}

.roast-score-btn:active {
  transform: scale(0.96);
}

.sr-only-slider {
  position: absolute;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
  z-index: 10;
  top: 0; left: 0;
  margin: 0;
  pointer-events: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: none;
}

.star-input-wrapper {
  position: relative;
  display: inline-block;
  padding: 8px 0;
  min-height: 44px;
  cursor: pointer;
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}

.star-input-wrapper.is-rating-drag {
  cursor: grabbing;
  touch-action: none;
}

/* While dragging stars, stop the form scroll container from competing for one-finger pans. */
#view-form .scroll-container:has(.star-input-wrapper.is-rating-drag) {
  overflow: hidden;
  touch-action: none;
}

.stars-display {
  display: flex;
  gap: 8px;
  position: relative;
  z-index: 11;
  pointer-events: none;
}

.roast-inline-btn {
  flex-shrink: 0;
}

.rating-hint {
  display: inline-block;
  font-size: 11px;
  font-weight: 700;
  color: var(--brand);
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  background: rgba(206, 64, 26, 0.12);
  border: 1px solid rgba(206, 64, 26, 0.28);
  margin-top: 4px;
  margin-left: 10px;
  pointer-events: none;
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity 0.18s ease, transform 0.18s ease;
}

.rating-hint.visible {
  opacity: 1;
  transform: translateY(-2px);
}

/* Was coral (#ff6f6f) on a 20% crimson wash, a dark-theme pairing that
   survived the overhaul because no review in the data scores 0, so it never
   rendered to be looked at. On paper it measures 2.2:1. Same treatment the
   stamped pill already got: the wash becomes vermilion and the letterform
   takes --vermilion-deep, which is also the ramp's own low stop. */
.roasted-score-tag {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  background: rgba(206, 64, 26, 0.12);
  border: 1px solid rgba(206, 64, 26, 0.34);
  color: var(--vermilion-deep);
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.35px;
}

.review-tag-toggle-group {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.review-tag-btn {
  border: 1px solid var(--hairline-strong);
  background: var(--paper-raised);
  color: var(--text-secondary);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-pill);
  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
  transition: all 0.16s ease;
  box-shadow: none;
}

.review-tag-btn:hover {
  border-color: rgba(206, 64, 26, 0.35);
  color: var(--text-primary);
  background: rgba(206, 64, 26, 0.08);
}

.review-tag-btn.active {
  border-color: var(--vermilion);
  background: rgba(206, 64, 26, 0.12);
  color: var(--vermilion-deep);
  box-shadow: 0 6px 14px rgba(206, 64, 26, 0.18);
}

#review-tag-toggle-group {
  margin-bottom: 12px;
}

#review-tag-toggle-group + .form-section-subtitle {
  margin-top: 18px;
}

@media (max-width: 520px) {
  /* Shrink-wrap so the invisible range layer matches the visible star strip.
     Full-width wrapper made taps map to the wrong score along a wide track. */
  .star-input-wrapper {
    display: block;
    width: fit-content;
    max-width: 100%;
    padding: 10px 0 6px;
    min-height: 48px;
  }

  .stars-display {
    justify-content: flex-start;
    gap: 10px;
  }

  .rating-hint {
    display: block;
    width: fit-content;
    margin-top: 5px;
    margin-left: 0;
    transform: translateY(-2px);
  }

  .rating-hint.visible {
    transform: translateY(0);
  }
}

.review-tags-display {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: var(--space-3);
}

.review-tag-pill {
  display: inline-flex;
  align-items: center;
  padding: 5px 10px;
  border-radius: var(--radius-pill);
  border: 1px solid rgba(206, 64, 26, 0.40);
  background: rgba(206, 64, 26, 0.10);
  color: var(--vermilion-deep);
  font-size: 11px;
  font-weight: 700;
  /* A tag is not a raised object; the vermilion halo went with the glow. */
  box-shadow: none;
}

.detail-review-header {
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid var(--hairline);
  padding-bottom: 16px;
  margin-bottom: 16px;
  gap: 12px;
}

.detail-review-head-left {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  flex-wrap: wrap;
}

.detail-reviewer-identity {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.detail-reviewer-name-row {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 8px;
  row-gap: 6px;
  min-width: 0;
}

/* Matches the feed avatar: circular and flat. */
.detail-reviewer-avatar {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1px solid var(--hairline);
  background: var(--paper-sunk);
  color: var(--vermilion-deep);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  overflow: hidden;
}

.detail-reviewer-avatar-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.detail-reviewer-avatar-initial {
  font-family: var(--font-logo);
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
}

.detail-reviewer-name {
  font-size: 14px;
}

.detail-review-actions {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.detail-review-action-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border-radius: var(--radius-pill);
  padding: 5px 9px;
  font-size: 12px;
  font-weight: 700;
  border: 1px solid var(--hairline-strong);
  cursor: pointer;
  background: var(--paper-sunk);
  color: var(--text-secondary);
  transition: background 0.16s ease, border-color 0.16s ease, transform 0.16s ease, color 0.16s ease;
}

.detail-review-action-btn:hover {
  background: var(--wash-strong);
}

.detail-review-action-btn:active {
  transform: scale(0.96);
}

.detail-review-action-btn:focus-visible {
  outline: 2px solid rgba(206, 64, 26, 0.95);
  outline-offset: 1px;
}

.detail-review-action-btn-edit {
  border-color: rgba(206, 64, 26, 0.6);
  background: rgba(206, 64, 26, 0.14);
  color: var(--vermilion-deep);
}

.detail-review-action-btn-edit:hover {
  background: rgba(206, 64, 26, 0.2);
  border-color: rgba(206, 64, 26, 0.85);
}

/* Was #ff9d9d on a 10% pink wash, a pale pink on a near-white ground, which
   is the light-on-light counterpart of the dark-on-dark panels earlier
   phases kept finding. Destructive stays a distinct red rather than borrowing
   the accent, so "delete" never looks like "the main action". */
.detail-review-action-btn-delete {
  border-color: rgba(143, 42, 34, 0.38);
  background: rgba(143, 42, 34, 0.08);
  color: #8F2A22;
}

.detail-review-action-btn-delete:hover {
  background: rgba(143, 42, 34, 0.14);
  border-color: rgba(143, 42, 34, 0.55);
  color: #75211B;
}

.detail-review-action-icon {
  font-size: 12px;
  line-height: 1;
}

.detail-review-date {
  font-size: 12px;
  color: var(--text-secondary);
  white-space: nowrap;
}

.detail-review-body {
  flex: 1;
}

/* Was a 3% white wash, a faint lift on near-black, and nothing at all on
   cream. A recess on paper has to be a warm tint, not an absence. */
.detail-review-content-wrap {
  background: var(--wash);
  border-radius: var(--radius-sm);
  padding: var(--space-4);
}

.detail-review-footer {
  padding-top: var(--space-6);
  margin-top: auto;
  border-top: 1px solid var(--hairline);
}

.detail-review-score-label {
  color: var(--text-secondary);
}

.lb-podium-duration {
  margin: 0;
  font-size: var(--text-label);
  color: var(--vermilion-deep);
  font-weight: 700;
}

/* Was object-fit: contain over a near-black gradient, so every thumbnail
   rendered as a dark slab with the photo floating inside it, nine grey-black
   squares down a cream page. Filling the frame is both correct and the same
   rule the rest of the app's photos now follow. */
.lb-cafe-thumb {
  width: 56px;
  height: 56px;
  border-radius: var(--radius-sm);
  flex-shrink: 0;
  object-fit: cover;
  object-position: center;
  display: block;
  border: 1px solid var(--hairline);
  background: var(--paper-sunk);
  box-shadow: none;
}

.lb-cafe-thumb.is-placeholder {
  opacity: 0.45;
  object-fit: contain;
  padding: var(--space-3);
  background-color: var(--paper-sunk);
}

/* The single-photo case on a detail review card. Carried the same near-black
   letterbox gradient and white hairline as the carousel slides, only visible
   on reviews with exactly one photo, which is why the first sweep of this
   view missed it. */
.review-card-photo {
  width: 100%;
  border-radius: var(--radius-sm);
  margin-top: var(--space-3);
  border: 1px solid var(--hairline);
  aspect-ratio: var(--photo-aspect);
  max-height: none;
  min-height: 0;
  object-fit: cover;
  object-position: center;
  display: block;
  background: var(--paper-sunk);
}

.detail-review-content-wrap .review-photo-carousel {
  margin-top: var(--space-3);
}

/* The slide no longer needs a min-height or a ground of its own: the photo
   fills its own fixed shape, so there is never any slide left showing. */
.detail-review-content-wrap .review-photo-slide {
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  background: none;
  border: none;
}

.detail-review-content-wrap .review-photo-slide .review-card-photo,
.detail-review-content-wrap .review-card-photo.activity-photo-slide-img {
  margin-top: 0;
  width: 100%;
  aspect-ratio: var(--photo-aspect);
  max-height: none;
  min-height: 0;
  object-fit: cover;
  object-position: center;
  border-radius: var(--radius-sm);
  border: 1px solid var(--hairline);
  background: transparent;
}

.detail-review-verdict {
  font-size: var(--text-body);
  line-height: 1.55;
  color: var(--text-primary);
  margin: 0;
  font-style: italic;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* Leaderboard Tab Styles */
.lb-tab-btn {
  white-space: nowrap;
  padding: 8px 16px;
  border-radius: var(--radius-pill);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  background: var(--bg-surface);
  /* Was rgba(255,255,255,0.05), a white hairline that simply vanished when
     the ground turned to paper, leaving these tabs edgeless. */
  border: 1px solid var(--hairline);
  color: var(--text-secondary);
  transition: all var(--transition);
}
.lb-tab-btn.active {
  background: rgba(206, 64, 26, 0.1);
  /* 13px label on a vermilion wash, the fill colour reads 3.4:1 here. */
  color: var(--vermilion-text);
  font-weight: 600;
  border-color: var(--brand);
}

/* ── Leaderboard time window ───────────────────────────────────────────────
   A second axis: which reviews count, rather than what they are ranked by.
   Deliberately a different shape from .lb-tab-btn, a segmented pair rather
   than a scrolling row of pills, so the two rows do not read as one set of
   six options. */
.lb-window-row {
  display: inline-flex;
  /* The header is a flex column, so `inline-flex` alone still gets stretched
     to the full width by the default `align-items: stretch`: the track ran
     1240px wide around two buttons totalling 183px. These two make it hug its
     contents whether the parent is flex, grid or block. */
  align-self: flex-start;
  width: fit-content;
  padding: 3px;
  gap: 2px;
  margin-bottom: var(--space-6);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-pill);
  background: var(--paper-sunk);
}

.lb-window-btn {
  padding: 6px 14px;
  border: none;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--text-meta);
  font-weight: 600;
  cursor: pointer;
  transition: background var(--duration-fast) ease, color var(--duration-fast) ease;
}

.lb-window-btn:hover:not(.active) {
  color: var(--text-primary);
}

.lb-window-btn.active {
  background: var(--paper-raised);
  color: var(--vermilion-text);
  box-shadow: var(--shadow-card);
}

.lb-window-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* Sits where the ticker would be, and says why the ticker is not there. */
.lb-window-note {
  margin-bottom: var(--space-6);
  padding: var(--space-3) var(--space-4);
  border-left: 2px solid var(--brand);
  background: var(--wash);
  border-radius: var(--radius-xs);
  color: var(--text-secondary);
  font-size: var(--text-meta);
  line-height: 1.45;
}

/* Profile pill button, header entry point to Wrapped/Settings.
   Flat paper with a hairline, replacing a vermilion gradient and glow: the
   direction calls for flat colour only, and this is chrome, not the primary
   action. The one solid-vermilion thing in the header should be Login. */
.user-greeting {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 4px 12px 4px 4px;
  border-radius: var(--radius-pill);
  background: var(--paper-raised);
  border: 1px solid var(--hairline-strong);
  cursor: pointer;
  font-family: var(--font-body);
  color: var(--text-primary);
  box-shadow: none;
  transition: background var(--duration-fast) ease, border-color var(--duration-fast) ease, transform var(--duration-fast) ease;
}

.user-greeting:hover {
  background: rgba(206, 64, 26, 0.07);
  border-color: var(--vermilion);
}

.user-greeting:active {
  transform: scale(0.96);
}

/* Circular, profile pictures stay round across the app; the arch and the
   oval are for cafe photography. */
.profile-trigger-avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--vermilion);
  color: var(--paper-raised);
  font-family: var(--font-logo);
  font-weight: 700;
  font-size: 13px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  border: none;
  letter-spacing: 0;
}

.profile-trigger-name {
  font-size: 13px;
  font-weight: 700;
  color: var(--text-primary);
  letter-spacing: 0.1px;
  max-width: 110px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.profile-trigger-chevron {
  color: var(--brand);
  opacity: 0.75;
  flex-shrink: 0;
}

/* ── Action Sheet (avatar picker + future use) ───────────────────────────── */
.action-sheet-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  z-index: var(--z-sheet);
  display: flex;
  align-items: flex-end;
  animation: fadeIn 0.18s ease forwards;
}

.action-sheet {
  width: 100%;
  background: var(--bg-elevated);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  padding: 0 var(--space-6) calc(env(safe-area-inset-bottom, 0px) + var(--space-8));
  animation: actionSheetUp 0.28s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

@keyframes actionSheetUp {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}

/* The sheet panel itself is paper, but every affordance on it was still
   white-on-dark: the drag handle, the button borders, the hover and the
   cancel background were all invisible, so the sheet read as unstyled text. */
.action-sheet-handle {
  width: 36px;
  height: 4px;
  border-radius: 2px;
  background: var(--hairline-strong);
  margin: 12px auto 20px;
}

.action-sheet-title {
  font-family: var(--font-display);
  font-size: var(--text-heading);
  font-weight: 700;
  letter-spacing: var(--tracking-display);
  color: var(--ink);
  text-align: center;
  margin-bottom: 16px;
}

.action-sheet-btn {
  width: 100%;
  padding: var(--space-6) var(--space-8);
  background: var(--bg-surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  color: var(--text-primary);
  font-size: 16px;
  font-weight: 600;
  font-family: var(--font-body);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: var(--space-5);
  margin-bottom: 10px;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.12s ease;
  -webkit-tap-highlight-color: transparent;
}

.action-sheet-btn:hover {
  background: rgba(206, 64, 26, 0.07);
  border-color: var(--vermilion);
}

.action-sheet-btn:active {
  transform: scale(0.99);
  background: var(--wash-strong);
}

.action-sheet-btn svg {
  flex-shrink: 0;
  color: var(--brand);
}

.action-sheet-cancel {
  color: var(--ink-soft);
  background: var(--paper-sunk);
  border-color: var(--hairline);
  margin-top: 4px;
  justify-content: center;
}

/* ── Reviewer milestone badges (feed + café detail review cards) ── */
.reviewer-milestone-badge {
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
  flex-shrink: 0;
  margin: 0;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.15px;
  white-space: nowrap;
  line-height: 1.25;
  border: 1px solid transparent;
}

/* Each was a translucent dark gradient with light text, legible on a
   near-black ground, unreadable once paper showed through (1.06:1). On paper
   the same hierarchy reads better inverted: a light tint of the badge colour
   with dark text of that colour, which is also closer to the print
   reference than five solid blobs. */
.reviewer-milestone-badge--seedling {
  background: rgba(95, 112, 71, 0.14);
  border-color: rgba(95, 112, 71, 0.38);
  color: #3E4A2E;
}

.reviewer-milestone-badge--regular {
  background: rgba(176, 99, 44, 0.14);
  border-color: rgba(176, 99, 44, 0.38);
  color: #7A4318;
}

.reviewer-milestone-badge--rocket {
  background: rgba(78, 127, 166, 0.14);
  border-color: rgba(78, 127, 166, 0.40);
  color: #2F5673;
}

.reviewer-milestone-badge--royal {
  background: rgba(197, 150, 30, 0.20);
  border-color: rgba(160, 118, 18, 0.45);
  color: #6B4E0B;
}

/* Top tier keeps the most weight: solid rather than tinted, so it still
   reads as the rarest of the five. */
.reviewer-milestone-badge--legend {
  background: #4A2F73;
  border-color: #3A2459;
  color: var(--paper-raised);
}

/* ── What's New modal ──────────────────────────────────────────────────── */
.whats-new-modal {
  position: fixed;
  inset: 0;
  z-index: 1350;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: max(16px, env(safe-area-inset-top)) 16px max(16px, env(safe-area-inset-bottom));
}

.whats-new-modal[hidden] {
  display: none !important;
}

.whats-new-modal.is-open {
  animation: fadeIn 0.2s ease forwards;
}

.whats-new-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.72);
  backdrop-filter: blur(4px);
}

.whats-new-panel {
  position: relative;
  z-index: 1;
  width: min(100%, 420px);
  max-height: min(88vh, 640px);
  overflow-y: auto;
  padding: 24px 22px 20px;
  border-radius: var(--radius-lg);
  /* Dark panels are legible on their own terms, so the contrast audit passed
     them, but a near-black card in a paper app reads as a leftover. Full
     overlay rework is Phase 11; this is the surface correction, since this
     modal opens on load and is the first thing anyone sees. */
  border: 1px solid var(--hairline-strong);
  background: var(--paper-raised);
  box-shadow: 0 24px 48px rgba(90, 74, 58, 0.22);
  animation: whatsNewPanelIn 0.28s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

@keyframes whatsNewPanelIn {
  from {
    opacity: 0;
    transform: translateY(12px) scale(0.97);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

.whats-new-eyebrow {
  margin: 0 0 8px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--brand);
}

.whats-new-title {
  margin: 0 0 6px;
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: var(--tracking-display);
  color: var(--text-primary);
}

.whats-new-date {
  margin: 0 0 18px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-secondary);
}

.whats-new-list {
  margin: 0 0 22px;
  padding: 0 0 0 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.whats-new-list li {
  font-size: 14px;
  line-height: 1.45;
  color: var(--text-primary);
}

.whats-new-got-it {
  width: 100%;
  margin: 0;
}

body.whats-new-open {
  overflow: hidden;
}

@media (max-width: 520px) {
  .whats-new-panel {
    padding: 20px 18px 18px;
  }

  .whats-new-title {
    font-size: 20px;
  }
}

/* ── Monthly awards ceremony modal ─────────────────────────────────────── */
.monthly-awards-modal {
  position: fixed;
  inset: 0;
  z-index: 1360;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: max(16px, env(safe-area-inset-top)) 16px max(16px, env(safe-area-inset-bottom));
}

.monthly-awards-modal[hidden] {
  display: none !important;
}

.monthly-awards-modal.is-open {
  animation: fadeIn 0.2s ease forwards;
}

.monthly-awards-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.78);
  backdrop-filter: blur(5px);
}

.monthly-awards-panel {
  position: relative;
  z-index: 1;
  width: min(100%, 480px);
  max-height: min(90vh, 720px);
  overflow-y: auto;
  padding: 22px 20px 18px;
  border-radius: var(--radius-lg);
  border: 1px solid var(--hairline-strong);
  background: var(--paper-raised);
  /* Phase 3 flipped this panel to paper but left its elevation behind: a
     58%-black drop, a gold glow and an inset white highlight. */
  box-shadow: 0 24px 48px rgba(90, 74, 58, 0.22);
  animation: whatsNewPanelIn 0.28s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

.monthly-awards-eyebrow {
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--vermilion);
  text-align: center;
}

/* Was #ffe1b1 with a gold glow, pale cream on a paper panel, so the
   ceremony's own headline was near-invisible. The identical value was found
   on the trophy title in Phase 6; this is the second place it hid. Gold is
   the award colour, and here it is dark enough to read. */
.monthly-awards-title {
  margin: 0 0 6px;
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: var(--tracking-display);
  color: var(--gold);
  text-align: center;
  text-shadow: none;
}

.monthly-awards-subtitle {
  margin: 0 0 18px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-secondary);
  text-align: center;
}

.monthly-awards-winners {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin-bottom: 18px;
}

.monthly-awards-winner-card {
  min-height: 118px;
}

.monthly-awards-winner-card--featured {
  grid-column: 1 / -1;
  min-height: 132px;
}

.monthly-awards-winner-text {
  width: 100%;
}

.monthly-awards-cafe-name {
  margin-top: 4px;
  color: var(--text-primary);
  font-size: 13px;
  font-weight: 700;
  line-height: 1.25;
}

.monthly-awards-cafe-location {
  margin-top: 2px;
  color: var(--text-secondary);
  font-size: 11px;
  font-weight: 500;
  line-height: 1.25;
}

.monthly-awards-got-it {
  width: 100%;
  margin: 0;
}

body.monthly-awards-open {
  overflow: hidden;
}

@media (max-width: 520px) {
  .monthly-awards-panel {
    padding: 18px 16px 16px;
  }

  .monthly-awards-title {
    font-size: 21px;
  }

  .monthly-awards-winners {
    gap: 8px;
  }
}

/* ── Rate It roast stamps ──────────────────────────────────────────────── */
/* Gold wax-seal chip beside a stamped score row on detail review cards */
.rateit-stamp-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-left: 8px;
  padding: 3px 9px 3px 5px;
  border-radius: var(--radius-pill);
  /* Pale cream lettering (#efd9bc) on a 14–26% vermilion wash: 1.04:1 on
     paper. The chip was built to sit on the old near-black card, where a
     light letterform on a translucent accent is the correct move; on cream it
     is the same wash with nothing legible in it. The wash stays; it is what
     makes the chip read as a seal, and the lettering inverts to ink-dark
     vermilion, matching .activity-score-pill.is-stamped beside it. */
  border: 1px solid rgba(206, 64, 26, 0.38);
  background: linear-gradient(135deg, rgba(206, 64, 26, 0.16), rgba(168, 51, 15, 0.09) 55%, rgba(206, 64, 26, 0.14));
  color: var(--vermilion-deep);
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  white-space: nowrap;
  box-shadow: inset 0 1px 0 rgba(255, 250, 240, 0.5);
}

/* Stamped score pill in the activity feed */
/* Was pale gold text on a translucent gold wash, 1.21:1 on paper. A stamp
   is meant to be the scarce mark of quality, so it takes the hot accent. */
.activity-score-pill.is-stamped {
  border-color: rgba(206, 64, 26, 0.42);
  background: rgba(206, 64, 26, 0.12);
  color: var(--vermilion-deep);
  box-shadow: none;
}

.rateit-pill-seal {
  display: inline-block;
  vertical-align: -2px;
  margin-left: 4px;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.35));
}

/* Rate It action button on your own detail review cards */
/* Was a two-stop vermilion gradient carrying cream text and an inset
   highlight, tuned for the dark ground, and on paper it left #efd9bc on a
   pale wash. Flat wash, ink-dark accent text, like every other chip. */
.detail-review-action-btn-rateit {
  border-color: rgba(206, 64, 26, 0.5);
  background: rgba(206, 64, 26, 0.10);
  color: var(--vermilion-deep);
  white-space: nowrap;
  box-shadow: none;
}

.detail-review-action-btn-rateit:hover {
  background: rgba(206, 64, 26, 0.18);
  border-color: rgba(206, 64, 26, 0.8);
}

.rateit-action-seal {
  display: inline-flex;
  filter: none;
}

/* ── Rate It section in the review form ────────────────────────────────── */
.rate-it-form-section {
  margin: 6px 0 22px;
}

/* Was a near-black panel under two gradients, a diagonal sheen, an inset
   highlight and a 35%-black drop shadow, the largest surviving piece of the
   old dark shell anywhere in the app, and it rendered its own "Roast Stamps"
   heading as dark-on-dark. A contrast check had nothing to say about it,
   because a dark panel with dark text is a *consistency* failure rather than
   a legibility one; it took a screenshot to see. */
.rate-it-panel-frame {
  position: relative;
  border-radius: var(--radius-lg);
  border: 1px solid rgba(206, 64, 26, 0.28);
  padding: var(--space-7) var(--space-6) var(--space-6);
  background: rgba(206, 64, 26, 0.05);
  box-shadow: none;
  overflow: hidden;
}

.rate-it-form-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin-bottom: 14px;
}

.rate-it-form-seal {
  flex: none;
  margin-top: 2px;
  filter: none;
}

.rate-it-form-eyebrow {
  display: block;
  font-size: var(--text-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--vermilion-deep);
  font-weight: 700;
}

.rate-it-form-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-heading);
  letter-spacing: var(--tracking-display);
  color: var(--ink);
  margin-top: 1px;
}

.rate-it-form-hint {
  font-size: var(--text-meta);
  color: var(--ink-soft);
  line-height: 1.45;
  margin: 3px 0 0;
}

.rate-it-form-chips {
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
}

.rate-it-chip {
  display: flex;
  align-items: center;
  gap: 10px;
  border: 1px solid var(--hairline);
  background: var(--paper-raised);
  color: var(--text-secondary);
  border-radius: 14px;
  padding: 11px 12px;
  cursor: pointer;
  text-align: left;
  transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease, transform 0.16s ease, color 0.2s ease;
}

.rate-it-chip:hover:not(:disabled) {
  transform: translateY(-1px);
  border-color: rgba(206, 64, 26, 0.45);
  color: var(--text-primary);
}

/* Was a three-stop gradient carrying #f4e3cf cream text, legible on the old
   near-black panel, invisible on a light chip. */
.rate-it-chip.active {
  border-color: var(--vermilion);
  background: rgba(206, 64, 26, 0.14);
  color: var(--vermilion-deep);
  box-shadow: none;
}

.rate-it-chip:disabled {
  opacity: 0.45;
  cursor: default;
}

.rate-it-chip-seal {
  flex: none;
  filter: grayscale(1) opacity(0.4);
  transition: filter 0.2s ease;
}

.rate-it-chip.active .rate-it-chip-seal {
  filter: none;
}

.rate-it-chip.is-just-stamped .rate-it-chip-seal svg {
  animation: rateItStampPress 0.5s var(--ease-out-quint);
}

@keyframes rateItStampPress {
  0% {
    transform: scale(2) rotate(-16deg);
    opacity: 0;
  }
  55% {
    transform: scale(0.92) rotate(3deg);
    opacity: 1;
  }
  100% {
    transform: scale(1) rotate(0deg);
  }
}

.rate-it-chip-text {
  display: flex;
  flex-direction: column;
  line-height: 1.3;
  min-width: 0;
}

.rate-it-chip-label {
  font-size: 13px;
  font-weight: 700;
}

.rate-it-chip-note {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.2px;
  color: var(--text-secondary);
}

/* Was cream at 80%, readable on the old dark chip, all but invisible on the
   vermilion wash that replaced it. */
.rate-it-chip.active .rate-it-chip-note {
  color: var(--vermilion-deep);
  opacity: 0.85;
}

/* ── Rate It dialogue (detail view) ──────────────────────────────────────── */
.rateit-modal {
  position: fixed;
  inset: 0;
  z-index: 1380;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: max(16px, env(safe-area-inset-top)) 16px max(16px, env(safe-area-inset-bottom));
}

.rateit-modal[hidden] {
  display: none !important;
}

.rateit-modal.is-open {
  animation: fadeIn 0.2s ease forwards;
}

.rateit-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.74);
  backdrop-filter: blur(5px);
}

.rateit-panel {
  position: relative;
  z-index: 1;
  width: min(100%, 430px);
  max-height: min(88vh, 660px);
  overflow-y: auto;
  padding: 26px 22px 20px;
  border-radius: var(--radius-lg);
  /* The dialogue twin of the in-form Rate It panel fixed in Phase 7, a
     near-black gradient under a vermilion wash, a 55%-black drop shadow and
     an inset highlight. Different component, same dark shell. */
  border: 1px solid rgba(206, 64, 26, 0.35);
  background: var(--paper-raised);
  box-shadow: 0 24px 48px rgba(90, 74, 58, 0.22);
  text-align: center;
}

.rateit-modal-seal {
  display: flex;
  justify-content: center;
  margin-bottom: 10px;
  filter: none;
}

.rateit-eyebrow {
  font-size: var(--text-label);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--vermilion-deep);
  font-weight: 700;
  margin-bottom: 4px;
}

.rateit-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--text-title);
  letter-spacing: var(--tracking-display);
  color: var(--ink);
  margin-bottom: 6px;
}

.rateit-sub {
  font-size: var(--text-meta);
  color: var(--ink-soft);
  line-height: 1.5;
  margin: 0 auto 18px;
  max-width: 320px;
}

.rateit-rows {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 18px;
  text-align: left;
}

.rateit-row {
  display: flex;
  align-items: center;
  gap: 12px;
  border: 1px solid var(--hairline);
  background: var(--paper-sunk);
  border-radius: var(--radius-md);
  padding: 11px 12px;
}

.rateit-row.is-stamped {
  border-color: var(--vermilion);
  background: rgba(206, 64, 26, 0.12);
  box-shadow: none;
}

.rateit-row-loading {
  justify-content: center;
  color: var(--text-secondary);
  font-size: 13px;
}

.rateit-row-emoji {
  flex: none;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  font-size: 16px;
  background: rgba(206, 64, 26, 0.1);
  border: 1px solid rgba(206, 64, 26, 0.22);
}

.rateit-row-seal {
  flex: none;
  display: inline-flex;
  filter: none;
}

.rateit-row-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
}

.rateit-row-label {
  font-size: 14px;
  font-weight: 700;
}

.rateit-status {
  font-size: 11.5px;
  color: var(--text-secondary);
}

.rateit-status-stamped {
  color: var(--vermilion-deep);
  font-weight: 700;
}

/* Was a vermilion gradient carrying #efd9bc cream text, legible on the old
   near-black panel, washed out on paper. */
.rateit-row-btn {
  border-radius: var(--radius-pill);
  border: 1px solid rgba(206, 64, 26, 0.5);
  background: rgba(206, 64, 26, 0.10);
  color: var(--vermilion-deep);
  font-size: 12px;
  font-weight: 700;
  padding: 7px 14px;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.16s ease, border-color 0.16s ease, transform 0.16s ease;
}

.rateit-row-btn:hover {
  background: rgba(206, 64, 26, 0.18);
  border-color: var(--vermilion);
}

.rateit-row-btn:active {
  transform: scale(0.96);
}

.rateit-row-btn:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Matches the detail card's delete button: the pale-pink-on-near-white
   light-on-light pattern fixed in Phase 6, which lived here too. */
.rateit-row-btn-remove {
  border-color: rgba(143, 42, 34, 0.38);
  background: rgba(143, 42, 34, 0.08);
  color: #8F2A22;
}

.rateit-row-btn-remove:hover {
  background: rgba(143, 42, 34, 0.14);
  border-color: rgba(143, 42, 34, 0.55);
}

.rateit-done {
  width: 100%;
  margin: 0;
}

@media (max-width: 560px) {
  .rate-it-form-chips {
    grid-template-columns: 1fr;
  }

  .rate-it-chip {
    padding: 10px 12px;
  }
}

@media (max-width: 520px) {
  .rateit-panel {
    padding: 22px 16px 16px;
  }
}
