/* Secondary screens — ที่เที่ยว · เช็กอิน · พาสปอร์ต · ฉัน
 *
 * Shared chrome for the four screens hanging off the bottom nav. They reuse the home
 * screen's cards, chips, rails and rhythm from styles.css; only what does not exist on
 * /home is defined here. Loaded by every page, because the easy-mode block at the end
 * changes the home screen too. */

.page-head,
.view-switch,
.place-list-panel,
.map-panel,
.scan-section,
.nearby-checkin,
.stamps,
.memories,
.reward-teaser,
.profile,
.saved,
.settings {
  position: relative;
  background: var(--color-canvas);
}

.place-list-panel,
.map-panel,
.scan-section,
.nearby-checkin,
.passport--page,
.stamps,
.memories,
.reward-teaser,
.profile,
.saved,
.settings {
  padding-block: var(--space-section) var(--space-xs);
}


.page-head,
.view-switch,
.place-list-panel,
.map-panel,
.scan-section,
.page-head {
  display: grid;
  padding-block: var(--space-sm) var(--space-xs);
  padding-inline: var(--space-gutter);
  gap: var(--space-3xs);
  justify-items: start;
}

.page-head :is(h1, h2) {
  font-family: var(--font-display);
  font-size: clamp(1.375rem, 6vw, 1.625rem);
  font-weight: 700;
  line-height: var(--lh-display);
  letter-spacing: -0.01em;
}

.page-head p {
  color: var(--color-ink-muted);
  /* This is the sentence that says what the screen is for; it is body copy, not a note. */
  font-size: var(--text-base);
  line-height: var(--lh-body);
  line-height: 1.45;
}

/* The wordmark shrinks on secondary screens: the page title carries the identity
 * there, and two full-size titles on one screen read as a mistake. */
.wordmark--compact {
  /* Same shrink-to-room rule as the full mark (styles.css): one line beside two grown controls. */
  font-size: max(16px, min(clamp(1.25rem, 6vw, 1.5rem), var(--wordmark-room)));
}

.wordmark--compact::after {
  content: none;
}

.icon-button--ghost {
  display: block;
  width: var(--size-control);
  height: var(--size-control);
  background: none;
  box-shadow: none;
}

/* List / map is one control with two states, so it is a segmented pair rather than
 * two loose buttons (Bloomberg Connects, Michelin). */
.view-switch {
  display: grid;
  margin-inline: var(--space-gutter);
  padding: var(--space-3xs);
  gap: var(--space-3xs);
  grid-template-columns: 1fr 1fr;
  /* The shared rule above sets justify-items: start, which shrank each segment to its own
     label — an 82 px pill floating in a 199 px cell on the main browse screen. */
  justify-items: stretch;
  border: var(--rule-hair) solid var(--color-border);
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
}

.view-button {
  display: inline-flex;
  min-height: 2.25rem;
  gap: var(--space-2xs);
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}

.view-button .icon {
  width: 1.05rem;
  height: 1.05rem;
}

/* The selected segment used to be a 1.12:1 fill change on a 1.87:1 ink change — no
 * perceivable state in glare, on the control that decides which view a reader is looking at
 * and, on the route screen, which set of leg times is printed below. It takes the ink fill
 * its two sibling controls already use. */
.view-button[aria-pressed="true"] {
  background: var(--color-ink);
  color: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.view-button[aria-pressed="true"] .icon {
  color: var(--color-surface);
}

/* Full-width rows, not the home screen's rail cards: this screen is for scanning a
 * list, so the photo shrinks and the name takes the width. */
.place-list {
  display: grid;
  margin: 0;
  padding-inline: var(--space-gutter);
  padding-block-end: var(--space-xs);
  gap: var(--space-xs);
  list-style: none;
}

/* Long lists lay out one card at a time as they scroll into view instead of all ten (or
   fourteen) before the first paint — the 200 ms layout the phone spent under the fold. */
.place-row {
  content-visibility: auto;
  contain-intrinsic-block-size: auto 7rem;
  position: relative;
}

.place-row__link {
  display: grid;
  overflow: hidden;
  gap: var(--space-sm);
  grid-template-columns: 5.5rem minmax(0, 1fr);
  align-items: stretch;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
  color: inherit;
  text-decoration: none;
}

.place-row__photo {
  display: block;
  overflow: hidden;
}

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

.place-row__body {
  display: grid;
  padding-block: var(--space-xs);
  padding-inline-end: 3rem;
  gap: 0.15rem;
  align-content: center;
}

.place-row__name {
  /* The municipality's river is one word: Blink's Thai dictionary breaker was splitting
     "สะแกกรัง" across two lines in route names and headlines. */
  word-break: keep-all;
  /* No clip at any type size: the in-app easy switch used to be the only way to get the end
     of a name back, which left the OS text-scaling reader — the likelier one — truncated. */
  overflow-wrap: anywhere;
  /* The row's own name is read at arm's length outdoors; 16 px is the floor, not the opt-in. */
  font-size: var(--text-base);
  font-weight: 600;
  line-height: 1.45;
}

.save-heart--row {
  inset-block-start: 50%;
  inset-inline-end: 0.35rem;
  translate: 0 -50%;
  background: var(--color-surface);
}

/* The map is the point of this panel, so it takes a tall fixed block rather than an
 * aspect ratio that collapses on short phones. */
.map-panel {
  display: grid;
  padding-inline: var(--space-gutter);
  padding-block-end: var(--space-xs);
  gap: var(--space-xs);
}

/* A class that sets display outranks the user-agent rule for [hidden], so both view
 * panels have to say it themselves or ที่เที่ยว shows the list and the map at once. */
.map-panel[hidden],
.place-list-panel[hidden] {
  display: none;
}

/* The .page-head rule above sweeps this panel in and hands it a gutter its children already
 * carry (.place-list, .fine-note, .places-official, .section-heading), so every card sat a
 * second gutter in — 16 px right of the chip above it. */
.place-list-panel {
  padding-inline: 0;
}

.map-full,
.map-fallback {
  min-height: 22rem;
  overflow: hidden;
  border-radius: var(--radius-tile);
  background: color-mix(in srgb, var(--color-tint-chill) 42%, var(--color-surface));
  box-shadow: var(--shadow-control);
}

/* MapLibre measures its container on construction, so the map host is always laid out.
 * The drawn plate is the failure state instead: hidden by default, shown only if the
 * tile archive cannot be read. */
.map-fallback {
  display: none;
}

/* When the map cannot draw, its dead 22rem box goes with it — otherwise the plate renders
 * underneath an empty tinted card twice its own height. */
.map-shell[data-map-state="error"] {
  display: none;
}

.map-shell[data-map-state="error"] + .map-fallback {
  display: grid;
  padding: var(--space-sm);
  gap: var(--space-xs);
  place-items: center;
}

.map-fallback svg {
  width: 100%;
  max-width: 15rem;
  height: auto;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.map-fallback__note {
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
  text-align: center;
}

.map-legend {
  display: flex;
  margin: 0;
  padding: 0;
  gap: var(--space-sm);
  flex-wrap: wrap;
  list-style: none;
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
}

.map-legend li {
  display: inline-flex;
  gap: 0.3rem;
  align-items: center;
}

.dot {
  width: 0.7rem;
  height: 0.7rem;
  border: var(--rule-hair) solid var(--color-ink);
  border-radius: 50%;
}

.dot--food { background: var(--color-tint-food); }
.dot--oldtown { background: var(--color-tint-oldtown); }
.dot--merit { background: var(--color-tint-merit); }
.dot--photo { background: var(--color-tint-photo); }
.dot--chill { background: var(--color-tint-chill); }

.map-note,
.fine-note {
  color: var(--color-ink-muted);
  /* The smallest step in the app is still read outdoors by people who are not young, so the
   * floor for a note is 12 px, not 11.5. */
  font-size: var(--text-xs);
  line-height: 1.5;
}

.fine-note {
  padding-inline: var(--space-gutter);
  padding-block-start: var(--space-2xs);
}

/* The live map lives on ที่เที่ยว only. MapLibre is ~250 KB of script and the home
 * screen has a 200 KB budget, so /home keeps the drawn plate and links here. */
.map-shell {
  position: relative;
  /* The panel is a grid with justify-items: start, and MapLibre's canvas is absolutely
     positioned — so without a width of its own this shell shrink-to-fit to its 2 px borders
     and the whole map view rendered as an empty column. */
  inline-size: 100%;
}

.map-full,
.map-fallback {
  inline-size: 100%;
}

/* The pin is tapped on a moving map outdoors, so the control is the full 44 px while the
 * drawn disc stays small enough not to swallow the street it sits on. The disc carries the
 * category glyph, because eight identical dots are not a map. */
.map-pin {
  display: grid;
  width: var(--size-control);
  height: var(--size-control);
  padding: 0;
  place-items: center;
  border: 0;
  background: none;
  cursor: pointer;
}

.map-pin__disc {
  display: grid;
  width: 1.75rem;
  height: 1.75rem;
  place-items: center;
  border: 2px solid var(--color-ink);
  border-radius: 50%;
  background: var(--pin, var(--color-tint-food));
  box-shadow: var(--shadow-control);
  transition: transform var(--dur-short) var(--ease-out);
}

.map-pin__disc .icon {
  width: 0.95rem;
  height: 0.95rem;
  color: var(--color-ink);
}

.map-pin.is-active .map-pin__disc {
  border-width: 3px;
  transform: scale(1.3);
}

/* The control that widens the view to the two hill temples as well. */
.map-scope {
  position: absolute;
  inset-block-start: var(--space-sm);
  inset-inline-start: var(--space-sm);
  z-index: 2;
  min-height: var(--size-control);
  padding-inline: var(--space-md);
}

html[data-input="key"] .map-pin:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

/* The selected place rides over the map as a sheet, the way every map app does it. */
.map-place-card {
  position: absolute;
  z-index: 2;
  inset-inline: var(--space-xs);
  inset-block-end: var(--space-xs);
  display: grid;
  padding: var(--space-sm) var(--space-sm) var(--space-xs);
  gap: 0.15rem;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-float);
}

.map-place-card[hidden] {
  display: none;
}

.map-place-card__close {
  position: absolute;
  inset-block-start: 0.15rem;
  inset-inline-end: 0.35rem;
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--color-ink-muted);
  cursor: pointer;
}

.map-place-card__name {
  padding-inline-end: 2rem;
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
}

.map-place-card__meta {
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
}

.map-place-card__nav {
  justify-self: start;
  margin-block-start: var(--space-2xs);
  min-height: 2.25rem;
  padding-inline: var(--space-md);
  display: inline-flex;
  align-items: center;
  border-radius: var(--radius-pill);
  background: var(--color-primary);
  color: var(--color-on-primary);
  font-size: var(--text-sm);
  font-weight: 600;
  text-decoration: none;
}

/* MapLibre's own chrome, brought into the palette. */
.maplibregl-ctrl-group {
  border-radius: var(--radius-control) !important;
  background: var(--color-surface) !important;
  box-shadow: var(--shadow-control) !important;
}

.maplibregl-ctrl-attrib {
  /* Above the place card: the OSM credit must stay reachable while a pin is open. */
  position: relative;
  z-index: 3;
  background: color-mix(in srgb, var(--color-surface) 82%, transparent) !important;
  /* A token, so easy mode lifts it with everything else — 11 px was below the note floor. */
  font-size: var(--text-2xs);
}

.maplibregl-ctrl-attrib-button {
  /* Raising the tap target to 44 px kept MapLibre's repeating background, so the 24 px ⓘ
     tiled into four overlapping badges. */
  background-repeat: no-repeat;
  background-position: center;
  background-size: 24px 24px;
  /* The zoom buttons beside it were already raised to the 44 px target. */
  min-width: var(--size-control);
  min-height: var(--size-control);
}

/* The enlarged ⓘ sat on top of the tail of "© OpenStreetMap", which is a licence
 * obligation, not decoration: the credit reserves the control's width. */
.maplibregl-ctrl-attrib-inner {
  padding-inline-end: var(--size-control);
}

/* เช็กอิน: a viewfinder that is honest about being a mockup. */
.scan-section {
  display: grid;
  padding-inline: var(--space-gutter);
  gap: var(--space-sm);
}

.scan-frame {
  position: relative;
  display: grid;
  aspect-ratio: 1 / 1;
  max-height: 17rem;
  padding: var(--space-md);
  gap: var(--space-2xs);
  place-content: center;
  border: 2px dashed var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface-sunken);
  text-align: center;
}

.scan-frame[data-state="done"] {
  border-style: solid;
  border-color: var(--color-success);
  background: color-mix(in srgb, var(--color-success) 8%, var(--color-surface));
}

.scan-corner {
  position: absolute;
  width: 2.25rem;
  height: 2.25rem;
  border: 3px solid var(--color-primary);
}

.scan-corner--tl { inset-block-start: 1rem; inset-inline-start: 1rem; border-width: 3px 0 0 3px; border-radius: 0.75rem 0 0 0; }
.scan-corner--tr { inset-block-start: 1rem; inset-inline-end: 1rem; border-width: 3px 3px 0 0; border-radius: 0 0.75rem 0 0; }
.scan-corner--bl { inset-block-end: 1rem; inset-inline-start: 1rem; border-width: 0 0 3px 3px; border-radius: 0 0 0 0.75rem; }
.scan-corner--br { inset-block-end: 1rem; inset-inline-end: 1rem; border-width: 0 3px 3px 0; border-radius: 0 0 0.75rem 0; }

.scan-hint {
  font-size: var(--text-sm);
  font-weight: 600;
}

.scan-sub {
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
}

.wide-button {
  display: flex;
  min-height: var(--size-control);
  padding-inline: var(--space-md);
  gap: var(--space-xs);
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--color-primary);
  color: var(--color-on-primary);
  font-size: var(--text-base);
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}

.wide-button .icon {
  width: 1.2rem;
  height: 1.2rem;
  flex: none;
}

.wide-button:disabled {
  cursor: not-allowed;
  border: var(--rule-hair) solid var(--color-border);
  background: var(--color-surface-sunken);
  color: var(--color-ink-muted);
  box-shadow: none;
}

.sb-ring {
  fill: none;
  stroke: var(--color-primary);
  stroke-width: 2.5;
}

.sb-ring--inner {
  stroke-dasharray: 3 5;
  stroke-width: 1.5;
}

.sb-art {
  color: var(--color-primary);
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.ghost-button {
  min-height: 2.25rem;
  padding-inline: var(--space-md);
  /* --color-border measures 1.31:1 against both the button fill and the page: no
     perceivable edge, which is WCAG 1.4.11 for a control boundary. The switch track was
     fixed the same way, with the same token. */
  border: var(--rule-hair) solid var(--color-ink-muted);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-ink);
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}

.checkin-list,
.setting-list,
.memory-list {
  display: grid;
  margin: 0;
  padding-inline: var(--space-gutter);
  gap: var(--space-2xs);
  list-style: none;
}

.checkin-row {
  display: flex;
  min-height: var(--size-control);
  padding: var(--space-xs) var(--space-sm);
  gap: var(--space-sm);
  align-items: center;
  justify-content: space-between;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.checkin-row__body {
  display: grid;
  min-width: 0;
  flex: 1;
  gap: 0.1rem;
}

.checkin-row__name {
  /* A place name is a proper noun: no ellipsis, and no mid-cluster break either — the chip
     that used to squeeze this column now sits on its own line. */
  font-size: var(--text-base);
  font-weight: 600;
}

.chip--done {
  display: inline-flex;
  gap: 0.2rem;
  align-items: center;
  background: var(--color-surface-sunken);
  color: var(--color-ink-muted);
}

.chip--done .icon {
  width: 0.8rem;
  height: 0.8rem;
  stroke-width: 3;
}

.chip--open,
.chip--soon,
.chip--label,
.chip--done {
  white-space: nowrap;
}

/* พาสปอร์ต: the page version of the home card, plus the book itself. */
.passport--page .passport-card {
  gap: var(--space-xs);
}

.passport-ring--lg {
  width: 5rem;
  height: 5rem;
}

.passport-tier {
  font-size: var(--text-xs);
}

.passport-tier strong {
  font-family: var(--font-display);
  font-size: var(--text-base);
}

.tier-track {
  position: relative;
  z-index: 1;
  display: flex;
  padding-block-start: var(--space-2xs);
  gap: var(--space-2xs);
  align-items: center;
  justify-content: space-between;
}

.tier {
  display: grid;
  width: 2rem;
  height: 2rem;
  place-items: center;
  border: var(--rule-hair) dashed var(--color-border);
  border-radius: 50%;
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
  font-weight: 700;
}

.tier.is-done {
  border-style: solid;
  border-color: var(--color-primary-bright);
  background: var(--color-primary-bright);
  color: var(--color-on-primary);
}

.tier.is-next {
  border-style: solid;
  border-color: var(--color-primary);
  color: var(--color-primary);
}

.stamp-grid {
  display: grid;
  margin: 0;
  padding-inline: var(--space-gutter);
  gap: var(--space-xs);
  grid-template-columns: repeat(4, minmax(0, 1fr));
  list-style: none;
}

/* A stamp slot is a perforated disc: filled ones carry the place's motif, empty ones
 * keep the dashed edge so the book reads as unfinished, not broken. */
.stamp {
  display: grid;
  padding: var(--space-2xs) 0.15rem;
  gap: 0.15rem;
  place-items: center;
  border: var(--rule-hair) dashed var(--color-border);
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  color: var(--color-ink-muted);
  text-align: center;
}

.stamp .icon {
  width: 1.35rem;
  height: 1.35rem;
  stroke-width: 1.7;
}

.stamp span {
  overflow: hidden;
  max-width: 100%;
  font-size: 0.625rem;
  line-height: 1.4;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.stamp.is-on {
  border-style: solid;
  border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-surface));
  background: color-mix(in srgb, var(--color-tint-merit) 45%, var(--color-surface));
  color: var(--color-primary);
}

.stamp.is-on span {
  color: var(--color-ink);
  font-weight: 600;
}

.memory-row {
  position: relative;
  display: flex;
  padding: var(--space-2xs) var(--space-sm) var(--space-2xs) var(--space-2xs);
  gap: var(--space-sm);
  align-items: center;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.memory-row__link {
  position: absolute;
  z-index: 2;
  inset: 0;
  border-radius: inherit;
}

html[data-input="key"] .memory-row__link:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

html[data-input="key"] .memory-row:first-child .memory-row__link:focus-visible {
  outline-offset: -2px;
}

.memory-photo {
  display: block;
  width: 3.25rem;
  height: 3.25rem;
  flex: none;
  overflow: hidden;
  border-radius: var(--radius-control);
}

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

.memory-body {
  display: grid;
  min-width: 0;
  gap: 0.1rem;
}

.memory-name {
  overflow-wrap: anywhere;
  font-size: var(--text-sm);
  font-weight: 600;
}

.reward-teaser {
  padding-inline: var(--space-gutter);
  padding-block: var(--space-xs) var(--space-sm);
}

.reward-card {
  display: flex;
  min-height: 3.5rem;
  padding: var(--space-xs) var(--space-sm);
  gap: var(--space-sm);
  align-items: center;
  border: var(--rule-hair) solid var(--color-border);
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  color: inherit;
  text-decoration: none;
}

.reward-mark {
  display: grid;
  width: 2.5rem;
  height: 2.5rem;
  flex: none;
  place-items: center;
  border-radius: 50%;
  background: var(--color-tint-merit);
  color: var(--color-ink);
}

.reward-mark .icon {
  width: 1.3rem;
  height: 1.3rem;
}

.reward-copy {
  display: grid;
  min-width: 0;
  flex: 1;
  gap: 0.1rem;
}

.reward-copy strong {
  font-size: var(--text-sm);
}

.reward-copy span {
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
  line-height: 1.4;
}

/* ฉัน */
.profile {
  display: grid;
  padding-inline: var(--space-gutter);
  gap: var(--space-xs);
}

.profile-card {
  display: flex;
  padding: var(--space-sm);
  gap: var(--space-sm);
  align-items: center;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.avatar {
  display: grid;
  width: 3.25rem;
  height: 3.25rem;
  flex: none;
  place-items: center;
  border-radius: 50%;
  background: var(--color-surface-sunken);
  color: var(--color-ink-muted);
}

.avatar .icon {
  width: 1.6rem;
  height: 1.6rem;
}

.profile-copy {
  display: grid;
  min-width: 0;
  gap: 0.15rem;
}

.profile-name {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
}

.profile-sub {
  color: var(--color-ink-muted);
  /* The account-status line: whether points live on this device or in an account. */
  font-size: var(--text-sm);
}

.stat-trio {
  display: grid;
  margin: 0;
  padding: 0;
  gap: var(--space-xs);
  grid-template-columns: repeat(3, minmax(0, 1fr));
  list-style: none;
}

.stat-trio li {
  display: grid;
  padding-block: var(--space-xs);
  gap: 0.1rem;
  justify-items: center;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.stat-trio strong {
  font-family: var(--font-display);
  font-size: var(--text-md);
  line-height: 1.1;
}

.stat-trio span {
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
}

/* The row reads as one tappable band, so it is one: the link stretches over the mark and
 * the chevron, both of which are aria-hidden, so the accessible name does not change. */
.setting-row {
  position: relative;
}

/* The state word beside a demo switch, so the visible text says what aria-pressed says. */
.setting-state {
  flex: 0 0 auto;
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  white-space: nowrap;
}

.setting-copy--link::after {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  content: "";
}

.setting-row {
  display: flex;
  min-height: 3.25rem;
  padding: var(--space-xs) var(--space-sm);
  gap: var(--space-sm);
  align-items: center;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

/* A row whose only child is the demo switch: the switch owns the width. */
.setting-row--switch {
  padding: 0;
  background: none;
  box-shadow: none;
}

.setting-row--switch .demo-switch {
  margin-inline: 0;
  inline-size: 100%;
}

.setting-mark {
  display: grid;
  width: 2.25rem;
  height: 2.25rem;
  flex: none;
  place-items: center;
  border-radius: var(--radius-control);
  background: var(--color-surface-sunken);
  color: var(--color-ink-muted);
}

.setting-mark .icon {
  width: 1.15rem;
  height: 1.15rem;
}

.setting-copy {
  display: grid;
  min-width: 0;
  flex: 1;
  gap: 0.1rem;
  color: inherit;
  text-decoration: none;
}

.setting-name {
  /* The label of a settings control is body text: these rows carry ออกจากระบบ, ขอความช่วยเหลือ
     ฉุกเฉิน and the โหมดใช้ง่าย switch itself, so a reader who cannot read 14 px could not read
     the control that fixes 14 px. */
  font-size: var(--text-base);
  font-weight: 600;
}

.setting-sub {
  color: var(--color-ink-muted);
  /* This line carries opening hours, a fee, a UV reading — answers, not hints — so it sits
   * one step below its label rather than two. */
  font-size: var(--text-sm);
  line-height: 1.4;
}

.setting-value {
  flex: none;
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
  font-weight: 600;
}

.setting-chevron {
  width: 1.1rem;
  height: 1.1rem;
  flex: none;
  color: var(--color-ink-muted);
}

/* A row that also carries a description: the value and chevron sit beside the label line, not
 * beside the description, and a long value wraps instead of squeezing the label into mid-word breaks. */
.setting-row > .setting-copy {
  min-width: 0;
}

.setting-name {
  overflow-wrap: normal;
}

.setting-row:has(.setting-sub) {
  align-items: flex-start;
}

.setting-row:has(.setting-sub) > .setting-mark {
  margin-block-start: 0.1rem;
}

.setting-row:has(.setting-sub) > .setting-value,
.setting-row:has(.setting-sub) > .setting-chevron {
  margin-block-start: 0.55rem;
}

.setting-row:has(.setting-sub) > .setting-value {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 42%;
  text-align: end;
}


html[data-input="key"] .switch:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

/* ── One vertical scale for the whole app ────────────────────────────────
 * Measured across all five screens, the gaps had drifted to 3 / 8 / 12 / 16 /
 * 20 / 25 px depending on which element happened to carry padding. Two steps
 * now cover everything:
 *
 *   28 px  between content blocks   (20 above + 8 below, the home screen's step)
 *   12 px  inside a control cluster (search field, view switch, filter chips)
 *
 * Every rule below exists to hit one of those two numbers, so no section has to
 * know what its neighbour does. */
.page-head {
  padding-block: var(--space-sm) var(--space-xs);
}

.search-zone,
.view-switch,
.filter-row {
  padding-block-start: var(--space-sm);
  padding-block-end: 0;
}

/* The last chip row hands over to content, so it closes the cluster. */
.filter-row {
  padding-block-end: var(--space-xs);
}

/* One exception, deliberate: the scan frame is the screen's subject and sits straight
 * under its title. (A comment used to sit INSIDE this selector list, which silently
 * pulled .place-list-panel and .map-panel into the rule and stole their section step.) */
.scan-section {
  padding-block-start: var(--space-sm);
}

/* The section grid is justify-items: start; without a width of its own the viewfinder
 * shrank to 216 px against a 358 px column and the caption crossed the corner brackets. */
.scan-frame,
.scan-section > .wide-button {
  inline-size: 100%;
}

.reward-teaser {
  padding-block-end: var(--space-sm);
}

/* Easy mode is a real product state, not a demo toggle: bigger type, no decoration,
 * flat surfaces. The switch on "ฉัน" writes the same attribute the season flags use,
 * so one control changes every screen. */
/* The scale itself moves on the root (see the token block below); body inherits it. */

/* The motif pack asks for opacity rather than display, so flipping a season or easy mode
 * never moves the layout underneath. Stickers are the exception the pack names: they leave
 * the DOM in mourning, because a tilted line at zero opacity is still read aloud. */
html[data-easy="on"] .section-swash,
html[data-easy="on"] .section-sparkle,
html[data-easy="on"] .wordmark__sparkles,
html[data-easy="on"] .doodle,
html[data-easy="on"] .map-doodle,
html[data-easy="on"] .passport-watermark,
html[data-easy="on"] .skyline-divider {
  opacity: 0;
}

html[data-easy="on"] .sb-rays {
  display: none;
}

html[data-easy="on"] .photo-sticker {
  display: none;
}

/* Easy mode is a floor, never a cap: a feature row's name is already larger than the base
 * step, so the override says "at least this", not "exactly this". */
html[data-easy="on"] .place-name,
html[data-easy="on"] .setting-name,
html[data-easy="on"] .checkin-row__name,
html[data-easy="on"] .place-row__name {
  font-size: max(var(--text-base), 1em);
  /* Bigger type inside a nowrap clip shows LESS of a name, which is the opposite of what
   * the reader asked for, so easy mode releases the clip and lets the name wrap. */
  white-space: normal;
  overflow: visible;
  overflow-wrap: anywhere;
  text-overflow: clip;
}

html[data-easy="on"] .place-meta,
html[data-easy="on"] .place-rating,
html[data-easy="on"] .fine-note {
  font-size: var(--text-sm);
}

/* The nearby rail's fixed-width cards cannot hold the easy-mode step at 40vw: the name and
 * the rating were silently clipped mid-word. Wider cards, and the facts allowed to wrap. */
html[data-easy="on"] .place-card {
  min-width: clamp(11rem, 58vw, 14rem);
  width: clamp(11rem, 58vw, 14rem);
}

html[data-easy="on"] .profile-sub {
  /* Which account holds your stamps — and, in the guest folio, that they live only on this
     device. It was the last body sentence still under the floor in easy mode. */
  font-size: var(--text-base);
}

html[data-easy="on"] .setting-sub {
  font-size: var(--text-base);
}

html[data-easy="on"] .app-shell {
  /* Two-line labels make the bar taller than --size-nav reserves, so the last strip of the
     footer sat under it on all nineteen navigated screens. */
  padding-block-end: calc(6.75rem + env(safe-area-inset-bottom, 0px));
}

html[data-easy="on"] .nav-item span {
  /* Easy mode is turned on by a reader who cannot read the app; the five labels they navigate
     by were the one control set pinned below the body step. They take the small step and two
     lines rather than staying at the smallest one. */
  font-size: var(--text-sm);
  /* Measured, not guessed: the Thai stack needs 28 px of an 18 px step — วรรณยุกต์ above
     and สระ below — and the bar clips whatever does not fit. 1.5 still cut a pixel. */
  line-height: 1.6;
  /* keep-all is a no-op against Blink's Thai dictionary breaker — it split พาสปอร์ต, one
     loanword, into พาส/ปอร์ต. The labels are short enough to hold one line at this step. */
  white-space: nowrap;
}

/* The bottom nav and the header home button became real links when the app grew past
 * one page. Links bring the user-agent's blue and an underline with them, neither of
 * which belongs on a control that used to be a button. */
.nav-item,
.icon-button,
.wide-button,
.reward-card,
.memory-row {
  text-decoration: none;
}

a.icon-button {
  color: var(--color-ink);
}

a.nav-item {
  color: var(--color-ink-muted);
}

a.nav-item.is-active {
  color: var(--color-primary-bright);
}

/* The active nav item fills its glyph, which reads well for a house or a pin but turns
 * the passport booklet into a solid block — its outline IS the drawing. */
.nav-item--outline.is-active .icon {
  fill: none;
  stroke-width: 2.1;
}

/* ── Bringing the secondary screens up to the home screen ────────────────
 * The four screens below the nav had photography in only one place and no motif
 * layer at all, so they read as a cheaper product than /home. */


/* Photographs in the nearby list: the same places already have pictures elsewhere in
 * the app, and a row of pure text was the flattest thing in the product. */
.checkin-row__photo {
  display: block;
  width: 3rem;
  height: 3rem;
  flex: none;
  overflow: hidden;
  border-radius: var(--radius-control);
}

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

.checkin-row {
  padding-inline-start: var(--space-2xs);
}

/* พาสปอร์ต: a stamp is an impression with a date on it, not a labelled tile. */
.stamp {
  position: relative;
  padding-block: var(--space-xs) var(--space-2xs);
}

.stamp.is-on {
  border-style: solid;
}

.stamp.is-on::before {
  position: absolute;
  inset: 0.25rem;
  border: 1px dashed color-mix(in srgb, var(--color-primary) 40%, transparent);
  border-radius: calc(var(--radius-tile) - 0.25rem);
  content: "";
  pointer-events: none;
}

.stamp-date {
  /* Undimmed and on the scale: 9 px was the smallest type in the product, on the screen the
   * whole stamp loop pays off into. */
  color: var(--color-primary);
  font-size: var(--text-2xs);
  font-style: normal;
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* The entrance choreography /home has, at a fraction of the length: a page that
 * assembles itself reads as made, not rendered. One-shot, so it costs no budget. */
@media (prefers-reduced-motion: no-preference) {
  html:not([data-entrance="off"]) .page-head {
    animation: enter-fade-down 380ms var(--ease-out) backwards;
  }

  html:not([data-entrance="off"]) .search-zone,
  html:not([data-entrance="off"]) .view-switch,
  html:not([data-entrance="off"]) .filter-row {
    animation: enter-fade-up 420ms var(--ease-out) 120ms backwards;
  }

  html:not([data-entrance="off"]) .scan-frame,
  html:not([data-entrance="off"]) .passport--page .passport-card,
  html:not([data-entrance="off"]) .profile-folio,
  html:not([data-entrance="off"]) .map-shell {
    animation: enter-card 460ms var(--ease-out) 180ms backwards;
    animation-timing-function: linear(0, 0.3, 0.65, 0.92, 1.02 70%, 1.002 85%, 1);
  }

  html:not([data-entrance="off"]) .place-row,
  html:not([data-entrance="off"]) .checkin-row,
  html:not([data-entrance="off"]) .setting-row,
  html:not([data-entrance="off"]) .memory-row {
    animation: enter-fade-up 380ms var(--ease-out) backwards;
  }

  html:not([data-entrance="off"]) .place-row:nth-child(1),
  html:not([data-entrance="off"]) .checkin-row:nth-child(1),
  html:not([data-entrance="off"]) .setting-row:nth-child(1),
  html:not([data-entrance="off"]) .memory-row:nth-child(1) { animation-delay: 240ms; }
  html:not([data-entrance="off"]) .place-row:nth-child(2),
  html:not([data-entrance="off"]) .checkin-row:nth-child(2),
  html:not([data-entrance="off"]) .setting-row:nth-child(2),
  html:not([data-entrance="off"]) .memory-row:nth-child(2) { animation-delay: 290ms; }
  html:not([data-entrance="off"]) .place-row:nth-child(3),
  html:not([data-entrance="off"]) .checkin-row:nth-child(3),
  html:not([data-entrance="off"]) .setting-row:nth-child(3),
  html:not([data-entrance="off"]) .memory-row:nth-child(3) { animation-delay: 340ms; }
  html:not([data-entrance="off"]) .place-row:nth-child(n + 4),
  html:not([data-entrance="off"]) .checkin-row:nth-child(n + 4),
  html:not([data-entrance="off"]) .memory-row:nth-child(n + 4),
  html:not([data-entrance="off"]) .setting-row:nth-child(n + 4) { animation-delay: 390ms; }

  html:not([data-entrance="off"]) .stamp {
    animation: enter-pop 360ms var(--ease-out) backwards;
    animation-timing-function: linear(0, 0.2, 0.6, 1.04 60%, 1.01 78%, 1);
  }

  html:not([data-entrance="off"]) .stamp:nth-child(1) { animation-delay: 260ms; }
  html:not([data-entrance="off"]) .stamp:nth-child(2) { animation-delay: 290ms; }
  html:not([data-entrance="off"]) .stamp:nth-child(3) { animation-delay: 320ms; }
  html:not([data-entrance="off"]) .stamp:nth-child(4) { animation-delay: 350ms; }
  html:not([data-entrance="off"]) .stamp:nth-child(5) { animation-delay: 380ms; }
  html:not([data-entrance="off"]) .stamp:nth-child(6) { animation-delay: 410ms; }
  html:not([data-entrance="off"]) .stamp:nth-child(7) { animation-delay: 440ms; }
  html:not([data-entrance="off"]) .stamp:nth-child(n + 8) { animation-delay: 470ms; }
}


/* ── Repairs the consult found, all of them real defects ────────────────────
 * Controls below the 44 px target, an active nav label that sat at ~3:1 on the
 * surface, and white numerals on the bright clay tier. */
.view-button,
.filter-chip,
.ghost-button,
.passport-cta {
  min-height: var(--size-control);
}

.save-heart {
  width: var(--size-control);
  height: var(--size-control);
}

.link-more,
.policy-link,
.footer-line a {
  display: inline-flex;
  min-height: var(--size-control);
  padding-inline: var(--space-xs);
  align-items: center;
}

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

a.nav-item.is-active .icon {
  color: var(--color-primary-bright);
}

.tier.is-done {
  border-color: var(--color-primary);
  background: var(--color-primary);
  color: var(--color-on-primary);
}

/* ── The system-level move: one opening stage per screen ────────────────────
 * The title and the screen's first real object share a single sunken sheet, and
 * the content below reads as the next layer — the same relationship the cream
 * page has to the collage on /home. */
.screen-stage {
  position: relative;
  isolation: isolate;
  overflow: clip;
  border-radius: 0 0 var(--radius-sheet) var(--radius-sheet);
  background: var(--color-surface-sunken);
  box-shadow: var(--shadow-card);
}

.screen-stage > .page-head,
.screen-stage > .search-zone,
.screen-stage > .view-switch,
.screen-stage > .filter-row,
.screen-stage > .scan-section,
.screen-stage > .passport,
.screen-stage > .profile {
  background: none;
}

.screen-stage > :last-child {
  padding-block-end: var(--space-section);
}

/* Easy mode flattens the stage: fewer surfaces, more contrast. */
html[data-easy="on"] .screen-stage {
  border-radius: 0;
  background: var(--color-canvas);
  box-shadow: none;
}

/* The last undersized targets the audit found. */
.filter-chip,
.view-button {
  min-width: var(--size-control);
}

.setting-copy--link {
  min-height: var(--size-control);
  align-content: center;
}


/* MapLibre ships 29 px zoom buttons. */
.maplibregl-ctrl-group button {
  width: var(--size-control) !important;
  height: var(--size-control) !important;
}

.maplibregl-ctrl-attrib a {
  display: inline-block;
  padding-block: var(--space-2xs);
}

/* A saved place says so on the photograph. */
.saved .place-photo {
  position: relative;
}

.saved-badge {
  position: absolute;
  inset-block-start: var(--space-2xs);
  inset-inline-end: var(--space-2xs);
  display: grid;
  width: 1.5rem;
  height: 1.5rem;
  place-items: center;
  border-radius: 50%;
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
  color: var(--color-primary);
}

.saved-badge .icon {
  width: 0.9rem;
  height: 0.9rem;
  fill: currentColor;
}

.sb-rays {
  position: absolute;
  width: 6.5rem;
  height: 6.5rem;
  fill: none;
  stroke: var(--color-primary-bright);
  stroke-width: 2.4;
  stroke-linecap: round;
  opacity: 0.55;
  pointer-events: none;
}

/* A place already collected carries the passport's own mark. */
.checkin-row__photo {
  position: relative;
}

/* The collected mark is a corner badge: as a full wash over a 48 px thumbnail it read as
 * three broken images stacked down the check-in list. */
.photo-stamp {
  position: absolute;
  inset: auto 0 0 auto;
  display: grid;
  width: 1.5rem;
  height: 1.5rem;
  place-items: center;
  border-radius: 50%;
  background: color-mix(in srgb, var(--color-surface) 88%, transparent);
}

.photo-stamp svg {
  width: 1.25rem;
  height: 1.25rem;
}

.photo-stamp svg {
  width: 2rem;
  height: 2rem;
  fill: none;
  stroke: var(--color-primary);
  stroke-width: 2.5;
  stroke-linecap: round;
  transform: rotate(-8deg);
}

.photo-stamp circle:nth-of-type(2) {
  stroke-dasharray: 3 5;
  stroke-width: 1.5;
}

/* The page title carries the home screen's sparkle. */
.page-head {
  position: relative;
}

.page-head .section-sparkle {
  inset-block-start: calc(var(--space-sm) + 0.35rem);
  inset-inline-start: auto;
  inset-inline-end: var(--space-gutter);
}


/* ═══ Sol's prescriptions, applied (2026-08-15) ═════════════════════════════
 * Places gets a photographic masthead and a three-scale directory; Check-in
 * gets a pine viewfinder and a photographic payoff; Passport opens as a cover
 * spread with real stamp impressions; Me becomes one formal account folio.
 * Surfaces the catalog marks motif=none carry no decoration at all. */

/* ── Places · masthead ── */
.page-head--places {
  min-height: calc(var(--space-3xl) + var(--size-control));
  margin-inline: var(--space-gutter);
  padding: 0;
  gap: 0;
  /* The photo column's floor is capped in viewport terms so a large text size cannot grow it
   * into the title's room and stack the title one glyph per line. */
  grid-template-columns: minmax(0, 1fr) minmax(min(var(--space-3xl), 22vw), 36%);
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.page-head--places .page-head__copy {
  display: grid;
  min-width: 0;
  padding: var(--space-md);
  gap: var(--space-2xs);
  align-content: center;
}

.page-head__photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* ── Places · three photo scales in one list ── */
.place-row--feature .place-row__link {
  grid-template-columns: minmax(0, 1fr);
  /* min-content: at a large text size the card grows with its own caption instead of letting
   * the caption spill past the photo's edges. */
  grid-template-rows: minmax(min-content, 1fr);
  aspect-ratio: 16 / 10;
  gap: 0;
  overflow: visible;
  background: none;
  box-shadow: none;
}

.place-row--feature .place-row__body {
  min-width: 0;
}

.place-row--feature .place-row__meta,
.place-row--feature .place-meta {
  white-space: normal;
}

.place-row--feature .place-row__photo,
.place-row--feature .place-row__body {
  grid-area: 1 / 1;
}

.place-row--feature .place-row__photo {
  border-radius: var(--radius-card);
}

.place-row--feature .place-row__body {
  position: relative;
  z-index: var(--z-base);
  align-self: end;
  margin: var(--space-sm);
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.place-row--feature .place-row__name {
  white-space: normal;
  font-family: var(--font-display);
  font-size: var(--text-md);
  line-height: var(--lh-title);
}

.place-row--feature .save-heart--row {
  inset-block-start: var(--space-sm);
  inset-inline-end: var(--space-sm);
  translate: none;
}

.place-row--wide-photo .place-row__link {
  grid-template-columns: minmax(0, 1fr) calc(var(--space-3xl) + var(--space-3xl));
}

/* Easy mode grows the type but not the row, so a 216 px photo column left 66 px for a name
 * that then broke over five lines. The photo steps back to the plain thumbnail width and
 * the words take the rest. */
html[data-easy="on"] .place-row--wide-photo .place-row__link {
  grid-template-columns: minmax(0, 1fr) var(--space-3xl);
}

/* Not on a feature row: there the photo is the 16/10 stage the body card sits on, and this
 * 4/3 override made it taller than its own row, bleeding under the next card. */
.shop-row:not(.place-row--feature) .place-row__photo {
  align-self: start;
  inline-size: 100%;
  block-size: auto;
  aspect-ratio: 4 / 3;
}

.place-row--wide-photo .place-row__photo {
  grid-column: 2;
  grid-row: 1;
  /* Sized from its own column, never from the row. Stretched to the row height, easy mode's
     larger type pushed this box 70 px past the column and the link's overflow cropped it. */
  align-self: start;
  inline-size: 100%;
  block-size: auto;
  min-inline-size: 0;
  aspect-ratio: 4 / 3;
}

.place-row--wide-photo .place-row__body {
  grid-column: 1;
  grid-row: 1;
  padding-inline: var(--space-sm) var(--space-2xs);
}

.place-row--wide-photo .save-heart--row {
  inset-block-start: var(--space-xs);
  inset-inline-end: var(--space-xs);
  translate: none;
}

/* ── Places · the map as the second hero ── */
.map-full,
.map-fallback {
  border: var(--rule-card) solid var(--color-surface);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
}

/* The pin's geometry lives in one place, further up this file. */

/* The disc is drawn by .map-pin__disc, which carries the category glyph. An earlier
 * generation of this block drew a second circle with ::before, and both rendered: two grid
 * rows, a blank tinted disc stacked on the icon, and the pin pushed off its coordinate. */
.map-pin.is-active {
  transform: none;
}

.map-pin.is-active .map-pin__disc {
  background: var(--color-primary-bright);
}

.map-place-card {
  padding: var(--space-md);
  gap: var(--space-2xs);
  border-radius: var(--radius-card);
}

.map-place-card__name {
  padding-inline-end: var(--size-control);
}

.map-place-card__close {
  inset-block-start: var(--space-2xs);
  inset-inline-end: var(--space-2xs);
  width: var(--size-control);
  height: var(--size-control);
}

.map-place-card__nav {
  justify-self: stretch;
  justify-content: center;
  min-height: var(--size-control);
  gap: var(--space-xs);
  white-space: nowrap;
}

/* The way into the place itself, secondary to the navigation hand-off beside it. */
.map-place-card__more {
  display: inline-flex;
  min-height: var(--size-control);
  padding-inline: var(--space-xs);
  align-items: center;
  color: var(--color-primary);
  font-size: var(--text-sm);
  font-weight: 600;
}

.map-place-card__nav .icon {
  width: var(--space-lg);
  height: var(--space-lg);
}

/* The reader's own claim that they walked the route, and the note that says so. */
.route-finish {
  display: grid;
  padding-inline: var(--space-gutter);
  padding-block-start: var(--space-sm);
  gap: var(--space-3xs);
  justify-items: start;
}

/* Dormant but real: notifications.html ships this pattern commented out, and the comment
 * promises the danger surface. Without the rule, uncommenting it renders an ordinary row. */
.notif-item--emergency .notif-link {
  border-inline-start: 4px solid var(--color-danger);
  background: color-mix(in srgb, var(--color-danger) 8%, var(--color-surface));
}

/* The inline map on a detail screen: same shell as /ที่เที่ยว, shorter, and it keeps the
 * gutter the sections around it use. */
.map-shell--inline {
  position: relative;
  margin-inline: var(--space-gutter);
  margin-block-end: var(--space-xs);
  overflow: hidden;
  border-radius: var(--radius-card);
}

.map-full--inline {
  height: 15rem;
}

.map-open {
  justify-self: start;
  margin-inline: var(--space-gutter);
}

/* A numbered stop reads as an order, which is the whole point of a route map. */
.map-pin__disc--step {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--color-ink);
}

/* A refused check-in is not a payoff: the sunburst and the stamp's landing animation belong
 * to the moment a stamp is won, and this screen also has to say when one was not. */
.payoff[data-state="blocked"] .sb-rays {
  display: none;
}

.payoff[data-state="blocked"] .payoff-stamp {
  animation: none;
}

.payoff[data-state="blocked"] .payoff-disc {
  opacity: 0.55;
}

/* ── Check-in · pine viewfinder ── */
.scan-frame {
  aspect-ratio: 4 / 3;
  max-height: none;
  border: var(--rule-card) solid var(--color-ink);
  background: var(--color-ink);
  box-shadow: var(--shadow-card);
  color: var(--color-on-primary);
}

.scan-frame[data-state="done"] {
  border-color: var(--color-success);
  background: var(--color-ink);
}

.scan-mark.icon {
  width: calc(var(--space-xl) + var(--space-md));
  height: calc(var(--space-xl) + var(--space-md));
  margin-inline: auto;
  color: var(--color-primary-bright);
}

.scan-hint,
.scan-sub {
  color: var(--color-on-primary);
}

.scan-sub {
  opacity: 0.75;
}

.scan-corner {
  width: var(--space-xl);
  height: var(--space-xl);
  border: var(--rule-card) solid var(--color-primary-bright);
}

.scan-corner--tl { inset-block-start: var(--space-md); inset-inline-start: var(--space-md); border-width: var(--rule-card) 0 0 var(--rule-card); border-radius: var(--radius-control) 0 0 0; }
.scan-corner--tr { inset-block-start: var(--space-md); inset-inline-end: var(--space-md); border-width: var(--rule-card) var(--rule-card) 0 0; border-radius: 0 var(--radius-control) 0 0; }
.scan-corner--bl { inset-block-end: var(--space-md); inset-inline-start: var(--space-md); border-width: 0 0 var(--rule-card) var(--rule-card); border-radius: 0 0 0 var(--radius-control); }
.scan-corner--br { inset-block-end: var(--space-md); inset-inline-end: var(--space-md); border-width: 0 var(--rule-card) var(--rule-card) 0; border-radius: 0 0 var(--radius-control) 0; }

/* ── Check-in · nearby as one divided sheet ── */
.checkin-list {
  margin-inline: var(--space-gutter);
  padding-inline: 0;
  gap: 0;
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--color-surface);
  box-shadow: var(--shadow-card);
}

.checkin-row {
  border-radius: 0;
  background: var(--color-surface);
  box-shadow: none;
}

.checkin-row + .checkin-row {
  border-block-start: var(--rule-hair) solid var(--color-border);
}

.checkin-row:first-child {
  display: grid;
  padding: 0 0 var(--space-sm);
  gap: var(--space-sm);
  grid-template-columns: minmax(0, 1fr) auto;
}

.checkin-row:first-child .checkin-row__photo {
  grid-column: 1 / -1;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 7;
  border-radius: 0;
}

.checkin-row:first-child .checkin-row__body {
  padding-inline-start: var(--space-sm);
}

.checkin-row:first-child > .chip {
  margin-inline-end: var(--space-sm);
}

/* ── Passport · cover spread ── */
.passport--page {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  min-width: 0;
}

.passport-cover-photo {
  width: 100%;
  aspect-ratio: 16 / 7;
  overflow: hidden;
  clip-path: url(#passport-blob-a);
}

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

.passport--page .passport-card {
  z-index: var(--z-raised);
  grid-template-columns: minmax(0, 1fr);
  min-width: 0;
  margin-block-start: calc(-1 * var(--space-lg));
  margin-inline: var(--space-xs);
  box-shadow: var(--shadow-card);
}

/* Cover maturity is local presentation of the truthful stamp count. No unlock copy, asset, or
 * motion: the existing watermark and two ink rules grow firmer as the book fills. */
.passport--page .passport-card[data-cover-stage="8"] .passport-watermark {
  opacity: 0.14;
}

.passport--page .passport-card[data-cover-stage="8"]::after {
  position: absolute;
  z-index: 0;
  inset-inline: var(--space-sm);
  inset-block-end: var(--space-xs);
  height: 1px;
  background: var(--color-primary);
  content: "";
  opacity: 0.72;
  pointer-events: none;
}

.passport--page .passport-card[data-cover-stage="16"] .passport-watermark,
.passport--page .passport-card[data-cover-stage="24"] .passport-watermark {
  opacity: 0.17;
}

.passport--page .passport-card[data-cover-stage="16"]::before,
.passport--page .passport-card[data-cover-stage="16"]::after,
.passport--page .passport-card[data-cover-stage="24"]::before,
.passport--page .passport-card[data-cover-stage="24"]::after {
  position: absolute;
  z-index: 0;
  border-radius: calc(var(--radius-tile) - 3px);
  content: "";
  pointer-events: none;
}

.passport--page .passport-card[data-cover-stage="16"]::before {
  inset: 4px;
  border: 2px solid var(--color-ink);
}

.passport--page .passport-card[data-cover-stage="16"]::after {
  inset: 8px;
  border: 1px solid var(--color-primary);
}

.passport--page .passport-card[data-cover-stage="24"]::before {
  inset: 3px;
  border: 3px double var(--color-ink);
}

.passport--page .passport-card[data-cover-stage="24"]::after {
  inset: 9px;
  border: 2px solid var(--color-primary);
}

.passport--page .passport-card[data-cover-stage="16"] > *,
.passport--page .passport-card[data-cover-stage="24"] > * {
  z-index: 1;
}

.passport--page .passport-latest {
  position: relative;
  z-index: 1;
  display: grid;
  min-width: 0;
  padding: var(--space-3xs) var(--space-xs);
  gap: var(--space-xs);
  grid-template-columns: 2.5rem minmax(0, 1fr);
  align-items: center;
  border-block: var(--rule-hair) solid color-mix(in srgb, var(--color-primary) 55%, transparent);
  color: var(--color-primary);
  rotate: -0.5deg;
}

.passport--page .passport-latest img {
  width: 2.5rem;
  height: 2.5rem;
  object-fit: contain;
}

.passport--page .passport-latest__copy {
  display: grid;
  min-width: 0;
}

.passport--page .passport-latest__copy strong {
  overflow: hidden;
  color: var(--color-ink);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  line-height: var(--lh-title);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.passport--page .passport-latest__copy time {
  font-size: var(--text-2xs);
}

html:is([data-easy="on"], [data-season="royal"], [data-season="mourning"])
  .passport--page .passport-card::before,
html:is([data-easy="on"], [data-season="royal"], [data-season="mourning"])
  .passport--page .passport-card::after {
  content: none;
}

html:is([data-easy="on"], [data-season="royal"], [data-season="mourning"])
  .passport--page .passport-card[data-cover-stage] .passport-watermark {
  opacity: 0;
}

html[data-easy="on"] .passport-cover-photo,
html[data-season="royal"] .passport-cover-photo,
html[data-season="mourning"] .passport-cover-photo {
  clip-path: none;
  border-radius: var(--radius-card);
}

/* ── Passport · stamp impressions ── */
.stamp-grid {
  gap: var(--space-sm);
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.stamp {
  aspect-ratio: 1;
  padding: var(--space-xs);
  align-content: center;
  border-radius: var(--radius-pill);
  background: var(--color-surface);
}

.stamp.is-on {
  border-width: var(--rule-card);
  background: var(--color-surface);
}

.stamp.is-on::before {
  inset: var(--space-2xs);
  border: var(--rule-hair) dashed var(--color-primary);
  border-radius: var(--radius-pill);
  opacity: 0.55;
}

.stamp .icon {
  width: var(--space-lg);
  height: var(--space-lg);
}

.stamp__emblem {
  width: 3rem;
  height: 3rem;
  object-fit: contain;
}

.stamp span {
  max-width: none;
  overflow: visible;
  white-space: normal;
  text-overflow: clip;
  font-size: var(--text-2xs);
  line-height: var(--lh-title);
}

.stamp-date {
  font-size: var(--text-2xs);
}

/* ── Passport · one memory as the photograph it was ── */
.memory-row:first-child {
  position: relative;
  display: grid;
  padding: 0;
  gap: 0;
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--color-surface);
  box-shadow: none;
}

.memory-row:first-child .memory-photo {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 7;
  border-radius: 0;
}

.memory-row:first-child .memory-body {
  position: absolute;
  inset-inline: var(--space-sm);
  inset-block-end: var(--space-sm);
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-control);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.memory-row:first-child .memory-name {
  white-space: normal;
  font-family: var(--font-display);
  font-size: var(--text-base);
  line-height: var(--lh-title);
}

/* ── Me · one account folio ── */
.profile-folio {
  display: grid;
  overflow: hidden;
  border: var(--rule-card) solid var(--color-surface);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  box-shadow: var(--shadow-card);
}

.profile-folio .profile-card {
  padding: var(--space-md);
  border-radius: 0;
  background: var(--color-surface-sunken);
  box-shadow: none;
}

.profile-folio .avatar {
  width: calc(var(--space-xl) + var(--space-md));
  height: calc(var(--space-xl) + var(--space-md));
  background: var(--color-surface);
  color: var(--color-primary);
  box-shadow: var(--shadow-control);
}

.profile-folio .profile-name {
  font-size: var(--text-md);
  line-height: var(--lh-title);
}

.profile-folio .profile-sub {
  font-size: var(--text-xs);
  line-height: var(--lh-body);
}

.profile-folio > .wide-button {
  margin-inline: var(--space-sm);
  margin-block: var(--space-xs) var(--space-sm);
}

/* The signed-in branch's button was flush with the card wall; same inset as the guest branch. */
.profile-folio > .ghost-button {
  margin-inline: var(--space-sm);
}

.profile-folio .stat-trio {
  gap: 0;
  border-block-start: var(--rule-card) dashed var(--color-border);
}

.profile-folio .stat-trio li {
  min-height: calc(var(--size-control) + var(--space-xs));
  border-radius: 0;
  background: none;
  box-shadow: none;
}

.profile-folio .stat-trio li + li {
  border-inline-start: var(--rule-hair) solid var(--color-border);
}

.profile-folio .stat-trio strong {
  color: var(--color-primary);
}

html[data-easy="on"] .profile-folio {
  box-shadow: none;
}

/* ── Me · the first saved place leads ── */
.saved .card-rail {
  mask-image: none;
}

.saved .card-rail .place-card:first-child {
  width: calc(100% - var(--space-xl));
  min-width: calc(100% - var(--space-xl));
}

.saved .card-rail .place-card:first-child .place-link {
  grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
}

.saved .card-rail .place-card:first-child .place-photo {
  aspect-ratio: 4 / 3;
}

.saved .card-rail .place-card:first-child .place-body {
  padding: var(--space-sm);
  align-content: center;
}

.saved .card-rail .place-card:first-child .place-name {
  white-space: normal;
  font-family: var(--font-display);
  font-size: var(--text-base);
  line-height: var(--lh-title);
}

.saved .card-rail .place-card:first-child .place-meta {
  flex-wrap: wrap;
}

/* ── Me · one grouped settings sheet ── */
.setting-list {
  margin-inline: var(--space-gutter);
  padding-inline: 0;
  gap: 0;
  border: var(--rule-hair) solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  box-shadow: var(--shadow-card);
}

/* The row reads as one tappable band, so it is one: the link stretches over the mark and
 * the chevron, both of which are aria-hidden, so the accessible name does not change. */
.setting-row {
  position: relative;
}

.setting-copy--link::after {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  content: "";
}

.setting-row {
  min-height: calc(var(--size-control) + var(--space-sm));
  border-radius: 0;
  background: none;
  box-shadow: none;
}

/* The row reads as one tappable band, so it is one: the link stretches over the mark and the
 * chevron, both of which are aria-hidden, so the accessible name does not change. A comment
 * pasted into the middle of the selector had collapsed this pair and left a hairline above
 * the first row of every list. */
.setting-row {
  position: relative;
}

.setting-copy--link::after {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  content: "";
}

.setting-row + .setting-row {
  border-block-start: var(--rule-hair) solid var(--color-border);
}

.setting-mark {
  background: var(--color-surface-sunken);
  color: var(--color-primary);
}

html[data-easy="on"] .setting-list {
  box-shadow: none;
}

/* ── A switch whose hit area is the full 44 px square ── */
.switch {
  position: relative;
  width: var(--size-control);
  height: var(--size-control);
  flex: none;
  margin: 0;
  appearance: none;
  border: 0;
  background: none;
  cursor: pointer;
}

.switch::before {
  position: absolute;
  inset-inline: 0;
  inset-block: calc((var(--size-control) - var(--space-lg)) / 2);
  /* The off state has to be visible as a control: the hairline measured 1.32:1 on the card,
   * which is a blank patch in sunlight. The muted ink clears the 3:1 non-text floor. */
  border: 2px solid var(--color-ink-muted);
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
  content: "";
}

/* The knob is #fffbf5 on a #f7eedf track — 1.12:1, which is not an edge outdoors. A
   hairline gives the off state a visible boundary; the on state takes the brand ring. */
.switch::after {
  position: absolute;
  border: 1px solid var(--color-ink-muted);
  inset-block-start: calc((var(--size-control) - var(--space-md)) / 2);
  inset-inline-start: var(--space-2xs);
  width: var(--space-md);
  height: var(--space-md);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
  content: "";
  transition: transform var(--dur-short) var(--ease-out);
}

.switch:checked::before {
  border-color: var(--color-primary);
  background: var(--color-primary);
}

.switch:checked::after {
  border-color: var(--color-primary);
}

.switch:checked::after {
  transform: translateX(calc(var(--size-control) - var(--space-md) - (2 * var(--space-2xs))));
}

/* ═══ UX-03 truth boundary · build audience before content ══════════════════ */
.truth-banner {
  display: grid;
  gap: 2px;
  margin: 0;
  padding-block: 10px 11px;
  padding-inline-start: max(16px, env(safe-area-inset-left));
  padding-inline-end: max(16px, env(safe-area-inset-right));
  color: #3e260d;
  background: #ffe3a7;
  border-block: 1px solid #c17a20;
  line-height: 1.35;
}

.truth-banner__title {
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 700;
}

.truth-banner__copy {
  font-size: .875rem;
}

.truth-block {
  width: min(100% - 32px, 520px);
  margin: clamp(32px, 9vh, 88px) auto;
  padding: 24px 20px;
  color: var(--ink);
  background: var(--paper, #fffaf1);
  border: 1px solid rgba(14, 71, 71, .25);
  border-radius: 20px;
  box-shadow: 0 12px 28px rgba(34, 47, 45, .09);
  text-align: center;
}

.truth-block__eyebrow {
  margin: 0 0 6px;
  color: var(--teal, #0e4747);
  font-size: .875rem;
  font-weight: 700;
}

.truth-block__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.45rem, 7vw, 2rem);
  line-height: 1.25;
}

.truth-block__copy {
  margin: 12px 0 0;
  line-height: 1.65;
}

.truth-block__actions {
  display: grid;
  gap: 10px;
  margin-top: 20px;
}

html[data-easy="on"] .truth-banner {
  padding-block: 13px;
}

html[data-easy="on"] .truth-banner__title,
html[data-easy="on"] .truth-banner__copy {
  font-size: 1rem;
}

/* ═══ Safe entity resolution — neutral before any detail ════════════════════ */
.entity-detail[hidden],
.entity-directory[hidden],
.entity-state[hidden],
.bottom-nav[hidden] {
  display: none !important;
}

/* Merchant/stay are directories until a hash asks for one row. Blocking head code sets this
   marker before paint, preventing a bad hash from flashing the directory underneath. */
html[data-entity-pending="true"] [data-entity-directory],
html[data-entity-pending="true"] .bottom-nav {
  display: none !important;
}

html[data-entity-pending="true"] .entity-state[hidden] {
  display: grid !important;
}

.entity-state {
  display: grid;
  min-height: min(68vh, 36rem);
  padding: clamp(var(--space-lg), 10vh, 5rem) var(--space-gutter) var(--space-xl);
  place-items: center;
}

.entity-state__card {
  display: grid;
  width: min(100%, 32rem);
  padding: clamp(var(--space-lg), 7vw, var(--space-xl));
  gap: var(--space-sm);
  border: var(--rule-card) solid var(--color-border);
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-card);
  color: var(--color-ink);
  text-align: center;
}

.entity-state__kicker {
  color: var(--color-primary);
  font-size: var(--text-sm);
  font-weight: 700;
}

.entity-state [data-entity-state-title] {
  font-family: var(--font-body);
  font-size: clamp(1.55rem, 7vw, 2rem);
  font-weight: 700;
  line-height: var(--lh-title);
  overflow-wrap: anywhere;
}

.entity-state [data-entity-state-reason],
.entity-state noscript p {
  color: var(--color-ink-muted);
  font-size: var(--text-base);
  line-height: var(--lh-body);
}

.entity-state__actions,
.entity-state noscript {
  display: grid;
  margin-block-start: var(--space-xs);
  gap: var(--space-xs);
}

.entity-state__actions > *,
.entity-state noscript .wide-button {
  width: 100%;
}

html[data-input="key"] .entity-state [data-entity-state-title]:focus-visible {
  outline: 3px solid var(--color-ink);
  outline-offset: 5px;
  border-radius: var(--radius-control);
}

html[data-easy="on"] .entity-state [data-entity-state-reason],
html[data-easy="on"] .entity-state noscript p {
  font-size: var(--text-md);
}

@media (max-width: 340px) {
  .entity-state {
    padding-inline: var(--space-sm);
  }

  .entity-state__card {
    padding-inline: var(--space-md);
  }
}

/* ═══ Place detail — the template screen (SLICE-01, 2026-08-15) ═════════════
 * Pattern evidence: work/study-place-detail.md (50-app study). One hero, facts
 * before prose, boxed practicals, drawn map hand-off, nearby rail. The opening
 * lives on the shared sunken stage like every secondary screen. */

.place-opening {
  display: grid;
  padding-block-start: var(--space-sm);
  gap: var(--space-sm);
  background: none;
}

.place-hero {
  position: relative;
  margin-inline: 0;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border-radius: 0;
  touch-action: pan-y pinch-zoom;
}

/* JS-off: the synchronous src assignment never ran, so the noscript copy covers
 * the empty primary image edge to edge inside the same mask. */
.place-hero noscript img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

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

.hero-pager__track {
  display: flex;
  width: 100%;
  height: 100%;
  transition: transform 220ms var(--ease-out);
  will-change: transform;
}

.hero-pager__track.is-dragging {
  transition: none;
}

.hero-pager__frame {
  position: relative;
  width: 100%;
  height: 100%;
  flex: 0 0 100%;
  cursor: zoom-in;
}

html[data-input="key"] .hero-pager__frame.is-current:focus-visible {
  outline: 3px solid var(--focus-ring, #E2A63F);
  outline-offset: -3px;
}

.hero-pager__counter {
  position: absolute;
  z-index: 3;
  inset-inline-end: var(--space-sm);
  inset-block-end: var(--space-sm);
  min-width: 2.5rem;
  padding: 0.2rem 0.55rem;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--color-ink) 76%, transparent);
  color: var(--color-surface);
  font-size: var(--text-2xs);
  line-height: 1.4;
  pointer-events: none;
  text-align: center;
}

.hero-pager__counter:empty {
  display: none;
}

.place-hero__sticker {
  position: absolute;
  z-index: 2;
  inset-block-start: auto;
  inset-block-end: var(--space-sm);
  inset-inline-start: var(--space-sm);
  inset-inline-end: auto;
  font-size: 1rem;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .hero-pager__track {
    transition: none !important;
  }
}

html[data-easy="on"] .hero-pager__track,
html[data-ambient="off"] .hero-pager__track,
html[data-season="royal"] .hero-pager__track,
html[data-season="mourning"] .hero-pager__track {
  transition: none;
}

/* เช็กอิน is a link dressed as the quiet twin of the filled CTA. */
a.ghost-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}

.save-heart--hero {
  position: absolute;
  z-index: 4;
  inset-block-start: calc(var(--space-sm) + var(--space-xs));
  inset-inline-end: var(--space-xs);
  background: var(--color-surface);
}

.place-title-block {
  display: grid;
  padding-inline: var(--space-gutter);
  gap: var(--space-2xs);
}

.place-kicker {
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: 0.02em;
}

.place-title-block :is(h1, h2) {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 7vw, 1.75rem);
  font-weight: 700;
  line-height: var(--lh-display);
  letter-spacing: 0; /* Thai display never tracks negative — locked correction */
}

/* The stamp value is a fact chip, warm but ชัด. */
.chip--points {
  background: color-mix(in srgb, var(--color-tint-merit) 55%, var(--color-surface));
  color: var(--color-ink);
  white-space: nowrap;
}

.place-meta--detail {
  display: flex;
  gap: var(--space-2xs) var(--space-xs);
  flex-wrap: wrap;
  align-items: center;
}

/* Two decision facts under the name, before any action: caption above, value below. */
.place-facts {
  display: flex;
  margin: var(--space-2xs) 0 0;
  gap: var(--space-xs) var(--space-md);
  flex-wrap: wrap;
}

.place-fact {
  display: grid;
  min-width: 0;
  gap: 0.1rem;
}

.place-fact dt {
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
  font-weight: 600;
}

.place-fact dd {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: var(--lh-title);
  overflow-wrap: anywhere;
}

html[data-easy="on"] .place-fact dd {
  font-size: var(--text-base);
}

/* One filled action, the rest stay quiet (Wanderlog's one-bright-CTA move). */
.place-actions {
  display: grid;
  padding-inline: var(--space-gutter);
  gap: var(--space-xs);
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr) auto;
  align-items: stretch;
}

/* The check-in label is one word: it never breaks in the middle, and at an extreme text size
 * it clips inside its own column rather than riding over the navigate button. */
.place-actions .place-checkin {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
}

.place-checkin {
  display: inline-flex;
  gap: var(--space-2xs);
  align-items: center;
  justify-content: center;
}

.place-checkin .icon {
  width: 1.2rem;
  height: 1.2rem;
}

.icon-button--ring {
  width: var(--size-control);
  height: var(--size-control);
  border: var(--rule-hair) solid var(--color-border);
  border-radius: 50%;
  background: var(--color-surface);
}

/* The passport speaks on the place page: collected is a quiet receipt, not-yet
 * is an invitation with the points on it. */
.place-stamp-note {
  display: flex;
  min-height: var(--size-control);
  margin-inline: var(--space-gutter);
  padding: var(--space-2xs) var(--space-sm);
  gap: var(--space-xs);
  flex-wrap: wrap;
  align-items: center;
  border-radius: var(--radius-control);
  font-size: var(--text-xs);
}

.place-stamp-note .icon {
  width: 1.05rem;
  height: 1.05rem;
  flex: none;
  stroke-width: 2.5;
}

.place-stamp-note a[hidden] {
  display: none;
}

.place-stamp-note a {
  margin-inline-start: auto;
  display: inline-flex;
  min-height: var(--size-control);
  padding-inline: var(--space-2xs);
  align-items: center;
  color: inherit;
  font-weight: 600;
  text-underline-offset: 3px;
}

.place-stamp-note[data-state="collected"] {
  background: color-mix(in srgb, var(--color-success) 9%, var(--color-surface));
  color: var(--color-success);
}

.place-stamp-note[data-state="collected"] span {
  color: var(--color-ink);
}

.place-stamp-note[data-state="invite"] {
  background: color-mix(in srgb, var(--color-tint-merit) 40%, var(--color-surface));
  color: var(--color-ink);
}

/* ── Story: the longest block on the page, prose leading, one still doodle. ── */
.place-story {
  position: relative;
  padding-inline: var(--space-gutter);
}

.place-story__prose {
  display: grid;
  gap: var(--space-sm);
  max-width: 34rem;
}

.place-story__prose p {
  font-size: var(--text-base);
  line-height: 1.7;
}

.place-doodle-sun {
  position: absolute;
  inset-block-start: -0.4rem;
  inset-inline-end: var(--space-gutter);
  width: 2.1rem;
  height: 2.1rem;
}

/* ── The recurring-event card: sun-warm, factual, never a countdown. ── */
.season-card {
  display: grid;
  margin-inline: var(--space-gutter);
  padding: var(--space-md);
  gap: var(--space-3xs);
  border-radius: var(--radius-card);
  background: color-mix(in srgb, var(--color-tint-merit) 55%, var(--color-surface));
}

.season-kicker {
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
  font-weight: 600;
}

.season-name {
  color: var(--color-primary);
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 700;
  line-height: var(--lh-title);
}

.season-window {
  font-size: var(--text-sm);
  font-weight: 600;
}

.season-note {
  color: var(--color-ink-muted);
  /* The festival's whole description lives in this one line — body copy, not a note. */
  font-size: var(--text-sm);
  line-height: 1.55;
}

/* ── Map hand-off + the line only the municipality can write. ── */
.place-map {
  display: grid;
  padding-inline: var(--space-gutter);
  gap: var(--space-xs);
}

/* The section already sets the gutter; the shared card and button must not add their own. */
.place-map .map-card,
.place-map .map-open,
.place-map .map-shell--inline {
  margin-inline: 0;
}

/* The story section pads its own gutter, so the shared heading must not add a
 * second one. .place-info has no inline padding — there the heading keeps its
 * own margin and lines up with the sheet. */
.place-story .section-heading {
  max-width: 100%;
  margin-inline-start: 0;
}

/* Inline links that are not .pressable still get the locked 2px ring. */
html[data-input="key"] .place-stamp-note a:focus-visible,
html[data-input="key"] .place-info .setting-copy--link:focus-visible,
html[data-input="key"] .route-brief .setting-copy--link:focus-visible,
html[data-input="key"] .place-nearby .link-more:focus-visible,
html[data-input="key"] .footer-line a:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
  border-radius: var(--radius-control);
}

.place-official {
  display: flex;
  gap: var(--space-2xs);
  /* The seal marks the START of the attribution: centred against a three-line sentence it
     floated beside line two and read as a stray glyph. */
  align-items: flex-start;
  justify-content: center;
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
}

.place-official .icon {
  margin-block-start: 0.1rem;
}

.place-official .icon {
  width: 1rem;
  height: 1rem;
  color: #237802; /* civic green — official attribution only, per the brand lock */
}

/* The registry name in the title block: same attribution anatomy as the footer rows,
   but ranged left into the title stack instead of centred under a seal. */
.place-official--detail {
  justify-content: flex-start;
}

.place-nearby {
  background: var(--color-canvas);
}

/* The riverside tint was never defined for drawn-map pins (styles.css stops at
 * photo); places.html's fallback plate needs it too. */
.mm-pin--chill {
  fill: var(--color-tint-chill);
}

/* The motif contract: mourning and royal silence the hand-drawn layer on every
 * screen, the same set easy mode already hides. The mockup has no live season
 * switch, but the state must be correct when an operator flips it. */
html[data-season="mourning"] .section-swash,
html[data-season="mourning"] .section-sparkle,
html[data-season="mourning"] .wordmark__sparkles,
html[data-season="mourning"] .doodle,
html[data-season="mourning"] .map-doodle,
html[data-season="mourning"] .passport-watermark,
html[data-season="mourning"] .skyline-divider,
html[data-season="royal"] .section-swash,
html[data-season="royal"] .section-sparkle,
html[data-season="royal"] .wordmark__sparkles,
html[data-season="royal"] .doodle,
html[data-season="royal"] .map-doodle,
html[data-season="royal"] .passport-watermark,
html[data-season="royal"] .skyline-divider {
  opacity: 0;
}

/* The route plate is deliberately absent from the lists above: it is the plan of the route,
   the information the section heading promises, not a hand-drawn flourish. It renders in easy
   mode and in both seasons; only the swash beside its heading is suppressed. */

/* A celebratory sunburst is a mood, and mourning removes it rather than fading it. */
html[data-season="mourning"] .sb-rays,
html[data-season="royal"] .sb-rays {
  display: none;
}

/* A sticker is a mood, and mourning removes it rather than fading it, so it leaves the
 * accessibility tree with it. */
html[data-season="mourning"] .photo-sticker,
html[data-season="royal"] .photo-sticker {
  display: none;
}

/* Mourning is a palette as well as a silence: the canvas desaturates, the clay steps back
 * to ink, and photography loses its colour. Everything else keeps its structure. */
html[data-season="mourning"] {
  /* The theme tints and the ink move with the rest: a greyscale photograph beside a
   * full-saturation gold chip reads worse than no mourning treatment at all. */
  --color-ink: #33413F;
  /* The status green survived a fully greyscale page; the danger red stays saturated only
   * where it carries an emergency meaning, which mourning does not soften. */
  --color-success: #3D4F45;
  --color-tint-food: #E7E4DF;
  --color-tint-coffee: #E4E1DC;
  --color-tint-chill: #E9E7E2;
  --color-tint-merit: #E7E4DF;
  --color-tint-oldtown: #E4E1DC;
  --color-tint-photo: #E9E7E2;
  --color-canvas: #F2F0EC;
  --color-surface: #FAF9F7;
  --color-surface-sunken: #ECEAE6;
  --color-primary: #33413F;
  --color-primary-bright: #4A5A57;
  --color-accent: #33413F;
  --color-border: #DDDAD4;
  --color-page-backdrop-a: #EFEDE9;
  --color-page-backdrop-b: #E4E1DC;
}

/* Monochrome is mourning's alone. A royal occasion is formal and restrained — ornament
 * suppressed, the formal register — but its photography keeps its colour. */
/* The basemap is a canvas, so an img-only rule left the largest saturated surface on the page
   in full colour while every photograph beside it was grey. */
html[data-season="mourning"] .maplibregl-canvas,
html[data-season="mourning"] .map-full,
html[data-season="mourning"] img {
  filter: grayscale(1);
}

/* The black ribbon the brand lock puts in the header, and the commemoration notice under it. */
html[data-season="mourning"] .app-header::before {
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 0.375rem;
  background: #1f2725;
  content: "";
}

.mourning-notice {
  display: none;
}

/* The royal season suppresses every motif, desaturates the photography and mutes the check-in
   celebration app-wide; it has to be able to say why, the same way mourning does. app.js writes
   the wording for whichever season is set. */
html[data-season="mourning"] .mourning-notice,
html[data-season="royal"] .mourning-notice {
  display: grid;
  margin-inline: var(--space-gutter);
  margin-block-end: var(--space-sm);
  padding: var(--space-md);
  gap: var(--space-2xs);
  justify-items: center;
  border: var(--rule-hair) solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  color: var(--color-ink);
  font-size: var(--text-sm);
  text-align: center;
}

/* The screen's vertical rhythm joins the app-wide 28 px scale. */
.place-story,
.place-season,
.place-info,
.place-map,
.place-nearby {
  padding-block: var(--space-section) var(--space-xs);
}

/* Entrance: the same one-shot assembly the other screens use. */
@media (prefers-reduced-motion: no-preference) {
  html:not([data-entrance="off"]) .place-hero {
    animation: enter-card 460ms var(--ease-out) backwards;
    animation-timing-function: linear(0, 0.3, 0.65, 0.92, 1.02 70%, 1.002 85%, 1);
  }

  html:not([data-entrance="off"]) .place-title-block {
    animation: enter-fade-down 380ms var(--ease-out) 120ms backwards;
  }

  html:not([data-entrance="off"]) .place-actions,
  html:not([data-entrance="off"]) .place-stamp-note {
    animation: enter-fade-up 420ms var(--ease-out) 200ms backwards;
  }
}


/* ═══ เส้นทางเที่ยว — list + timeline detail (SLICE-02, 2026-08-15) ═════════
 * Pattern evidence: work/study-routes.md (50-app study + three synthesis lenses).
 * The shape the whole study converges on: photo-first cards, one numbered spine, a
 * leg row of its own between two stops, and epistemic typography — a measured
 * distance carries weight, an estimated time stays light and says "ราว". */

/* Route controls are card-shaped links, never inline prose links. */
a.route-stop__nav,
a.route-plate {
  text-decoration: none;
}

/* The route list needs a door from the screen people actually land on. */
.routes-door {
  display: flex;
  min-height: var(--size-control);
  margin-inline: var(--space-gutter);
  margin-block-start: var(--space-sm);
  padding: var(--space-xs) var(--space-sm);
  gap: var(--space-sm);
  align-items: center;
  border-radius: var(--radius-tile);
  background: color-mix(in srgb, var(--color-tint-merit) 34%, var(--color-surface));
  color: var(--color-ink);
  text-decoration: none;
}

.routes-door__copy {
  display: grid;
  min-width: 0;
  flex: 1;
  gap: var(--space-3xs);
}

.routes-door__copy strong {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
  line-height: var(--lh-title);
}

/* Direct child only: the descendant form also caught the derived <span data-count> inside
 * the headline, printing "ที่พักและร้านค้า ₁₂ แห่ง" with a shrunken grey numeral. */
.routes-door__copy > span {
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
  line-height: 1.45;
}

.routes-door .icon {
  width: 1.2rem;
  height: 1.2rem;
  flex: none;
  color: var(--color-primary);
}

html[data-input="key"] .routes-door:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

/* ── The list ── */
.routes-curator {
  justify-content: flex-start;
  margin-inline: var(--space-gutter);
  padding-block-start: var(--space-xs);
}

.route-row__tags {
  display: flex;
  gap: var(--space-2xs);
  flex-wrap: wrap;
  align-items: center;
}

.chip--theme {
  background: color-mix(in srgb, var(--color-tint-photo) 55%, var(--color-surface));
  color: var(--color-ink);
}

.chip--theme.chip--merit { background: color-mix(in srgb, var(--color-tint-merit) 60%, var(--color-surface)); }
.chip--theme.chip--food { background: color-mix(in srgb, var(--color-tint-food) 60%, var(--color-surface)); }
.chip--theme.chip--oldtown { background: color-mix(in srgb, var(--color-tint-oldtown) 60%, var(--color-surface)); }
.chip--theme.chip--nature { background: color-mix(in srgb, var(--color-tint-chill) 60%, var(--color-surface)); }

/* The span chip is the canonical duration tag, so it reads as a fact, not a mood. */
.chip--span {
  border: var(--rule-hair) solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-ink-muted);
}

/* Thai has no word spaces, so the dot-separated meta line wraps as a block rather
 * than truncating mid-cluster. 14 px is the outdoor floor for a line of facts. */
.route-meta {
  display: block;
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: 1.5;
  white-space: normal;
}

.route-row .place-row__name {
  white-space: normal;
}

/* A route name is longer than a place name, so the wide-photo row gives the words
 * back the width the 192 px photo column was taking. */
.route-row.place-row--wide-photo .place-row__link {
  grid-template-columns: minmax(0, 1fr) calc(var(--space-3xl) + var(--space-xl));
}

/* A shop row carries a name, a blurb and an hours line, which is one line more than a place
 * row: at 390 px the 192 px photo column left 110 px for a 159 px unbreakable Thai run, and
 * the body — later in the DOM than the photo — painted its last syllables on the photograph.
 * The same 136 px column the route rows already take. */
.shop-row.place-row--wide-photo .place-row__link {
  grid-template-columns: minmax(0, 1fr) calc(var(--space-3xl) + var(--space-xl));
}

/* Both panels are display-typed by class, so the hidden attribute needs to say so. */
.routes-empty[hidden],
.route-list[hidden],
.route-groups[hidden],
.route-row[hidden] {
  display: none;
}

.routes-empty {
  display: grid;
  margin-inline: var(--space-gutter);
  padding: var(--space-lg) var(--space-md);
  gap: var(--space-2xs);
  justify-items: center;
  border: var(--rule-hair) dashed var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  text-align: center;
}

.routes-empty__doodle {
  width: 1.8rem;
  height: 1.8rem;
  margin-block-end: var(--space-2xs);
}

.routes-empty__title {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 700;
  line-height: var(--lh-title);
}

.routes-empty__body {
  max-width: 22rem;
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.routes-empty .ghost-button {
  margin-block-start: var(--space-xs);
}

.route-groups {
  display: grid;
  gap: var(--space-section);
}

.route-group[hidden] {
  display: none;
}

.route-group__heading {
  margin-block-end: var(--space-xs);
}

.route-group__link {
  display: inline-flex;
  min-height: 44px;
  align-items: center;
  color: var(--color-ink);
  text-decoration: none;
}

.route-group__list {
  gap: var(--space-xs);
}

/* ── The detail: opening ── */
/* One word, one line: Blink's Thai dictionary breaker splits สะแกกรัง, and the river is the
 * municipality's own name for itself. Applied where names and headlines are printed. */
.route-title-block h1,
.place-row__name,
.place-name,
.news-headline,
.memory-name,
.stay-row__name,
.route-stop__name {
  word-break: keep-all;
}

.route-title-block {
  gap: var(--space-3xs);
}

.route-curator {
  justify-content: flex-start;
  padding-block-start: var(--space-3xs);
}

/* Facts before prose, in cells — one Thai caption per column, never one dense line. */
.route-stats {
  display: grid;
  margin: 0;
  padding-inline: var(--space-gutter);
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.route-stat {
  display: grid;
  padding-inline: var(--space-xs);
  gap: var(--space-3xs);
  border-inline-start: var(--rule-hair) solid var(--color-border);
}

.route-stat:first-child {
  padding-inline-start: 0;
  border-inline-start: 0;
}

.route-stat dt {
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
  font-weight: 600;
}

/* The three facts are what a reader checks before committing to a walk, so the
 * numerals carry body scale rather than caption scale. */
.route-stat dd {
  margin: 0;
  font-size: var(--text-base);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: var(--lh-title);
}

.route-actions {
  grid-template-columns: minmax(0, 1fr) auto;
}

.route-start-note {
  padding-inline: var(--space-gutter);
  color: var(--color-ink-muted);
  /* Names the button's destination — the smallest type on the screen was the wrong place
     for the one sentence that explains where a tap goes. */
  font-size: var(--text-sm);
  line-height: 1.5;
}

/* ── The ribbon: the whole sequence before the first stop card ── */
.route-ribbon-block {
  padding-block: var(--space-section) 0;
}

.route-ribbon {
  display: flex;
  margin: 0;
  padding-inline: var(--space-gutter);
  padding-block-end: var(--space-2xs);
  gap: var(--space-xs);
  list-style: none;
}

.route-ribbon-scroll {
  overflow-x: auto;
  scrollbar-width: none;
  overscroll-behavior-inline: contain;
}

.route-ribbon-scroll::-webkit-scrollbar {
  display: none;
}

/* Five stops overflow 390 px, so the edge dissolves the way the home rails do. */
.route-ribbon-scroll {
  padding-inline-end: calc(var(--space-gutter) + 2rem);
  mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent);
}

html[data-input="key"] .route-ribbon-scroll:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: -2px;
  border-radius: var(--radius-control);
  /* the fade is decoration; it must never clip the focus ring */
  mask-image: none;
}

.route-ribbon__stop {
  position: relative;
  display: grid;
  min-width: 4.5rem;
  max-width: 6.5rem;
  gap: var(--space-3xs);
  flex: none;
  justify-items: center;
  text-align: center;
}

/* The dashes join the dots, so the row reads as one walk rather than five badges. */
.route-ribbon__stop:not(:last-child)::after {
  content: "";
  position: absolute;
  inset-block-start: 0.85rem;
  inset-inline-start: calc(50% + 1.1rem);
  width: calc(100% - 2.2rem + var(--space-xs));
  height: 2px;
  background: repeating-linear-gradient(to right, var(--color-border) 0 5px, transparent 5px 10px);
}

.route-ribbon__dot {
  display: grid;
  width: 1.75rem;
  height: 1.75rem;
  place-items: center;
  border-radius: 50%;
  background: var(--color-surface);
  box-shadow: inset 0 0 0 2px var(--color-primary);
  color: var(--color-primary);
  font-size: var(--text-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.route-ribbon__name {
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
  line-height: 1.4;
}

/* ── The mode control: it changes every number on the screen ── */
.route-mode-block {
  display: grid;
  padding-block: var(--space-sm) 0;
  gap: var(--space-2xs);
  justify-items: start;
}

.route-mode {
  padding-block: 0;
}

.route-mode .view-button {
  gap: var(--space-2xs);
}

.route-mode-total {
  padding-inline: var(--space-gutter);
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
  line-height: 1.5;
}

/* ── The timeline: one spine, numbered, with the leg as its own row ── */
.route-timeline-block {
  padding-block: var(--space-section) var(--space-xs);
}

.route-spine {
  position: relative;
  display: grid;
  padding-inline: var(--space-gutter);
  gap: var(--space-xs);
}

.route-timeline {
  display: grid;
  margin: 0;
  padding: 0;
  gap: var(--space-xs);
  list-style: none;
}

/* The rail runs from the first badge to the terminus flag, behind everything. */
.route-spine::before {
  content: "";
  position: absolute;
  inset-block: 1.4rem 1.4rem;
  inset-inline-start: calc(var(--space-gutter) + 1.3rem);
  width: 2px;
  background: repeating-linear-gradient(to bottom, var(--color-border) 0 6px, transparent 6px 12px);
}

.route-stop {
  position: relative;
  display: grid;
  gap: var(--space-xs);
  grid-template-columns: 2.75rem minmax(0, 1fr);
  align-items: start;
}

/* The terminus closes the spine: same two columns, one line of type. */
.route-timeline__end {
  display: grid;
  gap: var(--space-xs);
  grid-template-columns: 2.75rem minmax(0, 1fr);
  align-items: center;
}

.route-stop__badge {
  display: grid;
  width: 2.75rem;
  height: 2.75rem;
  place-items: center;
  border-radius: 50%;
  background: var(--color-primary);
  color: var(--color-on-primary);
  font-size: var(--text-base);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.route-stop__card {
  display: grid;
  padding: var(--space-xs);
  gap: var(--space-xs);
  grid-template-columns: clamp(5.5rem, 27vw, 7rem) minmax(0, 1fr);
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.route-stop__photo {
  display: block;
  overflow: hidden;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-control);
}

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

.route-stop__body {
  display: grid;
  min-width: 0;
  gap: var(--space-3xs);
  align-content: start;
}

.route-stop__name {
  color: var(--color-ink);
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
  line-height: var(--lh-title);
  letter-spacing: 0;
  text-decoration: none;
}

/* The name carries a real target height rather than the 23 px a bare line of type
 * gives it — and a shop stop's name sits on the same baseline as a POI stop's, even
 * though only one of them is a link. */
.route-stop__name {
  display: flex;
  min-height: var(--size-control);
  align-items: center;
}

a.route-stop__name:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

html[data-input="key"] a.route-stop__name:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
  border-radius: var(--radius-control);
}

/* A shop stop is real content with no detail page yet, so it says so rather than
 * linking somewhere that does not exist. Shop pages land with /stay-shop. */
.chip--shop {
  justify-self: start;
  background: var(--color-surface-sunken);
  color: var(--color-ink-muted);
}

.route-stop__line {
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

/* The practical fact sits apart from the sentence, so an eye scanning for opening
 * hours never has to read the story first. */
.route-stop__tip {
  margin-block-start: var(--space-3xs);
  padding: var(--space-3xs) var(--space-xs);
  border-radius: var(--radius-control);
  background: color-mix(in srgb, var(--color-tint-merit) 30%, var(--color-surface));
  color: var(--color-ink);
  font-size: var(--text-xs);
  line-height: 1.5;
}

.route-stop__nav {
  display: inline-flex;
  min-height: var(--size-control);
  margin-block-start: var(--space-3xs);
  gap: var(--space-2xs);
  align-items: center;
  justify-self: start;
  color: var(--color-primary);
  font-size: var(--text-sm);
  font-weight: 600;
  text-decoration: none;
}

.route-stop__nav .icon {
  width: 1.05rem;
  height: 1.05rem;
}

.route-stop__badge--end {
  background: var(--color-surface);
  box-shadow: inset 0 0 0 2px var(--color-primary);
  color: var(--color-primary);
}

.route-stop__badge--end .icon {
  width: 1.2rem;
  height: 1.2rem;
}

.route-end__text {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: var(--lh-title);
}

.route-leg {
  display: grid;
  padding-inline-start: 0.75rem;
  padding-block-start: var(--space-xs);
  gap: var(--space-3xs);
  grid-column: 2;
}

/* Measured distance carries the weight; the derived minute count stays light and
 * says ราว, so the two are never mistaken for one another. */
.route-leg__label {
  display: inline-flex;
  min-height: 2rem;
  padding: var(--space-3xs) var(--space-sm);
  gap: var(--space-2xs);
  align-items: center;
  justify-self: start;
  border: var(--rule-hair) solid var(--color-border);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
}

.route-leg__label .icon {
  width: 1.05rem;
  height: 1.05rem;
  color: var(--color-ink-muted);
}

.route-leg__dist {
  font-size: var(--text-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.route-leg__time {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.route-leg__note {
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
  line-height: 1.5;
}

.route-suggest-note {
  padding-inline: var(--space-gutter);
  padding-block-start: var(--space-sm);
  color: var(--color-ink-muted);
  /* Instruction, not disclosure: it says what the นำทาง button will do. The notes floor
     is reserved for the "ค่าตัวอย่างสำหรับต้นแบบ" lines. */
  font-size: var(--text-sm);
  line-height: 1.5;
}

/* ── The plate: geography as a drawing, and it says so ── */
.route-map {
  display: grid;
  padding-block: var(--space-section) var(--space-xs);
  gap: var(--space-xs);
}

.route-plate {
  display: grid;
  margin-inline: var(--space-gutter);
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
  color: inherit;
  text-decoration: none;
}

.route-plate__drawing {
  display: block;
  background: color-mix(in srgb, var(--color-tint-chill) 34%, var(--color-surface));
}

.route-plate__drawing svg {
  display: block;
  width: 100%;
  height: auto;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.route-plate__drawing .mm-path {
  stroke: var(--color-primary);
  stroke-width: 2.4;
}

.mm-pin-number {
  fill: var(--color-ink);
  stroke: none;
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 600;
  text-anchor: middle;
}

/* ── ก่อนออกเดิน: the practical strip, kept out of the timeline ── */
.route-brief {
  padding-block: var(--space-section) var(--space-xs);
}

.route-brief .setting-sub {
  white-space: normal;
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.route-others {
  padding-block: var(--space-section) var(--space-xs);
  background: var(--color-canvas);
}

.route-card .place-body {
  gap: 0.2rem;
}

/* Scoped to route cards on purpose: me.html's saved hero card centres its body against the
 * photo and must keep doing so. Here a shorter card was sharing its slack between rows, so
 * two adjacent cards' chip rows sat 10 px apart. */
.route-card .place-link {
  align-content: start;
}

.route-card .place-name {
  white-space: normal;
  font-size: var(--text-sm);
  line-height: var(--lh-title);
}

.route-card .place-meta {
  display: block;
  font-size: var(--text-sm);
  line-height: 1.45;
  white-space: normal;
}

.route-card .chip--theme,
.route-card .chip--span {
  /* Both, not one: the span chip defaulted to stretch inside the card grid and rendered as a
     full-width outlined bar under a small pill, on all four lane hubs. */
  justify-self: start;
}

/* Easy mode: the decoration goes, the numbers grow, the rail stays. */
html[data-easy="on"] .route-stop__line,
html[data-easy="on"] .route-leg__time,
html[data-easy="on"] .route-leg__dist {
  font-size: var(--text-base);
}

html[data-easy="on"] .route-stop__tip,
html[data-easy="on"] .route-meta,
html[data-easy="on"] .route-ribbon__name,
html[data-easy="on"] .route-start-note,
html[data-easy="on"] .route-stat dt,
html[data-easy="on"] .route-mode-total,
html[data-easy="on"] .route-suggest-note,
html[data-easy="on"] .route-leg__note,
html[data-easy="on"] .route-card .place-meta {
  font-size: var(--text-sm);
}

/* Entrance: the timeline assembles itself once, inside the 500 ms stagger ceiling. */
@media (prefers-reduced-motion: no-preference) {
  html:not([data-entrance="off"]) .route-stats,
  html:not([data-entrance="off"]) .route-ribbon {
    animation: enter-fade-up 420ms var(--ease-out) 160ms backwards;
  }

  html:not([data-entrance="off"]) .route-stop {
    animation: enter-fade-up 380ms var(--ease-out) backwards;
  }

  html:not([data-entrance="off"]) .route-stop:nth-child(1) { animation-delay: 220ms; }
  html:not([data-entrance="off"]) .route-stop:nth-child(2) { animation-delay: 260ms; }
  html:not([data-entrance="off"]) .route-stop:nth-child(3) { animation-delay: 300ms; }
  html:not([data-entrance="off"]) .route-stop:nth-child(4) { animation-delay: 340ms; }
  html:not([data-entrance="off"]) .route-stop:nth-child(5) { animation-delay: 380ms; }
  html:not([data-entrance="off"]) .route-stop:nth-child(n + 6) { animation-delay: 420ms; }
  /* The terminus is the last thing to arrive. */
  html:not([data-entrance="off"]) .route-timeline__end {
    animation: enter-fade-up 380ms var(--ease-out) 460ms backwards;
  }
}


/* ═══ ข่าวสาร — the municipal feed and the reading surface (SLICE-03) ════════
 * The feed rhymes with /home's news block on purpose: the same kicker, the same
 * headline weight, the same row. What this screen adds is the notice slot above the
 * feed, the category filter, and a reading surface built for four paragraphs of
 * municipal Thai rather than for a caption. */

/* ── The notice: formal, alert-toned, and always dated ── */
/* The display rule outranks the UA [hidden] rule, so the expiry path needs its own —
 * without it, finished roadworks stayed on screen and the feed printed the story twice. */
.notice-card[hidden] {
  display: none;
}

.notice-card {
  display: grid;
  margin-inline: var(--space-gutter);
  margin-block-start: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  gap: var(--space-3xs);
  border-inline-start: 4px solid var(--color-danger);
  border-radius: var(--radius-tile);
  background: color-mix(in srgb, var(--color-danger) 7%, var(--color-surface));
}

.notice-kicker {
  display: flex;
  gap: var(--space-2xs);
  align-items: center;
  color: var(--color-danger);
  font-size: var(--text-2xs);
  font-weight: 600;
}

.notice-kicker .icon {
  width: 1rem;
  height: 1rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.notice-title {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
  line-height: var(--lh-title);
}

.notice-line {
  color: var(--color-ink);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.notice-window {
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
  font-weight: 600;
}

.notice-link {
  display: inline-flex;
  min-height: var(--size-control);
  gap: var(--space-2xs);
  align-items: center;
  justify-self: start;
  color: var(--color-primary);
  font-size: var(--text-sm);
  font-weight: 600;
  text-decoration: none;
}

.notice-link .icon {
  width: 1.05rem;
  height: 1.05rem;
}

html[data-input="key"] .notice-link:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
  border-radius: var(--radius-control);
}

/* ── The feed ── */
.news-filter {
  padding-block-end: var(--space-xs);
}

.news-panel {
  position: relative;
  padding-block: var(--space-section) var(--space-xs);
  background: var(--color-canvas);
}

.news-feed {
  display: grid;
  margin: 0;
  padding-inline: var(--space-gutter);
  gap: var(--space-xs);
  list-style: none;
}

.news-feed[hidden] {
  display: none;
}

.news-row .news-item {
  display: grid;
  padding: var(--space-sm) var(--space-md);
  gap: var(--space-3xs);
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
  color: inherit;
  text-decoration: none;
}

.news-row--lead .news-lead {
  display: grid;
  overflow: hidden;
  gap: 0;
  border-radius: var(--radius-card);
  background: var(--color-surface);
  box-shadow: var(--shadow-card);
  color: inherit;
  text-decoration: none;
}

.news-row--lead .news-photo {
  display: block;
  aspect-ratio: 16 / 10;
  overflow: hidden;
}

.news-row--lead .news-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.news-row--lead .news-body {
  display: grid;
  padding: var(--space-sm) var(--space-md) var(--space-md);
  gap: var(--space-3xs);
}

.news-row .news-kicker {
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
  font-weight: 600;
}

.news-row .news-headline {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
  line-height: var(--lh-title);
  letter-spacing: 0;
}

.news-row--lead .news-headline {
  font-size: var(--text-md);
}

.news-summary {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.news-empty {
  display: grid;
  margin-inline: var(--space-gutter);
  padding: var(--space-lg) var(--space-md);
  gap: var(--space-2xs);
  justify-items: center;
  border: var(--rule-hair) dashed var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  text-align: center;
}

.news-empty[hidden] {
  display: none;
}

.news-empty__title {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
  line-height: var(--lh-title);
}

.news-empty__body {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.news-empty .ghost-button {
  margin-block-start: var(--space-xs);
}

.news-official {
  justify-content: center;
  padding-block-start: var(--space-md);
}

/* ── The reading surface ── */
.article-head {
  display: grid;
  padding: var(--space-sm) var(--space-gutter) 0;
  gap: var(--space-2xs);
}

.article-kicker {
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
  font-weight: 600;
}

.article-head :is(h1, h2) {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 7vw, 1.75rem);
  font-weight: 700;
  line-height: var(--lh-display);
  letter-spacing: 0;
}

.article-summary {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

/* Documentary photography sits in a plain card: the blob mask belongs to the
 * editorial screens, not to a works notice. */
.article-photo {
  margin: var(--space-md) var(--space-gutter) var(--space-md);
  overflow: hidden;
  aspect-ratio: 16 / 10;
  border-radius: var(--radius-card);
}

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

.article-body {
  display: grid;
  max-width: 34rem;
  padding-inline: var(--space-gutter);
  padding-block: var(--space-section) 0;
  gap: var(--space-sm);
}

.article-body p {
  font-size: var(--text-base);
  line-height: 1.75;
}

.article-facts {
  padding-inline: var(--space-gutter);
  padding-block: var(--space-section) 0;
}

.fact-sheet {
  display: grid;
  margin: 0;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.fact-row {
  display: grid;
  padding: var(--space-sm) var(--space-md);
  gap: var(--space-3xs);
  grid-template-columns: minmax(6rem, 32%) minmax(0, 1fr);
  align-items: baseline;
  border-block-end: var(--rule-hair) solid var(--color-border);
}

.fact-row:last-child {
  border-block-end: 0;
}

.fact-row dt {
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
  font-weight: 600;
}

.fact-row dd {
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.article-actions {
  display: grid;
  padding-inline: var(--space-gutter);
  padding-block: var(--space-section) 0;
  gap: var(--space-sm);
  justify-items: center;
}

.article-actions .ghost-button {
  display: inline-flex;
  gap: var(--space-2xs);
  align-items: center;
}

.article-official {
  justify-content: center;
}

.article-related {
  padding-block: var(--space-section) var(--space-xs);
}

.article-related .news-item {
  display: grid;
  padding: var(--space-sm) var(--space-md);
  gap: var(--space-3xs);
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
  color: inherit;
  text-decoration: none;
}

.article-related .news-list {
  display: grid;
  margin: 0;
  padding-inline: var(--space-gutter);
  gap: var(--space-xs);
  list-style: none;
}

.article-related .news-kicker {
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
  font-weight: 600;
}

.article-related .news-headline {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
  line-height: var(--lh-title);
}

/* Easy mode: the small municipal print grows with everything else. */
html[data-easy="on"] .news-summary,
html[data-easy="on"] .news-row .news-kicker,
html[data-easy="on"] .article-kicker,
html[data-easy="on"] .fact-row dt,
html[data-easy="on"] .notice-window {
  font-size: var(--text-sm);
}

/* Entrance: the same one-shot assembly the other screens use. */
@media (prefers-reduced-motion: no-preference) {
  html:not([data-entrance="off"]) .notice-card {
    animation: enter-fade-down 380ms var(--ease-out) backwards;
  }

  html:not([data-entrance="off"]) .news-row {
    animation: enter-fade-up 380ms var(--ease-out) backwards;
  }

  html:not([data-entrance="off"]) .news-row:nth-child(1) { animation-delay: 200ms; }
  html:not([data-entrance="off"]) .news-row:nth-child(2) { animation-delay: 250ms; }
  html:not([data-entrance="off"]) .news-row:nth-child(3) { animation-delay: 300ms; }
  html:not([data-entrance="off"]) .news-row:nth-child(4) { animation-delay: 350ms; }
  html:not([data-entrance="off"]) .news-row:nth-child(n + 5) { animation-delay: 400ms; }

  html:not([data-entrance="off"]) .article-head,
  html:not([data-entrance="off"]) .article-photo {
    animation: enter-fade-up 420ms var(--ease-out) 120ms backwards;
  }
}


/* ═══ เช็กอินสำเร็จ — the payoff moment (SLICE-04) ═══════════════════════════
 * The catalog calls this the emotional peak and the one screen allowed the product's
 * single exclamation mark. The stamp lands once, with the sunburst behind it, and then
 * everything on the page is still. */
.payoff {
  display: grid;
  padding-inline: var(--space-gutter);
  padding-block: var(--space-lg) var(--space-md);
  gap: var(--space-xs);
  justify-items: center;
  text-align: center;
  background: var(--color-canvas);
}

.payoff-stamp {
  position: relative;
  display: grid;
  width: 8rem;
  height: 8rem;
  place-items: center;
}

.payoff-disc {
  width: 6rem;
  height: 6rem;
  rotate: -8deg;
  fill: none;
}

.payoff-disc .sb-ring {
  stroke: var(--color-primary);
  stroke-width: 2.4;
}

.payoff-disc .sb-ring--inner {
  stroke: var(--color-primary-bright);
  stroke-dasharray: 3 5;
  stroke-width: 1.6;
}

.payoff-disc .sb-art {
  fill: none;
  stroke: var(--color-primary);
  stroke-width: 1.6;
}

.payoff-emblem {
  position: absolute;
  z-index: 1;
  width: 6rem;
  height: 6rem;
  object-fit: contain;
  rotate: -8deg;
}

.payoff-stamp[data-emblem] .payoff-disc .sb-art {
  display: none;
}

.payoff-title {
  font-family: var(--font-display);
  font-size: clamp(1.625rem, 8vw, 2rem);
  font-weight: 700;
  line-height: var(--lh-hero);
  letter-spacing: 0;
}

.payoff-place {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 600;
  line-height: var(--lh-title);
}

/* The receipt is a balance: warm surface, plain wording, no celebration. */
.payoff-receipt {
  padding: var(--space-2xs) var(--space-md);
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--color-tint-merit) 45%, var(--color-surface));
  font-size: var(--text-base);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.payoff-actions {
  display: grid;
  width: 100%;
  margin-block-start: var(--space-sm);
  gap: var(--space-xs);
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) auto;
  align-items: stretch;
}

.payoff-actions .ghost-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}

/* The reward is also learning the place, so the story sits right under the number. */
.payoff-story {
  display: grid;
  padding-inline: var(--space-gutter);
  padding-block: var(--space-section) var(--space-xs);
  gap: var(--space-xs);
  justify-items: start;
}

.payoff-photo {
  width: 100%;
  aspect-ratio: 4 / 2.6;
  overflow: hidden;
  clip-path: url(#place-hero-blob);
}

html[data-easy="on"] .payoff-photo,
html[data-season="royal"] .payoff-photo,
html[data-season="mourning"] .payoff-photo {
  clip-path: none;
  border-radius: var(--radius-card);
}

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

.payoff-story__title {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 700;
  line-height: var(--lh-title);
}

.payoff-story__line {
  font-size: var(--text-base);
  line-height: 1.7;
}

.payoff-story__link {
  margin-inline-start: calc(var(--space-xs) * -1);
}

/* The passport in one line: twenty-four slots, the newest one marked. */
.payoff-progress {
  display: grid;
  padding-block: var(--space-section) var(--space-lg);
  gap: var(--space-xs);
  background: var(--color-canvas);
}

.payoff-progress__line,
.payoff-progress__next {
  padding-inline: var(--space-gutter);
  font-size: var(--text-sm);
}

.payoff-progress__next {
  color: var(--color-ink-muted);
}

.payoff-strip {
  display: grid;
  margin: 0;
  padding-inline: var(--space-gutter);
  gap: var(--space-2xs);
  grid-template-columns: repeat(12, minmax(0, 1fr));
  list-style: none;
}

.payoff-slot {
  aspect-ratio: 1;
  border: var(--rule-hair) dashed var(--color-border);
  border-radius: 50%;
  background: var(--color-surface);
}

.payoff-slot.is-on {
  border-style: solid;
  border-color: var(--color-primary);
  background: color-mix(in srgb, var(--color-primary) 22%, var(--color-surface));
}

.payoff-slot.is-new {
  border-width: 2px;
  background: var(--color-primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary-bright) 30%, transparent);
}

@media (prefers-reduced-motion: no-preference) {
  html:not([data-entrance="off"]) .payoff-stamp {
    animation: enter-pop 460ms var(--ease-out) backwards;
    animation-timing-function: linear(0, 0.2, 0.62, 1.06 62%, 1.01 80%, 1);
  }

  html:not([data-entrance="off"]) .payoff-title,
  html:not([data-entrance="off"]) .payoff-place,
  html:not([data-entrance="off"]) .payoff-receipt {
    animation: enter-fade-up 380ms var(--ease-out) 220ms backwards;
  }

  html:not([data-entrance="off"]) .payoff-actions,
  html:not([data-entrance="off"]) .payoff-strip {
    animation: enter-fade-up 420ms var(--ease-out) 340ms backwards;
  }
}

/* ═══ เหตุฉุกเฉิน — standalone safety utility (UX-01 V3) ═════════════════════
 * Motion is off only on this document. Browse screens keep their approved entrances,
 * pagers and ambient loops. Immediate :active colour feedback remains. */
html.sos-page,
html.sos-page body {
  scroll-padding-block-end: calc(var(--space-md) + env(safe-area-inset-bottom, 0px));
}

html.sos-page *,
html.sos-page *::before,
html.sos-page *::after {
  animation: none !important;
  scroll-behavior: auto !important;
  transition: none !important;
}

.app-shell--sos,
html[data-easy="on"] .app-shell--sos {
  padding-block-end: max(var(--space-lg), env(safe-area-inset-bottom, 0px));
  background: var(--color-canvas);
}

.header-spacer {
  width: var(--size-control);
  height: var(--size-control);
}

.sos-header {
  padding-inline-start: max(var(--space-gutter), env(safe-area-inset-left, 0px));
  padding-inline-end: max(var(--space-gutter), env(safe-area-inset-right, 0px));
}

.sos-title {
  min-width: 0;
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: var(--text-md);
  font-weight: 700;
  line-height: var(--lh-title);
  text-align: center;
}

.sos-intro,
.sos-calls,
.sos-position,
.sos-municipality,
.sos-guidance,
.sos-trust {
  padding-inline-start: max(var(--space-gutter), env(safe-area-inset-left, 0px));
  padding-inline-end: max(var(--space-gutter), env(safe-area-inset-right, 0px));
}

.sos-intro {
  padding-block: var(--space-xs) var(--space-sm);
  color: var(--color-ink);
  font-size: var(--text-base);
  font-weight: 600;
}

.sos-call-list {
  display: grid;
  margin: 0;
  padding: 0;
  gap: var(--space-xs);
  list-style: none;
}

.sos-call-list > li {
  min-width: 0;
}

.sos-call {
  display: grid;
  min-height: 4.5rem;
  padding: var(--space-sm) var(--space-md);
  gap: var(--space-sm);
  grid-template-columns: 2.75rem minmax(0, 1fr) 1.5rem;
  align-items: center;
  border-radius: var(--radius-tile);
  color: var(--color-on-primary);
  text-decoration: none;
}

.sos-call--danger {
  background: var(--color-danger);
  box-shadow: var(--shadow-control);
}

.sos-call--danger:active {
  background: #761823;
}

/* 1669 answers the first glance, without making 191 or 199 look optional. */
.sos-call--primary {
  min-height: 5rem;
  padding-block: var(--space-md);
}

.sos-call--primary .sos-call__copy strong {
  font-size: var(--text-lg);
}

.sos-call--primary .sos-call__mark {
  width: 3rem;
  height: 3rem;
}

.sos-call--city {
  border: var(--rule-card) solid var(--color-ink);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
  color: var(--color-ink);
}

.sos-call--city:active {
  background: var(--color-surface-sunken);
}

.sos-call--city .sos-call__mark {
  background: var(--color-ink);
  color: var(--color-surface);
}

.sos-call__mark {
  display: grid;
  width: 2.75rem;
  height: 2.75rem;
  place-items: center;
  border-radius: 50%;
  background: color-mix(in srgb, var(--color-surface) 24%, transparent);
}

.sos-call__mark .icon,
.sos-call__phone {
  width: 1.5rem;
  height: 1.5rem;
  color: currentColor;
}

.sos-call__copy {
  display: grid;
  min-width: 0;
  gap: var(--space-3xs);
}

.sos-call__copy strong {
  font-size: max(var(--text-md), 1.375rem);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: var(--lh-title);
  overflow-wrap: anywhere;
}

.sos-call__copy span {
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: var(--lh-title);
}

html[data-input="key"] .sos-call:focus-visible {
  outline: 3px solid var(--color-ink);
  outline-offset: 3px;
}

.sos-position,
.sos-guidance {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  min-width: 0;
  padding-block-start: var(--space-section);
  gap: var(--space-xs);
}

.sos-position h2,
.sos-guidance h2 {
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 700;
}

.sos-position__card {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  min-width: 0;
  padding: var(--space-md);
  gap: var(--space-sm);
  border: var(--rule-card) solid var(--color-border);
  border-radius: var(--radius-tile);
  background: var(--color-surface);
}

.sos-position__card[data-state="fresh"] {
  border-color: color-mix(in srgb, var(--color-success) 45%, var(--color-border));
}

.sos-position__card[data-state="denied"],
.sos-position__card[data-state="error"],
.sos-position__card[data-state="timeout"],
.sos-position__card[data-state="unverified"],
.sos-position__card[data-state="stale"],
.sos-position__card[data-state="low-accuracy"] {
  border-color: color-mix(in srgb, var(--color-danger) 42%, var(--color-border));
}

.sos-location-state {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  min-width: 0;
  gap: var(--space-sm);
}

.sos-location-state[hidden] {
  display: none;
}

.sos-location-result {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  min-width: 0;
  gap: var(--space-sm);
}

.sos-location-result[hidden] {
  display: none;
}

.sos-location-state p {
  margin: 0;
}

.sos-position__card .sos-action {
  border-radius: var(--radius-control);
}

html[data-input="key"] .sos-location-state:focus-visible {
  outline: 3px solid var(--color-ink);
  outline-offset: 4px;
  border-radius: var(--radius-control);
}

.sos-state-heading {
  display: grid;
  min-width: 0;
  gap: var(--space-sm);
  grid-template-columns: min(3rem, 12vw) minmax(0, 1fr);
  align-items: center;
}

.sos-state-heading > div {
  display: grid;
  min-width: 0;
  gap: var(--space-3xs);
}

.sos-state-heading p:not(.sos-state-title, .sos-coords, .sos-location-meta) {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.sos-state-icon {
  display: grid;
  width: min(3rem, 12vw);
  height: min(3rem, 12vw);
  place-items: center;
  border-radius: 50%;
  background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface));
  color: var(--color-primary);
}

.sos-state-icon .icon {
  width: min(1.65rem, 7vw);
  height: min(1.65rem, 7vw);
}

.sos-state-icon--success {
  background: color-mix(in srgb, var(--color-success) 14%, var(--color-surface));
  color: var(--color-success);
}

.sos-state-icon--danger {
  background: color-mix(in srgb, var(--color-danger) 12%, var(--color-surface));
  color: var(--color-danger);
}

.sos-state-icon--warning {
  background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface));
  color: var(--color-primary);
}

.sos-state-title {
  color: var(--color-ink);
  font-size: var(--text-base);
  font-weight: 700;
  line-height: var(--lh-title);
}

.sos-network {
  margin: 0;
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-control);
  background: color-mix(in srgb, var(--color-primary) 10%, var(--color-surface));
  color: var(--color-ink);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: var(--lh-body);
}

.sos-noscript {
  margin: 0;
  padding: var(--space-sm);
  border-radius: var(--radius-control);
  background: color-mix(in srgb, var(--color-danger) 10%, var(--color-surface));
  color: var(--color-ink);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: var(--lh-body);
}

.sos-coords {
  color: var(--color-ink);
  font-size: var(--text-lg);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: var(--lh-title);
  overflow-wrap: anywhere;
  user-select: text;
}

.sos-location-result .sos-location-meta {
  color: var(--color-ink);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.sos-location-advice,
.sos-privacy {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.sos-location-actions {
  display: grid;
  gap: var(--space-xs);
}

.sos-action {
  min-height: 3.25rem;
  background: var(--color-ink);
}

.sos-action:active {
  background: #092f2f;
}

.sos-share {
  display: flex;
  min-height: 3.25rem;
  gap: var(--space-xs);
  align-items: center;
  justify-content: center;
  color: var(--color-ink);
  font-size: var(--text-base);
  font-weight: 600;
}

.sos-share .icon {
  width: 1.2rem;
  height: 1.2rem;
}

.sos-retry-link {
  min-height: var(--size-control);
  padding: 0;
  justify-self: start;
  border: 0;
  background: none;
  color: var(--color-ink);
  font-size: var(--text-sm);
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

.sos-permission-help {
  border-radius: var(--radius-control);
  background: var(--color-surface-sunken);
}

.sos-permission-help summary {
  min-height: var(--size-control);
  padding: var(--space-xs) var(--space-sm);
  color: var(--color-ink);
  font-weight: 700;
  cursor: pointer;
}

.sos-permission-help p {
  padding: 0 var(--space-sm) var(--space-sm);
  color: var(--color-ink);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.sos-municipality {
  padding-block-start: var(--space-section);
}

.sos-copy-state {
  min-height: 1.5em;
  color: var(--color-ink);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: var(--lh-body);
}

.sos-copy-state[data-tone="success"] {
  color: var(--color-success);
}

.sos-copy-state[data-tone="danger"] {
  color: var(--color-danger);
}

.sos-steps {
  display: grid;
  margin: 0;
  padding-inline-start: 1.4rem;
  gap: var(--space-xs);
  font-size: var(--text-base);
  line-height: var(--lh-body);
}

.sos-trust {
  margin-block: var(--space-lg) 0;
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
  text-align: center;
}

html[data-easy="on"] .sos-call {
  min-height: 5rem;
}

/* At 200% text the word beside Home can consume the whole three-column header. Easy mode
 * keeps the verified icon + accessible name and reserves a real column for the SOS h1. */
html[data-easy="on"] .sos-header {
  grid-template-columns: 56px minmax(0, 1fr) 56px;
}

html[data-easy="on"] .sos-header .navigation-exit {
  inline-size: 56px;
  padding-inline: 0;
}

html[data-easy="on"] .sos-header .navigation-exit > span {
  display: none;
}

html[data-easy="on"] .sos-call__copy span,
html[data-easy="on"] .sos-location-meta,
html[data-easy="on"] .sos-location-advice,
html[data-easy="on"] .sos-privacy,
html[data-easy="on"] .sos-network,
html[data-easy="on"] .sos-noscript,
html[data-easy="on"] .sos-permission-help p,
html[data-easy="on"] .sos-trust {
  font-size: var(--text-base);
}

/* Focused public details keep one stress-readable route into the standalone SOS utility. */
a.icon-button.sos-shortcut {
  background: var(--color-danger);
  color: var(--color-on-primary);
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-decoration: none;
}

a.icon-button.sos-shortcut:active {
  background: #761823;
}

.app-header--sos-link .wordmark--compact {
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
}

/* Keep the compact mark short so it cannot collide with two safety controls at 200% text. */
.app-header--sos-link .wordmark__city {
  display: none;
}


/* ═══ ของรางวัล · คูปอง · การแจ้งเตือน · อากาศ · อันดับ (SLICES 06 / 10 / 11 / 12) ══
 * Four value-and-service surfaces. The catalog marks all of them motif: none — a pen
 * doodle beside a balance reads as cheap — so they share the tokens and nothing else:
 * corner-radius cards, plain register, and numbers that behave like numbers. */

/* ── Rewards ── */
/* One column at phone width: a 252 px action beside the title left 94 px for it, and
 * "การแจ้งเตือน" — one Thai word — broke in half. The action keeps its own line. */
.page-head--row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-sm);
  align-items: start;
  justify-items: stretch;
}

@media (min-width: 26.875rem) {
  .page-head--row {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
  }
}

.page-head--row .page-head__copy {
  display: grid;
  gap: var(--space-3xs);
}

.balance-card {
  display: grid;
  margin-inline: var(--space-gutter);
  margin-block-start: var(--space-sm);
  padding: var(--space-md);
  gap: var(--space-3xs);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.balance-label {
  color: var(--color-ink-muted);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 600;
}

.balance-value {
  font-size: clamp(1.75rem, 8vw, 2.25rem);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: var(--lh-display);
}

.balance-note {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.balance-link {
  display: inline-flex;
  min-height: var(--size-control);
  gap: var(--space-2xs);
  align-items: center;
  justify-self: start;
  color: var(--color-primary);
  font-size: var(--text-sm);
  font-weight: 600;
  text-decoration: none;
}

.balance-link .icon {
  width: 1.05rem;
  height: 1.05rem;
}

.rewards-panel {
  padding-block: var(--space-section) var(--space-xs);
  background: var(--color-canvas);
}

.reward-list {
  display: grid;
  margin: 0;
  padding-inline: var(--space-gutter);
  gap: var(--space-xs);
  list-style: none;
}

/* One row per offer: the photograph, the facts, and a button sized to its word. Five
 * full-width clay buttons stacked would shout on a surface that has to read as a balance. */
/* Easy mode grows the meta line but the row kept three fixed columns, so the middle one
 * fell to ~150 px and the price line broke into four ragged rows. The action takes its own
 * row instead, the way the low-balance variant below already restacks. */
html[data-easy="on"] .reward-item {
  grid-template-columns: 4rem minmax(0, 1fr);
  grid-template-areas: "photo body" "cta cta";
  align-items: start;
}

html[data-easy="on"] .reward-cta {
  inline-size: 100%;
}

.reward-list {
  container-type: inline-size;
}

/* A narrow card — a small phone, or a large user text size (rem grows, the card does not) —
 * puts the call to action under the copy instead of past the edge. */
@container (max-width: 21rem) {
  .reward-list .reward-item {
    grid-template-columns: min(4rem, 20vw) minmax(0, 1fr);
    grid-template-areas: "photo body" "cta cta";
  }
}

.reward-item .reward-cta {
  min-width: 0;
  white-space: normal;
}

.reward-item {
  display: grid;
  padding: var(--space-xs);
  gap: var(--space-xs);
  grid-template-columns: min(4rem, 20vw) minmax(0, 1fr) minmax(0, auto);
  grid-template-areas: "photo body cta";
  align-items: center;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.reward-photo {
  grid-area: photo;
  display: block;
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: var(--radius-control);
  background: var(--color-surface-sunken);
}

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

.reward-body {
  grid-area: body;
  display: grid;
  min-width: 0;
  gap: var(--space-3xs);
}

.reward-name {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
  line-height: var(--lh-title);
}

.reward-shop {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.reward-meta {
  font-size: var(--text-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.reward-cta {
  grid-area: cta;
  min-width: 4.5rem;
  min-height: var(--size-control);
  padding-inline: var(--space-md);
  font-size: var(--text-sm);
}

/* When the balance is short the button becomes a quiet sentence, so it wraps under the
 * facts rather than squeezing them. */
.reward-item:has(.ghost-button.reward-cta) {
  grid-template-columns: 4rem minmax(0, 1fr);
  grid-template-areas: "photo body" "cta cta";
}

.rewards-note {
  padding-inline: var(--space-gutter);
  padding-block-start: var(--space-sm);
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

/* The confirmation is the moment of consequence: formal, explicit, and it names the
 * number it is about to take. */
.confirm-sheet,
.ticket-sheet {
  position: fixed;
  inset: 0;
  z-index: var(--z-sticky);
  display: grid;
  padding-block-start: max(var(--space-md), env(safe-area-inset-top, 0px));
  padding-block-end: max(var(--space-md), env(safe-area-inset-bottom, 0px));
  padding-inline-start: max(var(--space-md), env(safe-area-inset-left, 0px));
  padding-inline-end: max(var(--space-md), env(safe-area-inset-right, 0px));
  overflow-y: auto;
  overscroll-behavior: contain;
  justify-items: center;
  align-content: safe end;
  background: rgb(14 71 71 / 38%);
}

.confirm-sheet[hidden],
.ticket-sheet[hidden] {
  display: none;
}

.confirm-card,
.ticket-sheet__card {
  display: grid;
  width: min(100%, 28rem);
  padding: var(--space-lg) var(--space-md) var(--space-md);
  gap: var(--space-xs);
  justify-items: center;
  border-radius: var(--radius-sheet);
  background: var(--color-surface);
  box-shadow: var(--shadow-float);
  text-align: center;
}

.confirm-card :is(h2, h3),
.ticket-sheet__card :is(h2, h3) {
  font-family: var(--font-body);
  font-size: var(--text-md);
  font-weight: 700;
  line-height: var(--lh-title);
}

.confirm-offer,
.ticket-sheet__shop {
  color: var(--color-ink);
  font-size: var(--text-sm);
}

.confirm-cost {
  font-size: var(--text-base);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.confirm-warning {
  color: var(--color-danger);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.confirm-actions {
  display: grid;
  width: 100%;
  margin-block-start: var(--space-xs);
  gap: var(--space-xs);
}

.confirm-actions .ghost-button {
  min-height: var(--size-control);
}

.redeem-result {
  display: grid;
  margin-inline: var(--space-gutter);
  margin-block-start: var(--space-md);
  padding: var(--space-md);
  gap: var(--space-xs);
  justify-items: center;
  border-radius: var(--radius-tile);
  background: color-mix(in srgb, var(--color-success) 10%, var(--color-surface));
  text-align: center;
}

.redeem-result[hidden] {
  display: none;
}

.redeem-result__line {
  color: var(--color-success);
  font-size: var(--text-base);
  font-weight: 600;
}

/* ── The wallet ── */
.wallet {
  padding-block: var(--space-section) var(--space-xs);
  background: var(--color-canvas);
}

.ticket-list {
  display: grid;
  margin: 0;
  padding-inline: var(--space-gutter);
  gap: var(--space-md);
  list-style: none;
}

.ticket-list[hidden] {
  display: none;
}

/* A ticket is a document, so it keeps square corners on its code block and takes its
 * perforation from the layout rather than from a decorative device. */
.ticket {
  display: grid;
  padding: var(--space-md);
  gap: var(--space-sm);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  box-shadow: var(--shadow-card);
}

.ticket--used {
  background: var(--color-surface-sunken);
  color: var(--color-ink-muted);
}

/* The pill sat on the surface it was painted in — 1:1 — so all that reached the screen was its
 * inline padding, which read as an indented sentence rather than a state badge. Every other chip
 * in this file carries a ground or an outline; this one takes the outline. */
.ticket__state {
  justify-self: start;
  background: var(--color-surface);
  border: var(--rule-hair) solid var(--color-border);
  color: var(--color-ink-muted);
}

.redeem-result--failed {
  background: color-mix(in srgb, var(--color-danger) 9%, var(--color-surface));
}

.redeem-result--failed .redeem-result__line {
  color: var(--color-danger);
}

.rank-privacy .link-more {
  display: inline-flex;
  min-height: var(--size-control);
  align-items: center;
}

.ticket__head {
  display: grid;
  gap: var(--space-3xs);
}

.ticket__name {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 700;
  line-height: var(--lh-title);
}

.ticket__shop {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.ticket__code {
  display: grid;
  padding-block: var(--space-sm);
  gap: var(--space-xs);
  justify-items: center;
  border-block: var(--rule-hair) dashed var(--color-border);
}

.ticket__qr {
  width: 7.5rem;
  height: 7.5rem;
  border-radius: var(--radius-control);
  overflow: hidden;
  background: var(--color-surface);
}

.ticket__qr svg {
  display: block;
  width: 100%;
  height: 100%;
}

.ticket__qr--large {
  display: grid;
  width: 11rem;
  height: 11rem;
  place-items: center;
  border: 1px dashed var(--color-border);
  /* A visible sample face — diagonal hatching and its own word — never a blank square. */
  background: repeating-linear-gradient(-45deg, var(--color-surface) 0 10px,
    color-mix(in srgb, var(--color-primary) 12%, var(--color-surface)) 10px 20px);
}

.ticket__qr--large::after {
  padding: 0.3rem 0.7rem;
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  font-weight: 600;
  content: none;
}

.ticket__code-text {
  font-size: var(--text-base);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
}

.ticket__expiry {
  font-size: var(--text-sm);
  font-weight: 600;
}

.ticket--used .ticket__qr {
  opacity: 0.35;
}

.wallet-empty {
  display: grid;
  margin-inline: var(--space-gutter);
  padding: var(--space-lg) var(--space-md);
  gap: var(--space-2xs);
  justify-items: center;
  border: var(--rule-hair) dashed var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  text-align: center;
}

.wallet-empty[hidden] {
  display: none;
}

.wallet-empty__title {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
}

.wallet-empty__body {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.ticket-sheet__code {
  font-size: var(--text-md);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
}

.ticket-sheet__note {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

/* ── The inbox ── */
.notif-panel {
  padding-block: var(--space-section) var(--space-xs);
  background: var(--color-canvas);
}

.notif-day {
  padding-inline: var(--space-gutter);
  padding-block: var(--space-sm) var(--space-2xs);
  color: var(--color-ink-muted);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 600;
}

.notif-list {
  display: grid;
  margin: 0;
  padding-inline: var(--space-gutter);
  gap: var(--space-2xs);
  list-style: none;
}

.notif-link {
  display: grid;
  padding: var(--space-sm) var(--space-md);
  gap: var(--space-sm);
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: start;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
  color: inherit;
  text-decoration: none;
}

.notif-item.is-unread .notif-link {
  border-inline-start: 3px solid var(--color-primary);
}

.notif-unread-dot {
  width: 0.5rem;
  height: 0.5rem;
  margin-block-start: 0.4rem;
  border-radius: 50%;
  background: var(--color-primary);
}

.permission-note__actions {
  display: flex;
  gap: var(--space-xs);
  flex-wrap: wrap;
}

.sos-state-toggle {
  justify-self: start;
}

.notif-mark {
  padding: var(--space-3xs) var(--space-xs);
  border-radius: var(--radius-control);
  background: var(--color-surface-sunken);
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
  font-weight: 600;
}

.notif-mark--danger {
  background: color-mix(in srgb, var(--color-danger) 12%, var(--color-surface));
  color: var(--color-danger);
}

.notif-body {
  display: grid;
  min-width: 0;
  gap: var(--space-3xs);
}

.notif-title {
  font-size: var(--text-base);
  font-weight: 600;
  line-height: var(--lh-title);
}

.notif-item.is-unread .notif-title {
  font-weight: 700;
}

.notif-line {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.notif-time {
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}

.notif-empty,
.rank-empty {
  display: grid;
  margin-inline: var(--space-gutter);
  margin-block-start: var(--space-md);
  padding: var(--space-lg) var(--space-md);
  gap: var(--space-2xs);
  justify-items: center;
  border: var(--rule-hair) dashed var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  text-align: center;
}

.notif-empty[hidden],
.rank-empty[hidden] {
  display: none;
}

.notif-empty__title,
.rank-empty__title {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
}

.notif-empty__body {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.permission-note {
  display: grid;
  margin-inline: var(--space-gutter);
  margin-block-start: var(--space-lg);
  padding: var(--space-md);
  gap: var(--space-2xs);
  justify-items: start;
  border-radius: var(--radius-tile);
  background: var(--color-surface-sunken);
}

.permission-note__title {
  font-size: var(--text-base);
  font-weight: 600;
}

.permission-note__body {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

/* ── Weather ── */
.wx-now {
  display: grid;
  padding-inline: var(--space-gutter);
  padding-block: var(--space-sm) var(--space-md);
  gap: var(--space-3xs);
}

.wx-temp {
  font-family: var(--font-display);
  font-size: clamp(3rem, 16vw, 4rem);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.wx-condition {
  font-size: var(--text-base);
  font-weight: 600;
}

.wx-stamp,
.wx-stale {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.wx-stale {
  color: var(--color-danger);
  font-weight: 600;
}

.wx-stale[hidden] {
  display: none;
}

/* The offline state dims the values it can no longer vouch for. */
body.is-stale .wx-temp,
body.is-stale .wx-condition,
body.is-stale .wx-air__value,
body.is-stale .wx-day__temp,
body.is-stale .wx-day__name {
  color: var(--color-ink-muted);
}

.wx-air,
.wx-hourly,
.wx-forecast,
.wx-rows,
.wx-source {
  padding-block: var(--space-section) 0;
}

.wx-air__card {
  display: grid;
  margin-inline: var(--space-gutter);
  padding: var(--space-md);
  gap: var(--space-xs);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.wx-air__value {
  display: flex;
  gap: var(--space-xs);
  align-items: baseline;
  flex-wrap: wrap;
  font-size: var(--text-lg);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.wx-air__unit {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  font-weight: 400;
}

.wx-air__line {
  font-size: var(--text-base);
}

.wx-bands {
  display: grid;
  margin: 0;
  padding: 0;
  gap: var(--space-2xs);
  grid-template-columns: repeat(4, minmax(0, 1fr));
  list-style: none;
}

.wx-band {
  display: grid;
  padding: var(--space-2xs) var(--space-3xs);
  gap: 0.1rem;
  border-radius: var(--radius-control);
  background: var(--color-surface-sunken);
  text-align: center;
}

.wx-band__label {
  font-size: var(--text-xs);
  font-weight: 600;
}

.wx-band__range {
  color: var(--color-ink);
  font-size: var(--text-2xs);
  font-variant-numeric: tabular-nums;
}

.wx-band--good { background: color-mix(in srgb, var(--color-success) 16%, var(--color-surface)); }
.wx-band--fair { background: color-mix(in srgb, var(--color-tint-merit) 55%, var(--color-surface)); }
.wx-band--warn { background: color-mix(in srgb, var(--color-tint-food) 65%, var(--color-surface)); }
.wx-band--bad { background: color-mix(in srgb, var(--color-danger) 14%, var(--color-surface)); }

/* The current band is marked by a ring and by the sentence under the scale, never by
 * colour alone. */
.wx-band.is-now {
  outline: 2px solid var(--color-ink);
  outline-offset: 1px;
}

.wx-bands__note {
  font-size: var(--text-sm);
  font-weight: 600;
}

.wx-days {
  display: grid;
  margin: 0;
  padding-inline: var(--space-gutter);
  gap: var(--space-2xs);
  list-style: none;
}

.wx-day {
  display: grid;
  padding: var(--space-sm) var(--space-md);
  gap: var(--space-sm);
  grid-template-columns: minmax(4.5rem, auto) 1.5rem minmax(0, 1fr) auto;
  align-items: center;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.wx-day__name {
  font-size: var(--text-sm);
  font-weight: 600;
}

.wx-day__icon {
  width: 1.5rem;
  height: 1.5rem;
  color: var(--color-primary);
}

.wx-day__temp {
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}

.wx-day__pm {
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}

.wx-source {
  display: grid;
  padding-inline: var(--space-gutter);
  padding-block: var(--space-section) var(--space-lg);
  gap: var(--space-xs);
  justify-items: start;
}

.wx-source__line {
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
}

/* ── Ranking ── */
.rank-basis {
  padding-inline: var(--space-gutter);
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.rank-panel {
  padding-block: var(--space-section) var(--space-xs);
  background: var(--color-canvas);
}

.rank-list {
  display: grid;
  margin: 0;
  padding-inline: var(--space-gutter);
  gap: var(--space-2xs);
  list-style: none;
}

.rank-list[hidden] {
  display: none;
}

.rank-row {
  display: grid;
  padding: var(--space-sm) var(--space-md);
  gap: var(--space-sm);
  grid-template-columns: 1.75rem 2.25rem minmax(0, 1fr) auto;
  align-items: center;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

/* The top three are the point of a board, and a coloured numeral was the whole treatment —
   invisible beside the reader's own outlined row. A medal disc and a larger avatar make the
   head of the list readable at a glance. */
.rank-row--top {
  padding-block: var(--space-md);
  grid-template-columns: 2.25rem 2.75rem minmax(0, 1fr) auto;
}

.rank-row--top .rank-avatar {
  width: 2.75rem;
  height: 2.75rem;
  font-size: var(--text-base);
}

.rank-place--medal {
  display: grid;
  width: 2rem;
  height: 2rem;
  place-items: center;
  border-radius: 50%;
  box-shadow: inset 0 0 0 var(--rule-hair) var(--color-sticker-stroke);
  color: var(--color-ink);
  font-size: var(--text-sm);
}

.rank-place--medal[data-medal="1"] {
  background: var(--color-tint-merit);
}

.rank-place--medal[data-medal="2"] {
  background: var(--color-tint-chill);
}

.rank-place--medal[data-medal="3"] {
  background: var(--color-tint-food);
}

/* Where the reader stands, stated once, above the rows. */
.rank-standing {
  display: grid;
  margin: var(--space-md) var(--space-gutter) 0;
  padding: var(--space-md);
  gap: var(--space-2xs);
  border: var(--rule-card) solid var(--color-primary);
  border-radius: var(--radius-card);
  background: color-mix(in srgb, var(--color-tint-merit) 28%, var(--color-surface));
  box-shadow: var(--shadow-control);
}

.rank-standing__label {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.rank-standing__place {
  display: flex;
  gap: var(--space-xs);
  align-items: baseline;
  color: var(--color-primary);
  font-size: var(--text-lg);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: var(--lh-display);
}

.rank-standing__of {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  font-weight: 600;
}

.rank-standing__points {
  font-size: var(--text-base);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.rank-standing__gap {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.rank-row.is-me {
  border: var(--rule-card) solid var(--color-primary);
  background: color-mix(in srgb, var(--color-tint-merit) 28%, var(--color-surface));
}

.rank-place {
  font-size: var(--text-base);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.rank-row--top .rank-place {
  color: var(--color-primary);
  font-size: var(--text-md);
}

.rank-avatar {
  display: grid;
  width: 2.25rem;
  height: 2.25rem;
  place-items: center;
  border-radius: 50%;
  background: var(--color-surface-sunken);
  font-size: var(--text-sm);
  font-weight: 600;
}

.rank-name {
  display: flex;
  gap: var(--space-2xs);
  align-items: center;
  min-width: 0;
  font-size: var(--text-base);
  font-weight: 600;
}

.chip--me {
  background: var(--color-primary);
  color: var(--color-on-primary);
}

.rank-points {
  font-size: var(--text-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.rank-privacy {
  display: grid;
  padding-inline: var(--space-gutter);
  padding-block-start: var(--space-md);
  gap: var(--space-3xs);
  justify-items: start;
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

/* Easy mode: every small line on these five screens grows with the rest. */
/* Easy mode grows the value screens too: these lines sit at 13–14 px by default, so the
 * override has to step up, not sideways. */
html[data-easy="on"] .reward-shop,
html[data-easy="on"] .reward-meta,
html[data-easy="on"] .rewards-note,
html[data-easy="on"] .balance-note,
html[data-easy="on"] .ticket__shop,
html[data-easy="on"] .ticket__expiry,
html[data-easy="on"] .notif-line,
html[data-easy="on"] .notif-time,
html[data-easy="on"] .wx-day__pm,
html[data-easy="on"] .wx-band__range,
html[data-easy="on"] .rank-privacy,
html[data-easy="on"] .rank-points,
html[data-easy="on"] .wx-source__line {
  font-size: var(--text-base);
}

@media (prefers-reduced-motion: no-preference) {
  html:not([data-entrance="off"]) .balance-card,
  html:not([data-entrance="off"]) .wx-now {
    animation: enter-fade-down 380ms var(--ease-out) backwards;
  }

  html:not([data-entrance="off"]) .reward-item,
  html:not([data-entrance="off"]) .ticket,
  html:not([data-entrance="off"]) .rank-row {
    animation: enter-fade-up 380ms var(--ease-out) 200ms backwards;
  }
}

/* The home weather card became a link; a link brings an underline and a colour with it. */
a.today-card--link {
  color: inherit;
  text-decoration: none;
}

/* passport: the quiet door to the weekly ranking. */
.rank-row-link {
  display: flex;
  min-height: var(--size-control);
  margin-inline: var(--space-gutter);
  margin-block-start: var(--space-sm);
  padding: var(--space-xs) var(--space-md);
  gap: var(--space-xs);
  align-items: center;
  justify-content: space-between;
  border-radius: var(--radius-control);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
  color: var(--color-ink);
  font-size: var(--text-sm);
  font-weight: 600;
  text-decoration: none;
}

.rank-row-link .icon {
  width: 1.1rem;
  height: 1.1rem;
  color: var(--color-primary);
}


/* ═══ ที่พักและร้านค้า · เลนธีม · เริ่มต้นใช้งาน · เข้าสู่ระบบ (SLICES 07 / 08 / 09) ══ */

/* ── The directory ── */
/* The filter can hide this block, and its display rule outranks the UA [hidden] rule. */
.signature[hidden] {
  display: none;
}

.signature {
  display: grid;
  padding-inline: var(--space-gutter);
  padding-block: var(--space-section) 0;
  gap: var(--space-sm);
}

.signature-photo {
  position: relative;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  border-radius: var(--radius-card);
}

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

.signature-sticker {
  position: absolute;
  inset-block-end: var(--space-sm);
  inset-inline-end: var(--space-sm);
  font-size: 1rem;
}

.signature-copy {
  display: grid;
  gap: var(--space-2xs);
  justify-items: start;
}

.signature-kicker {
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
  font-weight: 600;
}

.signature-copy h3 {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 700;
  line-height: var(--lh-title);
}

.signature-line {
  font-size: var(--text-base);
  line-height: 1.7;
}

.shop-panel {
  padding-block: var(--space-section) var(--space-xs);
}

.shop-list {
  gap: var(--space-xs);
}

.shop-list[hidden] {
  display: none;
}

.shop-row .place-row__name {
  white-space: normal;
}

.shop-blurb {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.shop-meta {
  display: flex;
  gap: var(--space-2xs);
  flex-wrap: wrap;
  align-items: center;
  font-size: var(--text-sm);
}

/* An entry with no photograph yet says what kind of place it is. It is a designed
 * absence, not a broken tile — the same rule the place pages already follow for a
 * missing rating or distance. */
.place-row__photo--none {
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--color-tint-merit) 40%, var(--color-surface-sunken));
}

.photo-none__label {
  padding-inline: var(--space-2xs);
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
  font-weight: 600;
  text-align: center;
}

.shop-empty {
  display: grid;
  margin-inline: var(--space-gutter);
  padding: var(--space-lg) var(--space-md);
  gap: var(--space-2xs);
  justify-items: center;
  border: var(--rule-hair) dashed var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  text-align: center;
}

.shop-empty[hidden] {
  display: none;
}

.shop-empty__title {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
}

.shop-empty__body {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.discovery-empty__doodle {
  width: 1.75rem;
  height: 1.75rem;
  color: var(--color-primary-bright);
}

/* ที่พัก is a section of the screen now, not a chip in the merchant filter: the owner
 * asked for somewhere to sleep to stop sitting among lunch. It keeps the merchant list's
 * gutter and rhythm so the two halves read as one product. */
.stay-note,
.stay-section {
  display: grid;
  padding-inline: var(--space-gutter);
  padding-block: var(--space-section) var(--space-xs);
  gap: var(--space-sm);
}

.stay-list {
  display: grid;
  gap: var(--space-md);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* A full-width card, because the two facts a booking turns on — when you can check in and
 * when the room goes back — do not fit a 104 px merchant row. */
.stay-row {
  display: grid;
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--color-surface);
  box-shadow: var(--shadow-card);
}

.stay-row__photo img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

.stay-row__body {
  display: grid;
  padding: var(--space-sm) var(--space-md) var(--space-md);
  gap: var(--space-2xs);
  justify-items: start;
}

.stay-row__name {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 700;
  line-height: var(--lh-title);
}

.stay-row__blurb {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.stay-row__times {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-xs);
  padding-block-start: var(--space-3xs);
}

.stay-row__time {
  padding-inline: var(--space-xs);
  padding-block: 0.15rem;
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
  color: var(--color-ink);
  font-size: var(--text-sm);
  font-weight: 600;
}

.stay-row__nav {
  margin-block-start: var(--space-2xs);
  gap: var(--space-2xs);
  align-items: center;
  display: inline-flex;
  min-height: var(--size-control);
}

.stay-row__nav .icon {
  width: 1.1rem;
  height: 1.1rem;
}

.stay-card {
  display: grid;
  padding: var(--space-md);
  gap: var(--space-2xs);
  justify-items: start;
  border-radius: var(--radius-card);
  background: color-mix(in srgb, var(--color-tint-chill) 40%, var(--color-surface));
}

.stay-kicker {
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
  font-weight: 600;
}

.stay-card h3 {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 700;
  line-height: var(--lh-title);
}

.stay-line {
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

/* ── The lanes ── */
.lane-stage[data-lane-tint="temple"] { background: color-mix(in srgb, var(--color-tint-merit) 42%, var(--color-surface-sunken)); }
.lane-stage[data-lane-tint="food"] { background: color-mix(in srgb, var(--color-tint-food) 42%, var(--color-surface-sunken)); }
.lane-stage[data-lane-tint="nature"] { background: color-mix(in srgb, var(--color-tint-chill) 42%, var(--color-surface-sunken)); }
.lane-stage[data-lane-tint="chill"] { background: color-mix(in srgb, var(--color-tint-oldtown) 42%, var(--color-surface-sunken)); }

.lane-head {
  display: grid;
  padding-inline: var(--space-gutter);
  padding-block: var(--space-sm) var(--space-md);
  gap: var(--space-2xs);
}

.lane-photo {
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius-card);
}

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

.lane-head :is(h1, h2) {
  font-family: var(--font-display);
  font-size: clamp(1.625rem, 8vw, 2rem);
  font-weight: 700;
  line-height: var(--lh-display);
  letter-spacing: 0;
}

.lane-tagline {
  font-size: var(--text-base);
  line-height: var(--lh-body);
}

.lane-filter {
  display: flex;
  margin-inline: var(--space-gutter);
  margin-block-end: var(--space-sm);
  padding: var(--space-2xs) var(--space-sm);
  gap: var(--space-sm);
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  font-size: var(--text-sm);
  font-weight: 600;
}

.lane-filter[hidden] {
  display: none;
}

.lane-filter .link-more {
  border: 0;
  background: none;
  color: var(--color-primary);
  font: inherit;
  cursor: pointer;
}

.lane-places,
.lane-routes,
.lane-others {
  padding-block: var(--space-section) var(--space-xs);
}

.lane-list {
  gap: var(--space-xs);
}

.lane-route-list {
  gap: var(--space-xs);
}

.lane-routes[hidden] {
  display: none;
}

.lane-tiles {
  display: grid;
  margin: 0;
  padding-inline: var(--space-gutter);
  gap: var(--space-xs);
  list-style: none;
}

.lane-tile__link {
  display: grid;
  padding: var(--space-md);
  gap: var(--space-3xs);
  border-radius: var(--radius-tile);
  color: var(--color-ink);
  text-decoration: none;
}

.lane-tile__link[data-lane-tint="temple"] { background: color-mix(in srgb, var(--color-tint-merit) 50%, var(--color-surface)); }
.lane-tile__link[data-lane-tint="food"] { background: color-mix(in srgb, var(--color-tint-food) 50%, var(--color-surface)); }
.lane-tile__link[data-lane-tint="nature"] { background: color-mix(in srgb, var(--color-tint-chill) 50%, var(--color-surface)); }
.lane-tile__link[data-lane-tint="chill"] { background: color-mix(in srgb, var(--color-tint-oldtown) 50%, var(--color-surface)); }

.lane-tile__name {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
  line-height: var(--lh-title);
}

/* The tagline sits on the tile's tint wash, where the muted ink measures 4.48:1 — under the
 * floor. It is browse copy a reader chooses from, so it takes the full ink. */
.lane-tile__line {
  color: var(--color-ink);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

/* ── First run ── */
.app-shell--onboard,
/* These two screens render no bottom nav, so the shell must not reserve its height —
   89 px of empty canvas hung below the footer rule. */
.app-shell--login,
.app-shell--onboard {
  padding-block-end: max(var(--space-md), env(safe-area-inset-bottom, 0px));
}

.app-shell--login {
  background: var(--color-canvas);
}

.onboard {
  display: grid;
  /* The pager is swiped; without this the browser takes every touch for scrolling and the
   * swipe never reaches the script on a phone. */
  touch-action: pan-y pinch-zoom;
  padding-inline: var(--space-gutter);
  padding-block: var(--space-lg) var(--space-xl);
  gap: var(--space-md);
}

.onboard-dots {
  display: flex;
  margin: 0;
  padding: 0;
  gap: var(--space-2xs);
  justify-content: center;
  list-style: none;
}

.onboard-dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--color-border);
}

.onboard-dot.is-on {
  width: 1.5rem;
  border-radius: var(--radius-pill);
  background: var(--color-primary);
}

.onboard-step {
  display: grid;
  gap: var(--space-sm);
  justify-items: start;
}

.onboard-step[hidden] {
  display: none;
}

.onboard-photo {
  width: 100%;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  clip-path: url(#place-hero-blob);
}

html[data-easy="on"] .onboard-photo,
html[data-season="royal"] .onboard-photo,
html[data-season="mourning"] .onboard-photo {
  clip-path: none;
  border-radius: var(--radius-card);
}

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

.onboard-wordmark {
  font-size: clamp(1.5rem, 7vw, 1.875rem);
}

.onboard-title {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 7vw, 1.75rem);
  font-weight: 700;
  line-height: var(--lh-display);
  letter-spacing: 0;
}

/* The consent and permission steps are municipal, so they drop the display face and the
 * warm register together. */
.onboard-title--formal {
  font-family: var(--font-body);
  font-weight: 600;
}

.onboard-line,
.onboard-body {
  font-size: var(--text-base);
  line-height: 1.7;
}

.onboard-note {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.onboard-langs {
  display: flex;
  margin: 0;
  padding: 0;
  gap: var(--space-xs);
  border: 0;
  flex-wrap: wrap;
}

.lang-choice {
  display: inline-flex;
  min-height: var(--size-control);
  padding-inline: var(--space-md);
  gap: var(--space-2xs);
  align-items: center;
  border: var(--rule-hair) solid var(--color-border);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  font-size: var(--text-sm);
  font-weight: 600;
}

.lang-choice input {
  /* The pill fill is the real state cue; the control itself should still be the brand's, not
     the browser's 13 px blue dot, on the first screen an elderly reader meets. */
  width: 1.25rem;
  height: 1.25rem;
  margin: 0;
  accent-color: var(--color-primary);
}

.lang-choice:has(input:checked) input {
  accent-color: var(--color-surface);
}

.lang-choice:has(input:checked) {
  border-color: var(--color-ink);
  background: var(--color-ink);
  color: var(--color-surface);
}

.lang-choice:has(input:disabled) {
  color: var(--color-ink-muted);
}

.onboard-mark {
  display: grid;
  width: 3rem;
  height: 3rem;
  place-items: center;
  border-radius: 50%;
  background: var(--color-surface);
}

.onboard-mark .icon {
  width: 1.5rem;
  height: 1.5rem;
  color: var(--color-primary);
}

.onboard-permissions {
  display: grid;
  margin: 0;
  padding: 0;
  gap: var(--space-sm);
  list-style: none;
}

.onboard-permissions li {
  display: grid;
  padding: var(--space-md);
  gap: var(--space-sm);
  grid-template-columns: 2.5rem minmax(0, 1fr);
  align-items: start;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
}

.onboard-permission__mark {
  display: grid;
  width: 2.5rem;
  height: 2.5rem;
  place-items: center;
  border-radius: 50%;
  background: var(--color-surface-sunken);
}

.onboard-permission__mark .icon {
  width: 1.25rem;
  height: 1.25rem;
  color: var(--color-primary);
}

.onboard-permission__copy {
  display: grid;
  gap: var(--space-3xs);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.onboard-permission__copy strong {
  font-size: var(--text-base);
}

.onboard-actions {
  display: grid;
  width: 100%;
  gap: var(--space-xs);
  justify-items: center;
}

.onboard-actions .wide-button,
.onboard-actions .ghost-button {
  width: 100%;
}

.onboard-actions .ghost-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--size-control);
  text-decoration: none;
}

/* ── Sign in ── */
.login {
  display: grid;
  padding-inline: var(--space-gutter);
  padding-block: var(--space-lg) var(--space-xl);
  gap: var(--space-md);
}

.login-title {
  font-family: var(--font-body);
  font-size: clamp(1.5rem, 7vw, 1.75rem);
  font-weight: 600;
  line-height: var(--lh-display);
}

.login-line,
.login-sent {
  font-size: var(--text-base);
  line-height: var(--lh-body);
}

.login-form {
  display: grid;
  gap: var(--space-xs);
}

.login-form[hidden] {
  display: none;
}

.field {
  display: grid;
  gap: var(--space-2xs);
}

.field__label {
  font-size: var(--text-sm);
  font-weight: 600;
}

.field__input {
  min-height: 3.25rem;
  padding-inline: var(--space-md);
  /* Muted ink, not the hairline token: the border is the only thing that says "this is a
   * field", and --color-border on cream is 1.2:1 — a glare-lit screen erases it. Same
   * swap .filter-chip and .ghost-button already took. */
  border: var(--rule-card) solid var(--color-ink-muted);
  border-radius: var(--radius-control);
  background: var(--color-surface);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: var(--text-base);
}

html[data-input="key"] .field__input:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

.field__input--otp {
  font-size: var(--text-lg);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.4em;
  text-align: center;
}

.field__help {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.field__error {
  color: var(--color-danger);
  font-size: var(--text-sm);
  font-weight: 600;
}

/* Empty is the resting state now: the paragraph stays in the accessibility tree only when
   it actually says something, because aria-describedby announces it either way. */
.field__error[hidden],
.field__error:empty {
  display: none;
}

.login-resend {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.login-alt {
  display: grid;
  gap: var(--space-xs);
  padding-block-start: var(--space-sm);
  border-block-start: var(--rule-hair) solid var(--color-border);
}

.login-alt .ghost-button {
  display: inline-flex;
  min-height: var(--size-control);
  align-items: center;
  justify-content: center;
  text-decoration: none;
}

.login-legal {
  color: var(--color-ink-muted);
  /* Consent is a rights-bearing string, so it sits on the body step like body copy. */
  font-size: var(--text-base);
  line-height: var(--lh-body);
}

.login-legal a {
  color: var(--color-primary);
}

/* The tap area without the 44 px line box that pushed the sentence's last word onto a line
 * of its own. */
.login-legal a::after {
  position: absolute;
  inset: -10px -4px;
  content: "";
}

html[data-easy="on"] .shop-blurb,
html[data-easy="on"] .lane-tile__line,
html[data-easy="on"] .onboard-note,
html[data-easy="on"] .field__help,
html[data-easy="on"] .login-legal {
  font-size: var(--text-base);
}

@media (prefers-reduced-motion: no-preference) {
  html:not([data-entrance="off"]) .signature,
  html:not([data-entrance="off"]) .lane-head {
    animation: enter-fade-up 420ms var(--ease-out) backwards;
  }

  html:not([data-entrance="off"]) .onboard-step {
    animation: enter-fade-up 300ms var(--ease-out) backwards;
  }
}

/* The air-quality chip changes with the band it reports, and the worst band is the one
 * colour the product reserves for danger. */
.chip--danger {
  background: color-mix(in srgb, var(--color-danger) 14%, var(--color-surface));
  color: var(--color-danger);
}

/* The directory row says where it goes, and the policy disclosure reads like a document. */
.shop-go {
  display: inline-flex;
  gap: var(--space-2xs);
  align-items: center;
  color: var(--color-primary);
  font-size: var(--text-sm);
  font-weight: 600;
}

.shop-go .icon {
  width: 1rem;
  height: 1rem;
}

/* No override: the chip row is one horizontal scroller everywhere, so a reader who learns
   it on ที่เที่ยว meets the same control on ที่พักและร้านค้า. */

.shop-row .place-row__body,
.lane-list .place-row__body {
  padding-inline-end: var(--space-sm);
}

.policy {
  width: 100%;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
}

.policy summary {
  min-height: var(--size-control);
  padding: var(--space-xs) var(--space-md);
  color: var(--color-primary);
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}

.policy__body {
  display: grid;
  padding: 0 var(--space-md) var(--space-md);
  gap: var(--space-xs);
  /* Consent prose is body copy: three PDPA paragraphs sat a step below the summary
     paragraph that introduces them. */
  font-size: var(--text-base);
  line-height: var(--lh-body);
}

/* The app-wide min-height rule is dead on an inline box, so the padding carries the 44 px
 * floor here — and the box stays inline, because inline-flex broke the sentence's wrap. */
.login-legal a {
  position: relative;
  display: inline;
  padding-block: 0.6rem;
}

/* A demo value says so on the screen, not only in a comment. */
.sos-demo,
.news-demo {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.news-demo {
  padding-inline: var(--space-gutter);
  padding-block-start: var(--space-md);
  text-align: center;
}

/* A pressed filter chip is ink-filled, so its focus ring has to be the cream. */
html[data-input="key"] .filter-chip[aria-pressed="true"]:focus-visible {
  outline-color: var(--color-on-primary);
}

/* ที่เที่ยว's empty state, and the passport's count that is not a link. */
.places-empty {
  display: grid;
  margin-inline: var(--space-gutter);
  margin-block-end: var(--space-md);
  padding: var(--space-lg) var(--space-md);
  gap: var(--space-xs);
  justify-items: center;
  border: var(--rule-hair) dashed var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  text-align: center;
}

.places-empty[hidden] {
  display: none;
}

.places-empty__title {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
}

.section-count {
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
  font-weight: 600;
}

.place-row[hidden] {
  display: none;
}

/* ── Round-three repairs: targets, type floors, and the states that carry text ── */

/* The map card's own controls are tapped on the same moving map as the pins. */
.map-place-card__close {
  width: var(--size-control);
  height: var(--size-control);
}

.map-place-card__nav {
  min-height: var(--size-control);
}

/* A chip carries the most decision-relevant micro-text in the product — เปิดอยู่ / ปิดแล้ว —
 * so easy mode has to grow it along with everything else, and a stamp name is allowed to
 * wrap rather than be cut. */
.stamp span {
  font-size: var(--text-xs);
  white-space: normal;
  overflow-wrap: anywhere;
}

/* Easy mode moves the type scale itself, so it reaches every component that reads a token —
 * including the emergency screen and the settings screen's own copy, which a hand-maintained
 * list of class names had been missing. The per-class overrides below stay only where a
 * component needs more than the scale gives it. */
/* The step goes on the root: every --text-* token is declared in rem, so putting it on body
 * left every explicitly-sized string — the redemption warning, the PDPA text, the SOS steps —
 * pinned at its old size while the switch promised "ตัวหนังสือชัดขึ้น". */
html[data-easy="on"] {
  font-size: 112.5%;
  --text-2xs: 0.875rem;
  --text-xs: 0.875rem;
  --text-sm: 1rem;
  --text-base: 1.125rem;
  --text-lg: 1.625rem;
}

html[data-easy="on"] .toast {
  font-size: 1.125rem;
}

/* The filter chips are the primary way to narrow a list, and easy mode had been leaving
 * them the smallest type on the screen. */
html[data-easy="on"] .filter-chip,
html[data-easy="on"] .view-button {
  font-size: var(--text-base);
}

html[data-easy="on"] .chip,
html[data-easy="on"] .stamp span,
html[data-easy="on"] .stamp-date,
html[data-easy="on"] .rank-basis,
html[data-easy="on"] .balance-note {
  font-size: var(--text-sm);
}

/* A news row is a thumbnail, a kicker, a headline and one line of summary — the same
 * shape the place rows use, so the two lists read as one product. */
.news-row .news-item {
  grid-template-columns: 4.5rem minmax(0, 1fr);
  gap: var(--space-sm);
  align-items: center;
}

.news-thumb {
  display: block;
  overflow: hidden;
  /* A thumbnail is a picture: it stops growing with the text size so the headline keeps its column. */
  width: min(4.5rem, 22vw);
  height: min(3.4rem, 16.5vw);
  border-radius: var(--radius-tile);
  background: var(--color-surface-sunken);
}

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

.news-row .news-item .news-body {
  display: grid;
  min-width: 0;
  gap: var(--space-3xs);
}

/* The row needs three lines to finish its short summary; the article has the rest. The type step stays at the
 * app's secondary size — this is the only descriptive copy on ข่าวสาร, not a disclosure
 * note, and the notes floor is for the "ค่าตัวอย่างสำหรับต้นแบบ" lines. */
.news-summary--row {
  display: -webkit-box;
  overflow: hidden;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
}

/* Easy mode asked for more words, not fewer: a fixed two-line budget with bigger type
 * hides up to four of six lines, so the clamp opens with the type. */
html[data-easy="on"] .news-summary--row {
  -webkit-line-clamp: 4;
}

/* Two links, one line, wrapping when the Thai does. */
.stay-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-md);
}

/* The toast: the visible half of announce(). It never blocks anything, it never traps
 * focus, and it says the same words the live region says. */
.toast {
  position: fixed;
  inset-block-end: calc(var(--size-nav) + var(--space-md) + env(safe-area-inset-bottom, 0px));
  inset-inline: var(--space-gutter);
  /* Above the sheets, because the announcements it carries are often about them. */
  z-index: var(--z-tooltip);
  margin-inline: auto;
  padding: var(--space-sm) var(--space-md);
  max-width: 22rem;
  border-radius: var(--radius-pill);
  background: var(--color-ink);
  box-shadow: var(--shadow-float);
  color: var(--color-canvas);
  font-size: var(--text-base);
  line-height: var(--lh-body);
  text-align: center;
  opacity: 0;
  transform: translateY(0.5rem);
  transition: opacity 200ms var(--ease-out), transform 200ms var(--ease-out);
  pointer-events: none;
}

.toast[data-state="on"] {
  opacity: 1;
  transform: translateY(0);
  /* Deliberately NOT pointer-events: auto. The bar sits over the last strip of scrollable
     content for up to six seconds, and an elderly reader's first tap there belongs to the
     control underneath, not to dismissing a message the live region has already spoken. */
}

@media (prefers-reduced-motion: reduce) {
  .toast {
    transform: none;
    transition: opacity 1ms linear;
  }
}

/* The search field the pill opens: one row, a real input, a way out. */
.search-field {
  display: grid;
  margin-block-start: var(--space-xs);
  gap: var(--space-xs);
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
}

.search-field[hidden] {
  display: none;
}

.search-input {
  min-height: var(--size-control);
  padding: var(--space-xs) var(--space-md);
  /* Same reason as .field__input: the box has no other boundary. */
  border: var(--rule-hair) solid var(--color-ink-muted);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-ink);
  font: inherit;
  font-size: var(--text-base);
}

/* The app focuses this field programmatically, so the ring is the only signal the reader
 * has been relocated — same treatment as .field__input. */
html[data-input="key"] .search-input:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

.search-input::placeholder {
  color: var(--color-ink-muted);
}

/* The hourly strip: eight readings, scrolled sideways, each one a tap-sized column so the
 * row is comfortable to drag with a thumb. */
.wx-hours-scroll {
  overflow-x: auto;
  scrollbar-width: none;
}

.wx-hours-scroll::-webkit-scrollbar {
  display: none;
}

.wx-hours {
  display: grid;
  padding-inline: var(--space-gutter);
  padding-block-end: var(--space-2xs);
  gap: var(--space-xs);
  grid-auto-flow: column;
  grid-auto-columns: 4rem;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
}

.wx-hours::-webkit-scrollbar {
  display: none;
}

html[data-input="key"] .wx-hours-scroll:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: -2px;
}

.wx-hour {
  display: grid;
  padding: var(--space-sm) var(--space-2xs);
  gap: var(--space-3xs);
  justify-items: center;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
  scroll-snap-align: start;
}

.wx-hour__time {
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
}

.wx-hour__icon {
  width: 1.5rem;
  height: 1.5rem;
  color: var(--color-primary);
}

.wx-hour__temp {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: 700;
}

/* ── The merchant panel: the menu and the coupon the seed always carried ── */
.merchant-panel {
  display: grid;
  margin-block-start: calc(var(--space-2xs) * -1);
  padding: var(--space-md);
  gap: var(--space-xs);
  border: var(--rule-hair) solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface-sunken);
}

.merchant-panel[hidden] {
  display: none;
}

.merchant-panel__hours {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.merchant-panel__title {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 700;
}

.merchant-panel__pending {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

.shop-menu {
  display: grid;
  gap: var(--space-2xs);
}

.shop-menu__row {
  display: flex;
  gap: var(--space-sm);
  justify-content: space-between;
  font-size: var(--text-sm);
}

.shop-menu__price {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.merchant-panel__offer {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs) var(--space-xs);
  align-items: center;
  font-size: var(--text-sm);
}

.merchant-panel__actions .ghost-button {
  width: 100%;
}

/* ── The badge case ── */
.badges {
  padding-block: var(--space-section) var(--space-xs);
}

.badge-grid {
  display: grid;
  padding-inline: var(--space-gutter);
  gap: var(--space-xs);
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.badge {
  display: grid;
  padding: var(--space-sm) var(--space-2xs);
  gap: var(--space-3xs);
  justify-items: center;
  border: var(--rule-hair) dashed var(--color-border);
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  color: var(--color-ink-muted);
  text-align: center;
}

.badge.is-on {
  border-style: solid;
  border-color: var(--color-primary);
  color: var(--color-ink);
}

.badge__mark {
  display: grid;
  width: 2.25rem;
  height: 2.25rem;
  place-items: center;
  border-radius: 50%;
  background: var(--color-surface-sunken);
  color: var(--color-ink-muted);
}

.badge__emblem {
  width: 2.25rem;
  height: 2.25rem;
  object-fit: contain;
}

.emblem-fallback {
  display: none;
}

html[data-season="royal"] .stamp__emblem,
html[data-season="mourning"] .stamp__emblem,
html[data-season="royal"] .badge__emblem,
html[data-season="mourning"] .badge__emblem {
  display: none;
}

html[data-season="royal"] .stamp .emblem-fallback,
html[data-season="mourning"] .stamp .emblem-fallback,
html[data-season="royal"] .badge .emblem-fallback,
html[data-season="mourning"] .badge .emblem-fallback {
  display: block;
}

.badge.is-on .badge__mark {
  background: color-mix(in srgb, var(--color-primary) 14%, var(--color-surface));
  color: var(--color-primary);
}

.badge__name {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 700;
  line-height: 1.3;
}

.badge__rule {
  font-size: var(--text-xs);
  line-height: 1.4;
}

/* ── The daily missions ── */
.missions {
  padding-block: var(--space-section) var(--space-xs);
}

.mission-list {
  display: grid;
  padding-inline: var(--space-gutter);
  gap: var(--space-xs);
}

.mission-row {
  display: grid;
  padding: var(--space-sm) var(--space-md);
  gap: var(--space-sm);
  /* The mission name keeps most of the row; the state chip shrinks and wraps before the name does. */
  grid-template-columns: minmax(58%, 1fr) minmax(0, auto);
  align-items: center;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.mission-row__body {
  display: grid;
  min-width: 0;
  gap: var(--space-3xs);
}

.mission-row__name {
  font-weight: 600;
}

.mission-row__meta {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.mission-bar {
  display: block;
  overflow: hidden;
  height: 0.375rem;
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
}

.mission-bar > span {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--color-primary);
}

/* The saved-view banner: a statement about the list, and the way back out of it. */
.places-note {
  display: grid;
  margin-inline: var(--space-gutter);
  margin-block-end: var(--space-xs);
  padding: var(--space-sm) var(--space-md);
  gap: var(--space-xs);
  justify-items: center;
  border-radius: var(--radius-card);
  background: var(--color-surface-sunken);
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  text-align: center;
}

/* Intent with no visible chip: one compact, dismissible line keeps URL, result set and count
 * visible together. The close control remains a full touch target even though its glyph is small. */
.intent-arrival {
  display: flex;
  min-height: 44px;
  margin: 0 var(--space-gutter) var(--space-xs);
  padding-inline-start: var(--space-sm);
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs);
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  font-weight: 600;
}

.intent-arrival__dismiss {
  width: 44px;
  min-width: 44px;
  height: 44px;
  border: 0;
  background: transparent;
  color: var(--color-primary);
  font: 700 var(--text-lg)/1 var(--font-body);
  cursor: pointer;
}

/* A jump control that looks like the link it replaced. */
.link-more--button {
  border: 0;
  background: none;
  font: inherit;
  cursor: pointer;
}

.places-official {
  margin-inline: var(--space-gutter);
  margin-block-start: var(--space-sm);
}

/* Both account states live in the markup; only one is shown. */
/* An author display rule outranks the user-agent [hidden] rule, so every class that sets
 * display has to say this itself — the step-one Back button rendered and took a tab stop
 * while marked hidden, and did nothing when pressed. */
.rank-standing[hidden],
.profile-folio[hidden],
.setting-row[hidden],
.saved[hidden],
.filter-row[hidden],
.search-zone[hidden],
.onboard-actions .ghost-button[hidden],
.ghost-button[hidden],
.wide-button[hidden] {
  display: none;
}

/* The sign-out row is a button but reads as the setting rows around it. */
button.setting-copy {
  border: 0;
  background: none;
  font: inherit;
  text-align: start;
}

/* The other-lanes tiles carry the lane's own photograph, like every other rail. */
.lane-tile__link {
  display: grid;
  overflow: hidden;
  grid-template-columns: 5rem minmax(0, 1fr);
  align-items: center;
  gap: var(--space-sm);
}

.lane-tile__photo {
  display: block;
  overflow: hidden;
  width: 5rem;
  height: 3.75rem;
  border-radius: var(--radius-tile);
  background: var(--color-surface-sunken);
}

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

.lane-tile__body {
  display: grid;
  min-width: 0;
  gap: var(--space-3xs);
}

/* Timestamp over unread dot, in the card's third column. */
.notif-meta {
  display: grid;
  gap: var(--space-3xs);
  justify-items: end;
}

.places-empty__title[hidden] {
  display: none;
}

/* ฉัน: nothing hearted yet. */
.saved-empty {
  display: grid;
  padding: var(--space-md);
  gap: var(--space-xs);
  justify-items: start;
  border: var(--rule-hair) dashed var(--color-border);
  border-radius: var(--radius-card);
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

/* The name sheet reuses the confirm sheet; its line is the same voice as the confirm copy. */
.confirm-line {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
}

/* The now-block's glyph, at the scale the temperature sets. */
.wx-now__icon {
  width: 2.5rem;
  height: 2.5rem;
  color: var(--color-primary);
}

/* An announcement carries the municipality's seal mark instead of a borrowed photograph:
 * a garbage-collection notice under a picture of the old town read as filler. */
/* Municipal stationery: a letterhead band that sizes to its own two lines. It must not keep
 * .article-photo's 16:10 frame — that left a seal floating in an empty photograph. */
.article-photo--notice {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  aspect-ratio: auto;
  border: var(--rule-hair) solid var(--color-border);
  border-block-start: 3px solid var(--color-primary);
  border-radius: var(--radius-tile);
  background: var(--color-surface-sunken);
  text-align: start;
}

.article-photo--notice .notice-seal {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  box-shadow: inset 0 0 0 var(--rule-hair) var(--color-border);
}

.article-photo--notice .icon {
  width: 1.5rem;
  height: 1.5rem;
  color: var(--color-primary);
}

.article-photo--notice .notice-copy {
  display: grid;
  gap: 0.125rem;
}

.article-photo--notice .notice-office {
  color: var(--color-ink);
  font-size: var(--text-sm);
  font-weight: 600;
}

.article-photo--notice .notice-date {
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
}

.news-thumb--notice,
.news-photo--notice {
  display: grid;
  place-items: center;
  border: var(--rule-hair) solid var(--color-border);
  background: var(--color-surface-sunken);
  color: var(--color-primary);
}

.news-photo--notice {
  aspect-ratio: 16 / 10;
}

.news-thumb--notice .icon,
.news-photo--notice .icon {
  width: 1.5rem;
  height: 1.5rem;
}

/* Each fact in a meta line is one unbreakable group. */
.place-meta > *,
.shop-meta > * {
  white-space: nowrap;
}

/* Easy mode grows these facts, and inside a clipping row the nowrap rule turns that growth
 * into loss — the same failure the .checkin-row__link release below fixed. The release
 * covers every meta fact and the rating, so nothing the reader asked to enlarge is cut. */
html[data-easy="on"] .place-meta > *,
html[data-easy="on"] .shop-meta > *,
html[data-easy="on"] .place-rating {
  white-space: normal;
}

/* The check-in row opens the place it names, so the whole band is one target. */
.checkin-row__link {
  display: grid;
  /* The status chip goes under the body rather than beside it: as a third auto column its
     nowrap text sized the whole row, pushing 426 px of row into a 375 px shell in easy mode
     and squeezing "ตักบาตรทางเรือ" down a 61 px column. */
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-2xs) var(--space-sm);
  align-items: center;
  color: inherit;
  text-decoration: none;
}

.checkin-row__link > .chip {
  grid-column: 2;
  justify-self: start;
  max-inline-size: 100%;
  white-space: normal;
}

.checkin-row:first-child .checkin-row__link {
  display: block;
}

/* The subtitle under a section heading is a caption, not body copy. */
.section-sub {
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
  line-height: 1.45;
}

/* The places search pill is a control with a text label, so it holds the body step like
   the ghost buttons beside it. Scoped on [aria-controls] so /home's approved pill, which
   has none, is untouched. */
.search-pill[aria-controls] {
  font-size: var(--text-base);
}

/* Controls carry their labels at the body step: a ghost button that dismisses an empty state
 * and a view toggle are read at arm's length outdoors, like everything else. */
.ghost-button,
.view-button {
  font-size: var(--text-base);
}

/* Each fact in a meta line is its own element, and the separator is drawn between them
 * rather than typed as a text node that can be orphaned on a line of its own. */
.place-meta {
  flex-wrap: wrap;
  row-gap: 0.15rem;
  align-items: baseline;
}

/* The dot hangs off the fact BEFORE the distance, not off the distance itself: welded to the
 * front of a nowrap distance it travelled with it, and the second line began with a middot. */
.place-meta .meta-part:has(+ [data-distance-to])::after {
  content: " ·";
}

.place-meta .chip + .meta-part::before {
  content: "";
}

/* A demo toggle that keeps one label needs a visible pressed state, or only a screen reader
 * knows which way it is set. */
.ghost-button[aria-pressed="true"] {
  border-color: var(--color-ink);
  background: var(--color-ink);
  color: var(--color-surface);
}

/* The season row's control drops below its copy so the row keeps the same copy width as
 * every other setting. */
.setting-row:has([data-season-toggle]) {
  grid-template-columns: auto minmax(0, 1fr);
}

.setting-row [data-season-toggle] {
  grid-column: 2 / -1;
  justify-self: start;
}

/* A demo state toggle is marked as one, so it can never be read as a product action —
 * "ดูสถานะเมื่อไม่พบพิกัด" sits directly under the emergency numbers. */
.demo-switch {
  display: grid;
  margin-inline: var(--space-gutter);
  margin-block-start: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  gap: var(--space-2xs);
  justify-items: start;
  border: var(--rule-hair) dashed var(--color-border);
  border-radius: var(--radius-card);
  color: var(--color-ink-muted);
}

/* The box already pads itself, so the note must not add the page gutter on top: three items in
 * one stack were sitting on two left edges. */
.demo-switch .fine-note {
  padding-inline: 0;
}

/* The emergency screen keeps the copy outcome on screen rather than in a message that fades. */
.sos-copy-state {
  color: var(--color-ink);
  font-size: var(--text-sm);
  font-weight: 600;
}

/* Empty it collapses to nothing, so it can stay in the tree from first paint — a role="status"
   that is display:none when the text arrives announces nothing. */
.sos-copy-state:empty {
  margin: 0;
}

/* ADR-018: the camera lives inside the scan frame while a QR is being read. The corners and
   the hint stay on top; the mark and the sub-line step aside for the picture. */
.scan-video {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}

.scan-frame[data-state="scanning"] {
  padding: 0;
  border-style: solid;
  border-color: var(--color-primary);
  overflow: hidden;
}

.scan-frame[data-state="scanning"] .scan-mark,
.scan-frame[data-state="scanning"] .scan-sub {
  display: none;
}

.scan-frame[data-state="scanning"] .scan-corner {
  z-index: 1;
}

.scan-frame[data-state="scanning"] .scan-hint {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0.75rem;
  z-index: 1;
  margin: 0;
  padding: 0.25rem 0.5rem;
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
}

/* ADR-019: the three ranking boards. A radio group styled as one segmented control — the
   selected state is the input's, so a screen reader and a keyboard both get it for free. */
.rank-windows {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  margin-block: var(--space-sm) 0;
  padding: 0.25rem;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-surface-sunken);
}

.rank-window {
  display: grid;
  min-height: 2.75rem;
  place-items: center;
  border-radius: 999px;
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}

.rank-window input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.rank-window:has(input:checked) {
  background: var(--color-surface);
  box-shadow: 0 1px 2px rgb(0 0 0 / 0.08);
  color: var(--color-primary);
}

html[data-input="key"] .rank-window:has(input:focus-visible) {
  outline: 3px solid var(--color-focus);
  outline-offset: 2px;
}

.rank-standing__window {
  color: var(--color-ink-muted);
  font-size: var(--text-2xs);
}

/* Phase 1: the stated closure of ของรางวัล and คูปองของฉัน. */
.closed-notice {
  display: grid;
  justify-items: center;
  gap: var(--space-sm);
  margin: var(--space-lg) var(--space-md);
  padding: var(--space-lg) var(--space-md);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  text-align: center;
}

.closed-mark {
  display: grid;
  width: 3.5rem;
  height: 3.5rem;
  place-items: center;
  border-radius: 999px;
  background: var(--color-surface-sunken);
  color: var(--color-primary);
}

.closed-title {
  font-size: var(--text-lg);
}

.closed-line {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.closed-notice .wide-button,
.closed-notice .ghost-button {
  width: 100%;
}

.rank-standing__none {
  font-size: var(--text-md);
  font-weight: 600;
}

/* ── อันดับ, design A (owner picked the podium render 2026-08-27) ───────────────
   The top three stand on a terracotta card with a cream hill under them; the rest of the
   board is one quiet ledger; the reader's own standing closes the screen. */
.podium {
  position: relative;
  margin: var(--space-md) var(--space-gutter) 0;
  padding: var(--space-lg) var(--space-sm) 0;
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--color-primary);
}

/* The cream hill the three stand on, so the plinths read as ground rather than as floating
   boxes. A wide ellipse, mostly below the card, showing only its crown. */
.podium::after {
  position: absolute;
  z-index: 0;
  inset-block-end: -60%;
  inset-inline: -12%;
  aspect-ratio: 2 / 1;
  border-radius: 50%;
  background: var(--color-canvas);
  content: "";
}

.podium[hidden] {
  display: none;
}

.podium-steps {
  position: relative;
  z-index: 1;
  display: grid;
  margin: 0;
  padding: 0;
  gap: var(--space-2xs);
  /* minmax(0, 1fr): a long name may wrap, never push the third step off the card. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: end;
  list-style: none;
}

.podium-step {
  position: relative;
  display: grid;
  justify-items: center;
  gap: var(--space-2xs);
}

/* First in the middle and tallest — the reading order stays 1, 2, 3 for a screen reader. */
.podium-step--1 { order: 2; }
.podium-step--2 { order: 1; }
.podium-step--3 { order: 3; }

.podium-figure {
  position: relative;
  z-index: 1;
  display: grid;
  width: 3.5rem;
  height: 3.5rem;
  place-items: center;
  border-radius: 50%;
  background: var(--color-tint-chill);
  box-shadow: 0 0 0 3px var(--color-on-primary);
  color: var(--color-ink);
  font-size: var(--text-md);
  font-weight: 700;
}

.podium-step--1 .podium-figure {
  width: 4.25rem;
  height: 4.25rem;
  background: var(--color-ink);
  color: var(--color-on-primary);
  font-size: var(--text-lg);
}

.podium-step--3 .podium-figure {
  background: var(--color-tint-photo);
}

/* Ornament: suppressed in mourning and royal seasons, like every other motif. */
.podium-crown {
  position: absolute;
  inset-block-start: -1.5rem;
  width: 2.25rem;
  height: auto;
}

.podium-rays {
  position: absolute;
  z-index: 0;
  inset-block-start: -1rem;
  inset-inline-start: 50%;
  width: 7.5rem;
  height: auto;
  opacity: 0.92;
  pointer-events: none;
  translate: -50% 0;
}

html[data-easy="on"] .podium-crown,
html[data-easy="on"] .podium-rays,
html[data-season="mourning"] .podium-crown,
html[data-season="mourning"] .podium-rays,
html[data-season="royal"] .podium-crown,
html[data-season="royal"] .podium-rays {
  display: none;
}

.podium-rank {
  position: relative;
  z-index: 2;
  display: grid;
  width: 1.6rem;
  height: 1.6rem;
  margin-block-end: -0.8rem;
  place-items: center;
  border-radius: 50%;
  background: var(--color-surface);
  box-shadow: 0 0 0 2px var(--color-primary);
  color: var(--color-ink);
  font-size: var(--text-2xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.podium-step--1 .podium-rank {
  background: var(--color-tint-merit);
}

/* The plinth each name stands on: cream, so the card's hill reads as ground. */
.podium-plinth {
  display: grid;
  width: 100%;
  min-height: 4.25rem;
  padding: var(--space-sm) var(--space-3xs) var(--space-md);
  align-content: start;
  justify-items: center;
  gap: 0.1rem;
  border-radius: var(--radius-tile) var(--radius-tile) 0 0;
  background: var(--color-surface);
  text-align: center;
}

.podium-step--1 .podium-plinth {
  min-height: 5.5rem;
  background: var(--color-tint-merit);
}

.podium-step--3 .podium-plinth {
  min-height: 3.75rem;
}

.podium-name {
  display: block;
  min-height: 2.6em;
  max-width: 100%;
  overflow: visible;
  font-size: var(--text-sm);
  font-weight: 700;
  line-height: 1.3;
  overflow-wrap: anywhere;
  white-space: normal;
}

.podium-points {
  font-size: var(--text-2xs);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.podium-self {
  color: var(--color-primary);
}

/* The remaining ranks: one sheet, hairlines between, no floating cards. */
.rank-list {
  gap: 0;
  overflow: hidden;
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.rank-panel .rank-row {
  border-radius: 0;
  box-shadow: none;
}

.rank-panel .rank-row + .rank-row {
  border-block-start: var(--rule-hair) solid var(--color-border);
}

/* The reader's own standing, at the end of the board. */
.rank-standing {
  display: grid;
  align-items: center;
  gap: var(--space-3xs) var(--space-sm);
  grid-template-areas:
    "avatar body points"
    "avatar gap   gap";
  grid-template-columns: auto minmax(0, 1fr) auto;
  border: 0;
  background: var(--color-primary);
  color: var(--color-on-primary);
}

.rank-standing__avatar { grid-area: avatar; }
.rank-standing__body { grid-area: body; }
.rank-standing__score { grid-area: points; }
.rank-standing .rank-standing__gap { grid-area: gap; }

.rank-standing__avatar {
  display: grid;
  width: 3rem;
  height: 3rem;
  place-items: center;
  border-radius: 50%;
  background: var(--color-tint-merit);
  color: var(--color-ink);
  font-size: var(--text-md);
  font-weight: 700;
}

.rank-standing__body {
  display: grid;
  gap: 0.15rem;
}

.rank-standing__score {
  display: block;
  text-align: end;
}

.rank-standing .rank-standing__label,
.rank-standing .rank-standing__gap {
  color: color-mix(in srgb, var(--color-on-primary) 82%, transparent);
  font-size: var(--text-2xs);
}

.rank-standing .rank-standing__place {
  color: var(--color-on-primary);
}

.rank-standing .rank-standing__of {
  color: color-mix(in srgb, var(--color-on-primary) 78%, transparent);
}

.rank-standing .rank-standing__points {
  font-size: var(--text-md);
  font-weight: 700;
}

.rank-standing__none {
  font-size: var(--text-base);
  font-weight: 700;
  white-space: nowrap;
}

/* ── ที่เที่ยว, tightened 2026-08-27 ────────────────────────────────────────────
   The screen is named ที่เที่ยว and used to show none above the fold: a hero card, a search
   pill, a view switch, three full-width doors and six chips came first. The doors moved to the
   end, the switch joined the search row, the disclaimer went to the bottom, and the subtitles
   went away. */
.page-head__count {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  font-weight: 600;
  white-space: nowrap;
}

.search-row {
  display: grid;
  padding-inline: var(--space-gutter);
  gap: var(--space-2xs);
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
}

.search-row .search-zone,
.search-row .view-switch {
  padding-inline: 0;
}

.view-switch--compact {
  display: flex;
  gap: 0;
  padding: 0.2rem;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  background: var(--color-surface-sunken);
}

.view-switch--compact .view-button {
  /* 44 px: the floor for anything a thumb has to hit. */
  min-height: 2.75rem;
  padding-inline: var(--space-xs);
  border: 0;
  border-radius: 999px;
  background: transparent;
  box-shadow: none;
  font-size: var(--text-2xs);
}

.view-switch--compact .view-button[aria-pressed="true"] {
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
  color: var(--color-primary);
}

/* The three other collections, one compact row, after the places. */
.browse-doors {
  display: grid;
  margin: var(--space-md) var(--space-gutter) 0;
  gap: var(--space-2xs);
  grid-template-columns: repeat(3, 1fr);
}

.browse-door {
  display: grid;
  min-height: 3.5rem;
  padding: var(--space-xs) var(--space-3xs);
  place-items: center;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-tile);
  background: var(--color-surface);
  color: inherit;
  text-align: center;
  text-decoration: none;
}

.browse-door__copy strong {
  display: block;
  font-size: var(--text-2xs);
  font-weight: 700;
  line-height: 1.35;
}

.browse-doors + .place-official {
  margin-block-start: var(--space-md);
}

/* ── While the account is still unknown — owner 2026-08-27 ─────────────────────
   The app used to paint a pretend citizen's stamps and points and swap them for the real ones
   a moment later, which read as the screen reloading itself. Nothing account-derived paints
   until the answer is in: `html[data-game]` is set by app.js to "ready" (the account answered),
   "offline" (no account or no network — the honest empty states take over) or "loading". The
   default, before any script runs, is the skeleton, so a wrong number can never reach a screen.
   No animation: the motion ceiling forbids a perpetual one, and a resting block is calmer. */
html:not([data-game="ready"]) [data-passport="held"],
html:not([data-game="ready"]) [data-passport="count"],
html:not([data-game="ready"]) [data-passport="tier"],
html:not([data-game="ready"]) [data-passport="memory-count"],
html:not([data-game="ready"]) [data-badges="count"],
html:not([data-game="ready"]) [data-me="stamps"],
html:not([data-game="ready"]) [data-me="saved"],
html:not([data-game="ready"]) [data-me="checkins"],
html:not([data-game="ready"]) [data-rewards="balance"],
html:not([data-game="ready"]) [data-rank="my-points"],
html:not([data-game="ready"]) .rank-standing__place {
  border-radius: 0.4rem;
  background: var(--color-surface-sunken);
  box-shadow: inset 0 0 0 1px var(--color-border);
  color: transparent;
}

html:not([data-game="ready"]) .rank-standing__place .rank-standing__of {
  color: transparent;
}

html:not([data-game="ready"]) [data-passport="line"],
html:not([data-game="ready"]) .passport-line,
html:not([data-game="ready"]) [data-rank="gap"] {
  border-radius: 0.4rem;
  background: var(--color-surface-sunken);
  color: transparent;
}

/* An SVG numeral cannot take a background, so it simply waits. */
html:not([data-game="ready"]) .passport-ring .ring-num,
html:not([data-game="ready"]) .stamp-strip .st.is-on {
  visibility: hidden;
}

/* The rows a list will hold, drawn as resting blocks so the screen keeps its shape. */
.sk-row {
  height: 3.25rem;
  border-radius: var(--radius-tile);
  background: var(--color-surface-sunken);
  box-shadow: inset 0 0 0 1px var(--color-border);
  list-style: none;
}

.sk-podium {
  display: grid;
  height: 8.5rem;
  margin: var(--space-md) var(--space-gutter) 0;
  border-radius: var(--radius-card);
  background: var(--color-surface-sunken);
  box-shadow: inset 0 0 0 1px var(--color-border);
}

.sk-tile {
  background: var(--color-surface-sunken);
  box-shadow: inset 0 0 0 1px var(--color-border);
}

/* ---- Sponsor-coupon preview — the rewards screen's closed state (owner 2026-08-28) ---- */
.sponsor-preview {
  display: grid;
  gap: var(--space-sm);
  padding-block: var(--space-section);
  padding-inline: var(--space-gutter);
}

.sponsor-list {
  display: grid;
  gap: var(--space-sm);
  margin: 0;
  padding: 0;
  list-style: none;
}

.sponsor-ticket {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  border: var(--rule-card) dashed var(--color-primary);
  border-radius: var(--radius-tile);
  background: var(--color-surface);
}

.sponsor-ticket__goal {
  display: grid;
  place-items: center;
  flex: none;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--color-tint-merit) 55%, var(--color-surface));
  color: var(--color-primary);
}

.sponsor-ticket__goal strong {
  font-family: var(--font-display);
  font-size: var(--text-md);
  line-height: 1;
}

.sponsor-ticket__goal span {
  font-size: var(--text-2xs);
  line-height: 1.2;
}

.sponsor-ticket__body {
  display: grid;
  gap: var(--space-3xs);
  min-width: 0;
}

.sponsor-ticket__name {
  font-weight: 600;
}

.sponsor-ticket__shop {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
}

.sponsor-ticket__rule {
  color: var(--color-primary);
  font-size: var(--text-2xs);
  font-weight: 600;
}

.sponsor-ticket .chip--sample {
  flex: none;
  margin-inline-start: auto;
}

/* ═══ UX-14 public-information utilities ════════════════════════════════════
 * News, Notifications and Weather are focused utilities: no bottom tab bar, no
 * sticky action, and no shell geometry changes outside this explicit modifier. */
.app-shell--public-info {
  padding-block-end: max(var(--space-md), env(safe-area-inset-bottom, 0px));
}

.ux14-public-info-main {
  flex: 1;
}

.screen-stage > .ux14-truth-banner {
  margin-inline: var(--space-gutter);
  margin-block: var(--space-xs) 0;
  padding: var(--space-sm) var(--space-md) var(--space-sm);
  border-inline-start: 0.25rem solid var(--color-primary);
  border-radius: var(--radius-control);
  background: var(--color-surface);
  color: var(--color-ink);
  font-size: var(--text-base);
  font-weight: 700;
  line-height: var(--lh-body);
}

.ux14-notifications-empty {
  display: grid;
  margin: var(--space-section) var(--space-gutter);
  padding: clamp(var(--space-lg), 9vw, var(--space-xl)) var(--space-md);
  gap: var(--space-sm);
  justify-items: center;
  border: var(--rule-hair) dashed var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  text-align: center;
}

.ux14-notifications-empty h2 {
  font-family: var(--font-display);
  font-size: var(--text-md);
  line-height: var(--lh-title);
}

.ux14-notifications-empty p {
  max-width: 26rem;
  color: var(--color-ink);
  font-size: var(--text-base);
  line-height: var(--lh-body);
}

.ux14-notifications-cta {
  width: min(100%, 14rem);
  margin-block-start: var(--space-xs);
}

.ux14-weather-grid {
  display: grid;
  margin-inline: var(--space-gutter);
  padding-block: var(--space-section) var(--space-md);
  gap: var(--space-sm);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr));
}

.ux14-weather-card {
  display: grid;
  min-width: 0;
  min-height: 10rem;
  padding: var(--space-md);
  gap: var(--space-sm);
  align-content: space-between;
  border-radius: var(--radius-card);
  background: var(--color-surface);
  box-shadow: var(--shadow-control);
}

.ux14-weather-card__head {
  display: flex;
  min-width: 0;
  gap: var(--space-xs);
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
}

.ux14-weather-card h2 {
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: 700;
  line-height: var(--lh-title);
}

.ux14-sample-label {
  display: inline-flex;
  min-height: 1.75rem;
  padding-inline: var(--space-xs);
  align-items: center;
  border: var(--rule-hair) solid var(--color-ink-muted);
  border-radius: var(--radius-pill);
  color: var(--color-ink);
  font-size: var(--text-xs);
  font-weight: 700;
  line-height: 1.2;
}

.ux14-weather-value {
  min-width: 0;
  overflow-wrap: anywhere;
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 14vw, 3.75rem);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.ux14-weather-value--pm {
  font-size: clamp(1.65rem, 8vw, 2.25rem);
  line-height: var(--lh-title);
}

.ux14-weather-value--pm span {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 600;
  white-space: nowrap;
}

.ux14-weather-future {
  margin: 0 var(--space-gutter) var(--space-lg);
  color: var(--color-ink);
  font-size: var(--text-sm);
  line-height: var(--lh-body);
  text-align: center;
}

.ux14-news-panel {
  padding-block-start: var(--space-md);
}

.app-shell:has(#news-title) .page-head {
  padding-block: var(--space-sm);
}

.app-shell:has(#news-title) .screen-stage > :last-child {
  padding-block-end: var(--space-xs);
}

.ux14-news-count,
.ux14-news-noscript {
  padding-inline: var(--space-gutter);
  color: var(--color-ink);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: var(--lh-body);
}

.ux14-news-count {
  padding-block-end: var(--space-xs);
}

.ux14-news-feed {
  gap: var(--space-sm);
}

.ux14-news-row .ux14-news-link {
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
  padding: var(--space-sm);
}

.ux14-news-row--activity .ux14-news-link {
  padding: 0;
  gap: 0;
  overflow: clip;
  border-radius: var(--radius-tile);
}

.ux14-news-row--activity .news-body {
  padding: var(--space-md);
}

/* At larger Easy type, keep headline and provenance before the decorative photo.
   The whole card is one article link; the image carries no separate control. */
html[data-easy="on"] .ux14-news-row--activity .news-body {
  grid-row: 1;
  padding: var(--space-sm);
}

html[data-easy="on"] .ux14-news-photo {
  grid-row: 2;
}

.ux14-news-photo {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--color-surface-sunken);
}

.ux14-news-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.ux14-news-photo > span {
  position: absolute;
  inset-inline-start: var(--space-sm);
  inset-block-end: var(--space-sm);
  padding: .25rem .6rem;
  border-radius: var(--radius-pill);
  background: var(--color-canvas);
  color: var(--color-ink);
  font-size: var(--text-xs);
}

.ux14-news-schedule {
  color: var(--color-primary);
  font-size: var(--text-sm);
  font-weight: 700;
  line-height: var(--lh-body);
}

.article-facts > h2 {
  margin-block-end: var(--space-sm);
  font-family: var(--font-display);
  font-size: var(--text-md);
}

.profile-shortcuts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  padding: var(--space-md) var(--space-gutter) 0;
}

.profile-shortcuts > a {
  flex: 1 1 8rem;
  min-height: var(--size-control);
}

#settings-title,
#saved-title {
  scroll-margin-block-start: var(--space-lg);
}

.lang-choice small {
  display: block;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 400;
}

.ux14-news-meta {
  display: grid;
  margin-block-start: var(--space-2xs);
  gap: var(--space-3xs);
  color: var(--color-ink);
  font-size: var(--text-xs);
  line-height: var(--lh-body);
}

.ux14-news-preview {
  width: max-content;
  max-width: 100%;
  padding: var(--space-3xs) var(--space-xs);
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
  font-weight: 700;
}

.ux14-news-publisher {
  overflow-wrap: anywhere;
  font-weight: 600;
}

.ux14-news-status {
  margin-block-start: var(--space-2xs);
  color: var(--color-ink);
  font-size: var(--text-sm);
  font-weight: 700;
  line-height: var(--lh-body);
}

@media (prefers-reduced-motion: no-preference) {
  html:not([data-entrance="off"]) .ux14-notifications-empty,
  html:not([data-entrance="off"]) .ux14-weather-card {
    animation: enter-fade-up 380ms var(--ease-out) 160ms backwards;
  }

  html:not([data-entrance="off"]) .ux14-weather-card:nth-child(2) {
    animation-delay: 220ms;
  }
}

/* ADR-023: the language row on ฉัน carries the same picker as onboard, wrapped under the copy. */
.setting-row--lang { flex-wrap: wrap; }
.setting-row--lang .onboard-langs--row { flex: 1 0 100%; margin-top: 8px; }

/* TOUR-HOME-TABLET: lists use the available width while ordered detail flows remain linear. */
@media (min-width: 700px) {
  .route-groups { width: 100%; }
  .place-list { width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-md); }
  .place-list > .place-row { min-width: 0; content-visibility: visible; }
  html .place-list .place-row__link {
    display: flex;
    flex-direction: column;
    height: 100%;
    aspect-ratio: auto;
    overflow: hidden;
    border-radius: var(--radius-card);
    background: var(--color-surface);
    box-shadow: var(--shadow-control);
  }
  html .place-list .place-row__photo {
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 10;
    min-height: 0;
    border-radius: 0;
  }
  html .place-list .place-row__body {
    flex: 1;
    align-self: stretch;
    margin: 0;
    padding: var(--space-sm);
    border-radius: 0;
    box-shadow: none;
  }
  html .place-list .place-row__name { white-space: normal; overflow-wrap: anywhere; }
  html .place-list .save-heart--row {
    inset-block-start: var(--space-xs);
    inset-inline-end: var(--space-xs);
    translate: none;
    transform: none;
  }
  .route-group__heading { flex-wrap: wrap; gap: var(--space-xs); }
  .entity-detail > :not(.screen-stage) { width: 100%; max-width: 70rem; margin-inline: auto; }
}
@media (min-width: 1100px) {
  .place-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (min-width: 700px) {
  html .place-hero { width: 100%; aspect-ratio: auto; height: clamp(18rem, 42vw, 26rem); }
  .route-group > .place-list { width: 100%; }
}

@media (min-width: 700px) {
  .passport-cover-photo { max-height: 16rem; }
  .passport-cover-photo img { max-height: 16rem; object-fit: cover; }
  .saved .card-rail > .place-card,
  .saved .card-rail > .place-card:first-child {
    flex: 0 0 calc((100% - (2 * var(--space-xs))) / 3);
    width: calc((100% - (2 * var(--space-xs))) / 3);
    min-width: 0;
  }
  .saved .place-link { display: flex; flex-direction: column; height: 100%; }
  .saved .place-photo { width: 100%; aspect-ratio: 4 / 3; }
  .saved .place-body { width: 100%; padding: var(--space-sm); }
  .ux14-news-feed:not([hidden]) { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ux14-news-row:first-child { grid-column: 1 / -1; }
  .ux14-news-photo,
  .article-photo:not(.article-photo--notice) {
    aspect-ratio: auto;
    height: clamp(18rem, 35vw, 23rem);
  }
}

@media (min-width: 700px) {
  .lane-photo { aspect-ratio: auto; height: clamp(18rem, 42vw, 26rem); }
  .lane-photo img { width: 100%; height: 100%; object-fit: cover; }
  .onboard-photo { aspect-ratio: auto; height: clamp(14rem, 32vh, 22rem); }
}

/* Option B: a single photo surface and its real thumbnail choices. */
html body .carousel-track[data-carousel-track] {
  display: flex;
  align-items: stretch;
  gap: 0;
  width: 100%;
  min-width: 0;
  max-width: 100%;
  margin-inline: 0;
  padding: 0;
  overflow-x: auto;
  overflow-y: hidden;
  list-style: none;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: 0;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  touch-action: pan-x pan-y pinch-zoom;
  transform: none;
  transition: none;
  will-change: auto;
}
html body .carousel-track[data-carousel-track]::-webkit-scrollbar { display: none; }
html body .carousel-track[data-carousel-track] > .carousel-slide {
  flex: 0 0 100%;
  width: 100%;
  min-width: 0;
  max-width: none;
  margin: 0;
  grid-column: auto;
  grid-row: auto;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}
.carousel-slide > [data-hero-pager-frame] { width: 100%; max-width: 100%; }
html body .carousel-track[data-carousel-track] > .carousel-slide[hidden],
.carousel-controls[hidden], .carousel-counter[hidden], [data-hero-pager-counter][hidden],
.carousel-thumbnail__image[hidden] { display: none; }
html body .carousel-track--dragging[data-carousel-track] { scroll-snap-type: none; cursor: grabbing; user-select: none; }

.carousel-controls--thumbnails {
  min-width: 0;
  margin: 0;
  padding: .5rem var(--space-md) .625rem;
  color: var(--color-ink);
}
.carousel-thumbnails {
  position: relative;
  display: flex;
  gap: .5rem;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--color-border) transparent;
  padding: .125rem;
  touch-action: pan-x pan-y pinch-zoom;
}
.carousel-thumbnail {
  position: relative;
  flex: 0 0 clamp(3.5rem, 17vw, 4.5rem);
  width: clamp(3.5rem, 17vw, 4.5rem);
  height: 3.25rem;
  min-width: 44px;
  min-height: 44px;
  padding: 2px;
  overflow: hidden;
  border: 2px solid transparent;
  border-radius: 12px;
  background: var(--color-surface);
  color: var(--color-ink);
  font: inherit;
  cursor: pointer;
  transition: border-color 160ms ease, transform 140ms ease, opacity 160ms ease;
}
.carousel-thumbnail[aria-current="true"] { border-color: var(--color-primary); }
.carousel-thumbnail:active { transform: scale(.96); }
.carousel-thumbnail:disabled { opacity: .6; cursor: default; }
.carousel-thumbnail__image {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 7px;
  object-fit: cover;
}
.carousel-thumbnail__fallback {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-variant-numeric: tabular-nums;
}
.carousel-counter { font-variant-numeric: tabular-nums; }
html body .carousel-track:focus-visible,
html body .carousel-thumbnail:focus-visible { outline: 3px solid var(--color-focus); outline-offset: -3px; }
@media (hover: hover) and (pointer: fine) {
  .carousel-thumbnail:hover:not(:disabled) { border-color: var(--color-primary); transform: translateY(-1px); }
}

html body .place-hero:has(.carousel-track) { touch-action: pan-x pan-y pinch-zoom; }
html body .hero-pager__track.carousel-track[data-carousel-track] { height: 100%; }
.hero-pager__track.carousel-track > .carousel-slide,
.hero-pager__track.carousel-track > .carousel-slide > .hero-pager__frame { height: 100%; }
.hero-pager__track.carousel-track > .carousel-slide > .hero-pager__frame { position: relative; }

/* Same gallery controls in the full-screen viewer; zoom/pan stay viewer-owned. */
html body .photo-viewer { grid-template-rows: auto minmax(0, 1fr) auto auto; }
html body .photo-viewer .carousel-track[data-carousel-track] { touch-action: pan-x pinch-zoom; }
html body .photo-viewer .carousel-track > .carousel-slide { display: grid; place-items: center; height: 100%; }
html body .photo-viewer .carousel-controls { padding: .375rem var(--space-sm); margin: 0; }
html body .photo-viewer .carousel-thumbnails { justify-content: safe center; }
html body .photo-viewer .carousel-thumbnail { background: var(--color-surface); }
html body .photo-viewer .photo-viewer__image { max-height: calc(100dvh - 13rem - env(safe-area-inset-top) - env(safe-area-inset-bottom)); }
html body .photo-viewer.is-zoomed .carousel-track[data-carousel-track] { overflow: hidden; touch-action: none; }

html:is([data-easy="on"], [data-ambient="off"], [data-season="royal"], [data-season="mourning"]) .carousel-thumbnail {
  transition: none;
  transform: none;
}
@media (prefers-reduced-motion: reduce) {
  html body .carousel-track[data-carousel-track] { scroll-behavior: auto; }
  .carousel-thumbnail { transition: none; transform: none; }
}
