/* ==========================================================================
   RoomCompare — Components
   Foundation set (Phase 1): buttons, fields, cards, the ledger table,
   dialog, toast, status. Feature components extend this in later phases.
   ========================================================================== */

/* --- Buttons -------------------------------------------------------------
   Radius 4px: inputs and buttons share a role, so they share a radius.
   No shadows, no gradients. */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  min-height: 40px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  line-height: 1.2;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color var(--duration-fast) var(--ease),
              border-color var(--duration-fast) var(--ease),
              color var(--duration-fast) var(--ease);
  /* Every style below draws its own pressed state, so the browser's grey
     tap flash would only sit on top of it. */
  -webkit-tap-highlight-color: transparent;
}

.btn--primary {
  background: var(--accent);
  color: var(--surface);
}

.btn--secondary {
  background: var(--surface);
  border-color: var(--rule);
  color: var(--ink);
}

.btn--quiet {
  padding-inline: var(--space-2);
  background: none;
  color: var(--accent);
}

.btn--danger {
  background: var(--surface);
  border-color: var(--rule);
  color: var(--alert);
}

/* Pressed and hovered look the same: both say "this is what you are about
   to act on". Pressed answers a finger the moment it lands; hover exists
   only where a pointer can hover (see base.css), because a touch screen
   would keep it after the tap.

   Colour is restated in every state: base.css `a:hover` beats
   `.btn--primary` on a link button, and would paint the text the same
   shade as this background. `:not(:disabled)` lifts each rule above it. */
.btn--primary:active:not(:disabled) {
  background: var(--accent-press);
  color: var(--surface);
}

.btn--secondary:active:not(:disabled) {
  background: var(--accent-tint);
  border-color: var(--accent);
  color: var(--accent);
}

.btn--quiet:active:not(:disabled) {
  background: var(--accent-tint);
  color: var(--accent-press);
}

.btn--danger:active:not(:disabled) {
  background: var(--alert-bg);
  border-color: var(--alert);
  color: var(--alert);
}

@media (hover: hover) {
  .btn--primary:hover:not(:disabled) {
    background: var(--accent-press);
    color: var(--surface);
  }

  .btn--secondary:hover:not(:disabled) {
    border-color: var(--accent);
    color: var(--accent);
  }

  .btn--quiet:hover:not(:disabled) {
    background: var(--accent-tint);
    color: var(--accent-press);
  }

  .btn--danger:hover:not(:disabled) {
    background: var(--alert-bg);
    border-color: var(--alert);
    color: var(--alert);
  }
}

.btn--small {
  min-height: 32px;
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-xs);
}

/* --- Fields --------------------------------------------------------------- */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.field__label {
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
}

.field__control {
  width: 100%;
  padding: var(--space-2) var(--space-3);
  min-height: 40px;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  transition: border-color var(--duration-fast) var(--ease);
}

@media (hover: hover) {
  .field__control:hover { border-color: var(--muted); }
}

.field__control:focus { border-color: var(--accent); }

.field__hint {
  color: var(--muted);
  font-size: var(--text-xs);
}

/* --- Field guidance (the survey form) ------------------------------------
   Every survey field says what it needs before it is filled in: a helper
   line under the control, a live count where there is a character limit,
   and an ⓘ beside the label for the details. */

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

.field__support {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.field__count {
  flex-shrink: 0;
  color: var(--muted);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums lining-nums;
}

/* The ⓘ that opens a field's How to fill panel, and the ? beside the survey
   form's title that opens the Survey guide. Quiet icons beside what they
   explain; on a touch screen each takes a 44px hit area (touch rules). */
.field__info,
.page-head__help {
  display: inline-grid;
  place-items: center;
  padding: var(--space-1);
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--muted);
  vertical-align: middle;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color var(--duration-fast) var(--ease),
              color var(--duration-fast) var(--ease);
}

.field__info-icon,
.page-head__help-icon { display: block; }

.field__info:active,
.page-head__help:active {
  background: var(--accent-tint);
  color: var(--accent-press);
}

@media (hover: hover) {
  .field__info:hover,
  .page-head__help:hover {
    background: var(--accent-tint);
    color: var(--accent);
  }
}

.choice-group__legend .field__info { margin-left: var(--space-1); }

/* Under a group's options, the helper keeps the step the legend has above. */
.choice-group__hint { margin-top: var(--space-3); }

/* What the chosen score means. Empty until a score is chosen, but kept in
   the page rather than hidden, so a screen reader announces each change. */
.rubric {
  margin-top: var(--space-2);
  padding-left: var(--space-3);
  border-left: 2px solid var(--accent);
  color: var(--ink);
  font-size: var(--text-xs);
  max-width: var(--measure);
}

.rubric:empty {
  margin: 0;
  padding: 0;
  border: 0;
}

/* An input carrying a currency or unit affix. The wrapper takes the border so
   prefix and field read as one control. */
.field__group {
  display: flex;
  align-items: stretch;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
}

@media (hover: hover) {
  .field__group:hover { border-color: var(--muted); }
}

/* Prefix and input are one control, so the focus ring belongs to the wrapper.
   The indicator is not removed, it moves outward — the only place an input
   drops its own outline, and only because something larger replaces it. */
.field__group:focus-within {
  border-color: var(--accent);
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: var(--focus-offset);
}

.field__group .field__control:focus-visible { outline: none; }

.field__affix {
  display: flex;
  align-items: center;
  padding-inline: var(--space-3);
  border-right: 1px solid var(--rule);
  background: var(--paper);
  color: var(--muted);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

/* Higher specificity than .field__control, so the wrapper owns the chrome. */
.field__group .field__control {
  border: none;
  border-radius: 0;
  background: transparent;
}

.field__group .field__control:hover,
.field__group .field__control:focus { border-color: transparent; }

.field[data-invalid="true"] .field__group { border-color: var(--alert); }

/* Errors name the fix; they do not apologize. */
.field__error {
  color: var(--alert);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
}

.field[data-invalid="true"] .field__control { border-color: var(--alert); }

/* Checkbox and radio groups are real fieldsets. */
.choice-group {
  margin: 0;
  padding: 0;
  border: none;
}

.choice-group__legend {
  padding: 0;
  margin-bottom: var(--space-3);
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
}

.choice-group__options {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--space-2);
}

.choice {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease),
              background-color var(--duration-fast) var(--ease);
  -webkit-tap-highlight-color: transparent;
}

@media (hover: hover) {
  .choice:hover { border-color: var(--muted); }
}

/* Answers the finger before the tick lands. */
.choice:active { background: var(--accent-tint); }

.choice:has(input:checked) {
  background: var(--accent-tint);
  border-color: var(--accent);
}

.choice input { accent-color: var(--accent); }

/* --- Cards ---------------------------------------------------------------
   Separation by a 1px rule on --surface. No shadow. */

.card {
  background: var(--surface);
  border: var(--border-hair);
  border-radius: var(--radius-md);
  padding: var(--space-4);
}

.card__title {
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.panel {
  background: var(--surface);
  border: var(--border-hair);
  border-radius: var(--radius-md);
  padding: var(--space-5);
}

/* --- Status --------------------------------------------------------------
   A word plus a rule weight, not a coloured pill. */

.status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--muted);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

.status::before {
  content: "";
  width: 14px;
  border-top: 2px solid currentColor;
}

.status[data-status="published"] { color: var(--accent); }

/* --- Key facts ----------------------------------------------------------- */

.facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
}

/* The key facts summarise the page, so they sit with its header; the first
   section starts after a gap rather than touching them (it was 0px — only
   `.section + .section` was spaced). */
.facts + .section { margin-top: var(--space-5); }

.facts__label {
  display: block;
  color: var(--muted);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

.facts__value {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums lining-nums;
}

/* --- Ledger (the comparison table) ---------------------------------------
   The origin of the design system. Hairline grid, square cells, tabular
   figures, sticky criteria column and header. Nothing is truncated. */

.ledger-wrap {
  overflow-x: auto;
  border: var(--border-hair);
  border-radius: var(--radius-md);
  background: var(--surface);
}

.ledger {
  min-width: 640px;
  border-radius: var(--radius-none);
}

.ledger th,
.ledger td {
  padding: var(--space-3);
  border-bottom: var(--border-hair);
  border-right: var(--border-hair);
  text-align: left;
  vertical-align: top;
}

.ledger th:last-child,
.ledger td:last-child { border-right: none; }

.ledger thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--surface);
  border-bottom: var(--border-group);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

/* The criterion column is the table's spine — chrome, not data. Tinting it
   to --paper sets it apart from the white value cells, and keeps it legible
   while the kos columns scroll underneath it. */
.ledger__criterion {
  position: sticky;
  left: 0;
  z-index: 1;
  width: 200px;
  min-width: 160px;
  background: var(--paper);
  color: var(--muted);
  font-weight: var(--weight-regular);
}

/* Beats `.ledger thead th`, which would otherwise repaint the corner white. */
.ledger thead .ledger__criterion {
  z-index: 3;
  background: var(--paper);
}

/* A group boundary means more than a row boundary, so it is banded rather
   than merely ruled. It carries the same paper tone as the spine, so header
   and spine join into one continuous frame around the values, and it steps
   up to body size because a section name outranks a criterion name. */
.ledger__group th {
  padding-block: var(--space-3);
  border-top: var(--border-group);
  border-bottom: var(--border-group);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-narrow);
  font-size: var(--text-base);
  font-weight: var(--weight-bold);
}

/* Rows whose values differ are marked, not judged: the label steps up from
   muted to ink semibold, and identical rows stay quiet. The weight change
   carries the cue on its own — there is deliberately no bar or inset rule
   on the cell's edge. */
.ledger tr[data-differs="true"] .ledger__criterion {
  color: var(--ink);
  font-weight: var(--weight-semibold);
}

.ledger__kos {
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  color: var(--ink);
}

/* Presence carried by the glyph; colour only reinforces it. */
.ledger__check { color: var(--accent); font-weight: var(--weight-semibold); }
.ledger__absent { color: var(--muted); }

/* The best value stated quietly. A fact, not a badge. */
.ledger__best { color: var(--accent); font-weight: var(--weight-semibold); }

/* The one orchestrated moment: columns resolving as the comparison opens.
   Each column carries its index, so they arrive left to right rather than
   all at once — it reads as the comparison being assembled. The flag sits on
   the wrapper around every section, so all of them resolve together. */
@media (prefers-reduced-motion: no-preference) {
  [data-revealing="true"] .ledger thead th[style*="--column"],
  [data-revealing="true"] .ledger td[style*="--column"] {
    animation: ledger-in var(--duration-base) var(--ease) both;
    animation-delay: calc(var(--column, 0) * 120ms);
  }
}

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

/* --- Sectioned ledger -----------------------------------------------------
   The comparison as one card per category, so Room and Surroundings read as
   separate things rather than bands inside one long grid. Every table shares
   one column set (fixed layout plus the same colgroup), so values sit on the
   same vertical lines from the first section to the last. */

.ledger-sections {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  overflow-x: auto;
}

/* `clip` rounds the corners without becoming a scroll container, which
   would stop the sticky criteria column; `hidden` is the fallback. */
.ledger-section {
  min-width: 640px;
  background: var(--surface);
  border: var(--border-hair);
  border-radius: var(--radius-md);
  overflow: hidden;
  overflow: clip;
}

.ledger--sectioned { table-layout: fixed; }

.ledger__col-criterion { width: 200px; }

/* The card's border closes each section; a last-row rule would double it. */
.ledger--sectioned tbody tr:last-child > * { border-bottom: none; }

/* The kos names, shown once above the first section. Its card is the
   frame, so the heavy group rule under the names would only double it. */
.ledger-section--columns .ledger thead th { border-bottom: none; }

/* A category outranks a criterion, and a criterion outranks a value: the
   section name steps up to --text-md in bold, on --accent-tint so the band
   reads as a different kind of thing from the paper criteria column, above
   the 2px group rule the ledger uses for a boundary that means more than a
   row. */
.ledger-section__title {
  padding: var(--space-3) var(--space-4);
  background: var(--accent-tint);
  border-bottom: var(--border-group);
  color: var(--ink);
  font-size: var(--text-md);
  font-weight: var(--weight-bold);
  line-height: var(--leading-snug);
}

/* Notes are prose, not figures. They keep the writer's own line breaks, as
   on the survey page, and wrap wherever they must — a pasted link with no
   spaces would otherwise run out of its column. */
.ledger__prose .ledger__cell-value {
  display: block;
  line-height: var(--leading-body);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* --- Empty state ---------------------------------------------------------
   An invitation to act, not a shrug. */

.empty {
  padding: var(--space-6) var(--space-5);
  background: var(--surface);
  border: 1px dashed var(--rule);
  border-radius: var(--radius-md);
  text-align: left;
}

.empty__title {
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
}

.empty__body {
  margin-top: var(--space-2);
  margin-bottom: var(--space-4);
  color: var(--muted);
}

/* --- Dialog --------------------------------------------------------------
   One of only two places a shadow appears. */

.dialog {
  width: min(440px, calc(100vw - var(--space-6)));
  padding: var(--space-5);
  border: var(--border-hair);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-dialog);
  /* A <dialog> takes the browser's own text colour, not the page's, so its
     title rendered black beside ink everywhere else. */
  color: var(--ink);
}

.dialog::backdrop { background: rgba(22, 36, 31, 0.4); }

/* A dialog arrives rather than appearing: a short rise and fade says it
   sits above the page, and where it came from. Phones get a sheet instead
   (see the narrow-screen rules below). */
@media (prefers-reduced-motion: no-preference) {
  .dialog[open],
  .lightbox-dialog[open] {
    animation: dialog-in var(--duration-base) var(--ease-out) both;
  }

  .dialog[open]::backdrop,
  .lightbox-dialog[open]::backdrop {
    animation: backdrop-in var(--duration-base) var(--ease) both;
  }
}

@keyframes dialog-in {
  from { opacity: 0; transform: translateY(var(--space-2)) scale(0.98); }
  to   { opacity: 1; transform: none; }
}

@keyframes backdrop-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.dialog__title {
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
}

.dialog__body {
  margin: var(--space-3) 0 var(--space-5);
  color: var(--muted);
}

.dialog__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
}

/* --- Toast ---------------------------------------------------------------
   The prototype gives no feedback at all after save, delete or star. */

.toast-region {
  position: fixed;
  left: 50%;
  bottom: var(--space-5);
  z-index: 70;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.toast {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--ink);
  border-radius: var(--radius-sm);
  color: var(--paper);
  font-size: var(--text-base);
}

.toast .btn--quiet { color: var(--paper); text-decoration: underline; }
.toast .btn--quiet:active:not(:disabled) { background: rgba(255, 255, 255, 0.12); color: var(--paper); }

@media (hover: hover) {
  .toast .btn--quiet:hover:not(:disabled) { background: rgba(255, 255, 255, 0.12); color: var(--paper); }
}

/* --- Banner --------------------------------------------------------------- */

.banner {
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--rule);
  border-left: 2px solid var(--muted);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

/* The notice region collapses to nothing when there is no notice. */
.column--notice {
  padding-top: var(--space-5);
  padding-bottom: 0;
}

.column--notice:empty { display: none; }

.banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
}

.banner[data-tone="alert"] {
  border-left-color: var(--alert);
  background: var(--alert-bg);
  color: var(--alert);
}

/* --- Skeleton ------------------------------------------------------------- */

.skeleton {
  background: var(--rule);
  border-radius: var(--radius-sm);
  height: 1em;
}

@media (prefers-reduced-motion: no-preference) {
  .skeleton { animation: skeleton-pulse 1.4s ease-in-out infinite; }
}

@keyframes skeleton-pulse {
  50% { opacity: 0.5; }
}

/* --- Survey card ---------------------------------------------------------
   One card serves both lists; the community variant swaps its actions.
   Cards in a row share a height and pin their actions to the bottom, so the
   action rows line up however long a kos name runs. */

.survey-card {
  height: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-3);
  transition: border-color var(--duration-fast) var(--ease);
}

@media (hover: hover) {
  .survey-card:hover { border-color: var(--muted); }
}

.survey-card:focus-within { border-color: var(--accent); }

.survey-card__body {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: 0 var(--space-1) var(--space-1);
}

.survey-card__link {
  color: inherit;
  text-decoration: none;
}

/* The whole card is clickable via the title, without nesting interactive
   elements inside a link. */
.survey-card__link::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius-md);
}

.survey-card { position: relative; }

/* Buttons must stay above the title's click target. */
.survey-card__actions { position: relative; z-index: 1; }

@media (hover: hover) {
  .survey-card__link:hover { text-decoration: underline; }
}

/* Location and status share a line, so a two-line name never squeezes them. */
.survey-card__meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: 20px;
}

.survey-card__facts {
  display: flex;
  gap: var(--space-5);
  margin-top: var(--space-2);
  padding-top: var(--space-3);
  border-top: var(--border-hair);
}

.survey-card__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: auto;
  padding-top: var(--space-4);
}

.card__fact-label {
  display: block;
  color: var(--muted);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

.card__fact-value {
  display: block;
  margin-top: var(--space-1);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
}

/* Stand-in tile for a survey with no photos. Never a broken image. */
.thumb {
  width: 100%;
  height: 120px;
  border-radius: var(--radius-sm);
  object-fit: cover;
}

.thumb--empty {
  display: grid;
  place-items: center;
  background: var(--accent-tint);
  color: var(--accent);
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  border: 1px solid var(--rule);
}

/* --- Dashboard hero ------------------------------------------------------
   Opens on the user's own kos already side by side, in the ledger's type. */

.hero {
  display: grid;
  grid-template-columns: 1fr 1.2fr;
  gap: var(--space-6);
  align-items: center;
  padding: var(--space-6) var(--space-5);
  background: var(--surface);
  border: var(--border-hair);
  border-radius: var(--radius-md);
}

/* Grid children default to min-width:auto, so a wide child would widen the
   whole page. Both tracks opt out. */
.hero > * { min-width: 0; }

.hero__text { display: flex; flex-direction: column; gap: var(--space-4); }
.hero__text h2 { font-size: var(--text-xl); letter-spacing: var(--tracking-tight); }

/* The way into the full comparison, above the table it continues. */
.hero__actions {
  display: flex;
  justify-content: flex-end;
  margin-bottom: var(--space-4);
}

/* --- Dashboard strip -------------------------------------------------------
   The comparison in miniature, sectioned like the full one: the kos names
   once, then one card per criterion. Every table has the same colgroup and
   a fixed layout, so values sit on the same vertical lines in every card.

   It never scrolls inside itself. It used to scroll sideways on a phone,
   hiding the third kos behind a scrollbar: a criterion column plus three
   kos columns does not fit 293px. The criterion now heads its card, which
   gives the kos columns the whole width, and below 500px they stack. */

.strip-sections {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* `clip` rounds the corners without becoming a scroll container. */
.strip-section {
  background: var(--surface);
  border: var(--border-hair);
  border-radius: var(--radius-md);
  overflow: hidden;
  overflow: clip;
}

.strip {
  border-collapse: collapse;
  table-layout: fixed;
}

.strip th,
.strip td {
  padding: var(--space-2) var(--space-3);
  text-align: left;
  vertical-align: top;
  font-variant-numeric: tabular-nums lining-nums;
}

/* A long kos name wraps inside its column rather than widening it. */
.strip thead th {
  overflow-wrap: anywhere;
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

/* A criterion outranks a value, so its name heads the card: bold, on
   --accent-tint, above the 2px group rule — the full comparison's section
   band, at the strip's body size. */
.strip-section__title {
  padding: var(--space-2) var(--space-3);
  background: var(--accent-tint);
  border-bottom: var(--border-group);
  color: var(--ink);
  font-size: var(--text-base);
  font-weight: var(--weight-bold);
  line-height: var(--leading-snug);
}

.strip__best { color: var(--accent); font-weight: var(--weight-semibold); }

/* The kos name repeated inside each value, for the stacked layout only; the
   column headers carry it everywhere else. */
.strip__kos { display: none; }

/* Below 500px, three columns would split "Rp 1.850.000" across lines. Each
   card lists its kos instead, the value at the row's end — the full
   comparison's phone layout. The names card goes, since every row now
   carries its kos. */
@media (max-width: 500px) {
  .strip-section--names { display: none; }

  .strip,
  .strip tbody,
  .strip tr { display: block; }

  .strip td {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    border-bottom: var(--border-hair);
  }

  .strip td:last-child { border-bottom: none; }

  .strip__kos {
    display: block;
    overflow-wrap: anywhere;
    color: var(--muted);
    font-family: var(--font-narrow);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
  }

  .strip__value { flex-shrink: 0; }
}

/* --- Listing rows --------------------------------------------------------- */

.listing {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding-bottom: var(--space-3);
  border-bottom: var(--border-hair);
  font-variant-numeric: tabular-nums lining-nums;
}

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

.listing__title {
  font-weight: var(--weight-semibold);
  color: var(--ink);
  text-decoration: none;
}

@media (hover: hover) {
  .listing__title:hover { text-decoration: underline; }
}

/* A row whose only control is its title opens from anywhere on the row, as
   a survey card does: the name alone was a 17px-tall target. Scoped to
   links, because the picker's rows carry a button of their own. */
.listing { position: relative; }

a.listing__title::after {
  content: "";
  position: absolute;
  inset: 0;
}

/* --- Definition lists (detail view) --------------------------------------- */

.defn-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-4);
  margin: 0 0 var(--space-4);
}

.defn__label {
  color: var(--muted);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

.defn__value {
  margin: var(--space-1) 0 0;
  font-weight: var(--weight-medium);
  font-variant-numeric: tabular-nums lining-nums;
}

/* --- Checklist -----------------------------------------------------------
   The full list with present and absent both marked: "no AC" is information,
   so it is shown rather than omitted. */

.checklist {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--space-2) var(--space-4);
}

.checklist__item {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  color: var(--muted);
}

.checklist__item[data-present="true"] { color: var(--ink); }

.checklist__mark {
  width: 1em;
  color: var(--muted);
  font-weight: var(--weight-semibold);
}

.checklist__item[data-present="true"] .checklist__mark { color: var(--accent); }

.notes {
  max-width: var(--measure-notes);
  white-space: pre-wrap;
}

/* --- Search row ----------------------------------------------------------- */

.search-row {
  max-width: 420px;
  margin-bottom: var(--space-5);
}

/* --- Community filters ---------------------------------------------------- */

.filter-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
  gap: var(--space-4);
  align-items: end;
}

.filter-grid__star { min-height: 40px; }

/* --- Survey form ---------------------------------------------------------
   Sections are numbered because this form is genuinely a sequence, and the
   requirement numbers it that way. The only numbered markers in the product. */

.survey-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.form-section {
  padding: var(--space-5);
  background: var(--surface);
  border: var(--border-hair);
  border-radius: var(--radius-md);
}

.form-section__head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.form-section__number {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-sm);
  background: var(--accent-tint);
  color: var(--accent);
  font-family: var(--font-narrow);
  font-weight: var(--weight-bold);
  font-size: var(--text-xs);
}

.form-section__guide {
  margin: var(--space-3) 0 var(--space-5);
  padding-left: var(--space-3);
  border-left: 2px solid var(--rule);
  color: var(--muted);
  max-width: var(--measure);
}

.form-section__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-4);
}

.form-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  padding-bottom: var(--space-5);
}

/* --- Narrow screens ------------------------------------------------------- */

@media (max-width: 900px) {
  .hero { grid-template-columns: 1fr; }
}

@media (max-width: 640px) {
  .form-actions { flex-direction: column-reverse; }
  .form-actions .btn { width: 100%; }
  .survey-card__actions .btn { flex: 1; }
}

/* --- Media uploader ------------------------------------------------------
   Files are stored as soon as they are chosen, so the grid below the picker
   shows what is already saved rather than a queue waiting on submit. */

.uploader {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.uploader__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.uploader__count {
  color: var(--muted);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums lining-nums;
}

.uploader__drop {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-5) var(--space-4);
  width: 100%;
  background: var(--surface);
  border: 1px dashed var(--rule);
  border-radius: var(--radius-md);
  color: var(--ink);
  text-align: center;
  transition: border-color var(--duration-fast) var(--ease),
              background-color var(--duration-fast) var(--ease);
}

.uploader__drop:active:not(:disabled) {
  border-color: var(--accent);
  background: var(--accent-tint);
}

@media (hover: hover) {
  .uploader__drop:hover:not(:disabled) {
    border-color: var(--accent);
    background: var(--accent-tint);
  }
}

.uploader__drop:disabled {
  color: var(--muted);
  cursor: not-allowed;
}

.uploader__drop-main { font-weight: var(--weight-semibold); }

.uploader__drop-hint {
  color: var(--muted);
  font-size: var(--text-xs);
}

/* Dragging over any part of the field, not just the button. */
.uploader[data-dragging="true"] .uploader__drop {
  border-color: var(--accent);
  border-style: solid;
  background: var(--accent-tint);
}

.uploader__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: var(--space-3);
}

.uploader__grid:empty { display: none; }

.uploader__item {
  position: relative;
  border: var(--border-hair);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--paper);
}

/* A photo this device does not hold keeps a photo's footprint, so the grid
   does not jump, and says where it is instead of showing a broken image. */
.uploader__thumb,
.uploader__elsewhere {
  width: 100%;
  height: 96px;
}

.uploader__thumb { object-fit: cover; }

.uploader__elsewhere {
  display: block;
  padding: var(--space-2);
  color: var(--muted);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

.uploader__item--file {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3);
}

.uploader__file { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }

.uploader__file-name {
  font-weight: var(--weight-medium);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.uploader__remove {
  position: absolute;
  right: var(--space-1);
  bottom: var(--space-1);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--surface);
  border: 1px solid var(--rule);
  color: var(--alert);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

.uploader__remove:active { background: var(--alert-bg); border-color: var(--alert); }

@media (hover: hover) {
  .uploader__remove:hover { background: var(--alert-bg); border-color: var(--alert); }
}

.uploader__item--file .uploader__remove { position: static; }

.uploader__status:empty { display: none; }

.uploader__errors {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

/* --- Map picker ----------------------------------------------------------
   Loaded on demand, so a blocked CDN degrades to coordinate inputs rather
   than leaving the form unusable. */

.map-pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--space-4);
}

.mappicker {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.mappicker__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

/* Leaflet numbers its own layers from 200 (tiles) to 1000 (zoom buttons).
   Without a stacking context of its own, the map competed with the page on
   those numbers, and scrolled over the sticky nav (z-index 50). `isolate`
   keeps Leaflet's order inside the map and stacks the map as one ordinary
   part of the page: markers, popups and controls work as before. */
.mappicker__canvas {
  isolation: isolate;
  height: 240px;
  border: var(--border-hair);
  border-radius: var(--radius-md);
  background: var(--paper);
  overflow: hidden;
}

.mappicker__canvas[hidden] { display: none; }

.mappicker__loading {
  display: grid;
  place-items: center;
  height: 100%;
  color: var(--muted);
  font-size: var(--text-xs);
}

.mappicker__readout {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-variant-numeric: tabular-nums lining-nums;
}

/* Leaflet paints its own controls; keep them on the system's radii. */
.mappicker__canvas .leaflet-control-zoom a { border-radius: var(--radius-sm); }
.mappicker__canvas .leaflet-container { font-family: var(--font-sans); }

.form-actions__note { margin-right: auto; }

@media (max-width: 640px) {
  .form-actions__note { margin-right: 0; text-align: center; }
}

/* --- Address lookup ------------------------------------------------------
   Sits above the canvas. The map's own size, chrome and behaviour are
   untouched; this only moves the pin. */

.mappicker__search {
  display: flex;
  gap: var(--space-2);
}

.mappicker__search .field__control { flex: 1; min-width: 0; }

.mappicker__search .btn { flex-shrink: 0; }

.mappicker [data-search-status]:empty { display: none; }

@media (max-width: 480px) {
  .mappicker__search { flex-direction: column; }
  .mappicker__search .btn { width: 100%; }
}

/* --- Gallery -------------------------------------------------------------
   Blobs arrive asynchronously, so tiles hold their place while loading. */

.gallery { display: flex; flex-direction: column; gap: var(--space-3); }

.gallery__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--space-3);
}

.gallery__item {
  border: var(--border-hair);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--paper);
}

.gallery__skeleton { display: block; width: 100%; height: 110px; }

.gallery__thumb-button {
  padding: 0;
  cursor: zoom-in;
  transition: border-color var(--duration-fast) var(--ease);
}

@media (hover: hover) {
  .gallery__thumb-button:hover { border-color: var(--accent); }
}

.gallery__thumb {
  display: block;
  width: 100%;
  height: 110px;
  object-fit: cover;
}

.gallery__item--video {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-2);
}

.gallery__video { width: 100%; border-radius: var(--radius-sm); }

.gallery__status:empty { display: none; }

/* --- Photos and videos (detail view) ---------------------------------------
   One panel holds every gallery, a group for each part of the kos. A group's
   title is a label, as in the definition lists above it; the panel's h2 is
   the heading. The class outranks the base h3 size; the leading is restated
   because the base sets headings tight. */

.media-groups {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.media-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.media-group__title {
  color: var(--muted);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-body);
}

/* --- Lightbox ------------------------------------------------------------
   The second of the two places a shadow is used: this floats above the page. */

.lightbox-dialog {
  width: min(880px, calc(100vw - var(--space-6)));
  padding: var(--space-4);
  border: var(--border-hair);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-lightbox);
  color: var(--ink);
}

.lightbox-dialog::backdrop { background: rgba(22, 36, 31, 0.6); }

.lightbox { display: flex; flex-direction: column; gap: var(--space-3); }

.lightbox__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
}

.lightbox__count {
  color: var(--muted);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

.lightbox__figure { margin: 0; display: flex; flex-direction: column; gap: var(--space-2); }

.lightbox__image {
  width: 100%;
  max-height: min(70vh, 560px);
  object-fit: contain;
  background: var(--paper);
  border-radius: var(--radius-sm);
}

.lightbox__nav { display: flex; gap: var(--space-2); }
.lightbox__nav .btn { flex: 1; }

/* --- Validation ----------------------------------------------------------
   Errors are written into the live form, so these styles apply to nodes that
   already exist rather than to a re-render. */

.error-summary { margin-bottom: var(--space-4); }
.error-summary[hidden] { display: none; }

.form-section__errors {
  margin-left: auto;
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--alert);
  border-radius: var(--radius-sm);
  background: var(--alert-bg);
  color: var(--alert);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  white-space: nowrap;
}

.form-section__errors[hidden] { display: none; }

/* A whole group can be wrong, not just a single input. */
.choice-group[data-invalid="true"] .choice-group__legend { color: var(--alert); }

.choice-group[data-invalid="true"] .choice {
  border-color: var(--alert);
}

.mappicker[data-invalid="true"] .mappicker__canvas,
.mappicker[data-invalid="true"] .mappicker__manual {
  border-color: var(--alert);
}

.field__count--over { color: var(--alert); font-weight: var(--weight-medium); }

/* A confirmation that is a question, not a destructive act. */
.dialog__actions .btn--primary { background: var(--accent); color: var(--surface); }

/* --- Best Match ----------------------------------------------------------
   Deliberately quiet, and closed until asked for. The comparison above is
   the product; this is a guide sitting underneath it. */

.bestmatch {
  margin-top: var(--space-5);
  border: var(--border-hair);
  border-radius: var(--radius-md);
  background: var(--surface);
}

.bestmatch__summary {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-4);
  cursor: pointer;
  list-style: none;
}

.bestmatch__summary::-webkit-details-marker { display: none; }

.bestmatch__summary::after {
  content: "Show";
  position: absolute;
  right: var(--space-4);
  color: var(--accent);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

.bestmatch[open] .bestmatch__summary::after { content: "Hide"; }

.bestmatch__summary { position: relative; }

.bestmatch__summary:focus-visible {
  outline: var(--focus-width) solid var(--focus-color);
  outline-offset: calc(-1 * var(--focus-width));
}

.bestmatch__summary-title {
  font-weight: var(--weight-semibold);
}

.bestmatch__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: 0 var(--space-4) var(--space-4);
  border-top: var(--border-hair);
  padding-top: var(--space-4);
}

.bestmatch__note { max-width: var(--measure); }

.bestmatch__results {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.bestmatch__result {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  padding-bottom: var(--space-2);
  border-bottom: var(--border-hair);
}

.bestmatch__result:last-child { border-bottom: none; }

.bestmatch__kos { font-weight: var(--weight-medium); }

/* A number, not a badge: no gradient chip, no trophy. */
.bestmatch__score {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-3);
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums lining-nums;
}

.bestmatch__score--leader { color: var(--accent); }

.bestmatch__flag {
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  border-left: 2px solid var(--accent);
  padding-left: var(--space-2);
}

.bestmatch__method {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-2) var(--space-4);
  padding-top: var(--space-3);
  border-top: var(--border-hair);
  font-size: var(--text-xs);
}

.bestmatch__method dt {
  color: var(--ink);
  font-family: var(--font-narrow);
  font-weight: var(--weight-semibold);
}

.bestmatch__method dd { margin: 0; color: var(--muted); max-width: var(--measure); }

@media (max-width: 640px) {
  .bestmatch__method { grid-template-columns: 1fr; gap: var(--space-1) 0; }
  .bestmatch__method dd { margin-bottom: var(--space-2); }
}

/* --- Comparison on narrow screens ----------------------------------------
   Below 900px a criteria column plus three kos columns cannot be read, so
   each group becomes a card and each row stacks its values. Same rows, same
   grouping, same order — only the axis changes.

   Explicit ARIA roles on the table survive this display change, so the
   structure is still a table to assistive technology, and the header row is
   hidden visually rather than removed so column associations remain. */

.ledger__cell-label { display: none; }

@media (max-width: 900px) {
  .ledger-wrap {
    overflow-x: visible;
    border: none;
    border-radius: 0;
    background: none;
  }

  .ledger {
    display: block;
    min-width: 0;
  }

  /* Clipped, not removed: screen readers keep the column headers. */
  .ledger thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .ledger tbody {
    display: block;
    margin-bottom: var(--space-4);
    border: var(--border-hair);
    border-radius: var(--radius-md);
    background: var(--surface);
    overflow: hidden;
  }

  .ledger tr { display: block; }

  .ledger__group th {
    display: block;
    border-top: none;
    border-bottom: var(--border-group);
    font-size: var(--text-base);
  }

  .ledger tbody tr:not(.ledger__group) {
    display: block;
    padding: var(--space-3) var(--space-4);
    border-bottom: var(--border-hair);
  }

  .ledger tbody tr:not(.ledger__group):last-child { border-bottom: none; }

  .ledger__criterion {
    position: static;
    display: block;
    width: auto;
    min-width: 0;
    padding: 0 0 var(--space-2);
    border: none;
    background: none;
    box-shadow: none;
    color: var(--ink);
    font-weight: var(--weight-semibold);
  }

  /* A differing row is marked by its label, since there is no column to rule. */
  .ledger tr[data-differs="true"] .ledger__criterion {
    box-shadow: none;
    color: var(--accent);
  }

  .ledger td {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-1) 0;
    border: none;
    text-align: left;
  }

  .ledger td.numeric { text-align: left; }

  .ledger__cell-label {
    display: block;
    flex: 1;
    color: var(--muted);
    font-family: var(--font-narrow);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
  }

  .ledger__cell-value {
    flex-shrink: 0;
    font-variant-numeric: tabular-nums lining-nums;
  }

  /* The weight breakdown reads the same way. */
  .bestmatch__table tbody tr:not(.ledger__group) { padding: var(--space-3) var(--space-4); }
}

/* --- Best Match breakdown on narrow screens --------------------------------
   Stacked, each criterion lists one figure per kos, and bare figures under
   "Facilities" did not say which kos each one was. Each reads
   `Kos Pelangi: 58`: the name first and in ink, since it is what the eye
   scans for, then the score in semibold, one space after the colon whatever
   the name's length. A long name ends in an ellipsis rather than wrapping,
   so every score stays on its kos's line. The weight belongs to the
   criterion, not to a kos, so it keeps the quiet label style. Placed after
   the narrow ledger rules above so it wins on source order. */

@media (max-width: 900px) {
  .bestmatch__table td {
    justify-content: flex-start;
    gap: var(--space-1);
  }

  .bestmatch__table .ledger__cell-label {
    display: flex;
    flex: 0 1 auto;
    min-width: 0;
    color: var(--ink);
    font-family: var(--font-sans);
    font-size: var(--text-base);
    font-weight: var(--weight-medium);
  }

  .bestmatch__cell-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .bestmatch__table .ledger__cell-value { font-weight: var(--weight-semibold); }

  .bestmatch__table .bestmatch__weight .ledger__cell-label,
  .bestmatch__table .bestmatch__weight .ledger__cell-value {
    color: var(--muted);
    font-family: var(--font-narrow);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
  }
}

/* --- Facility filters ----------------------------------------------------
   Folded away by default: there are 24 of them, and most filtering is by
   rent or area. */

.filter-facilities {
  margin-top: var(--space-4);
  border-top: var(--border-hair);
  padding-top: var(--space-4);
}

.filter-facilities__summary {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  cursor: pointer;
  font-weight: var(--weight-semibold);
  list-style: none;
}

.filter-facilities__summary::-webkit-details-marker { display: none; }

.filter-facilities__summary::after {
  content: "Show";
  margin-left: auto;
  color: var(--accent);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

.filter-facilities[open] .filter-facilities__summary::after { content: "Hide"; }

.filter-facilities__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding-top: var(--space-4);
}

.filter-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: var(--border-hair);
}

/* --- Filter bar ----------------------------------------------------------
   All that remains on the page: what is being shown, and the way in. The
   controls themselves are in a dialog, so the surveys are not pushed down
   the page by the means of finding them. */

.filter-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  /* Deliberate separation from the results below. */
  margin-bottom: var(--space-7);
}

.filter-bar__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.filter-bar__count {
  display: inline-grid;
  place-items: center;
  min-width: 20px;
  height: 20px;
  margin-left: var(--space-2);
  padding: 0 var(--space-1);
  border-radius: 999px;
  background: var(--accent);
  color: var(--surface);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums lining-nums;
}

/* --- Filter dialog -------------------------------------------------------- */

/* Scoped to [open]: a bare `display` here would beat the browser's own
   `dialog:not([open]) { display: none }` and leave the closed dialog on
   screen over the page. */
.dialog--wide {
  width: min(720px, calc(100vw - var(--space-6)));
  max-height: calc(100vh - var(--space-7));
  padding: 0;
}

.dialog--wide[open] {
  display: flex;
  flex-direction: column;
}

/* A list of kos needs less room than the filters. Declared before the
   narrow-screen rule below, so phones still get the full-screen sheet. */
.dialog--picker { width: min(560px, calc(100vw - var(--space-6))); }

/* The picker lists each source under its own name. The name takes the
   narrow face that labels table columns, so it reads as structure rather
   than as one more kos. */
.picker-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.picker-group__title {
  color: var(--muted);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

.picker-group__empty {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

/* The Survey guide is three short lists. */
.dialog--guide { width: min(480px, calc(100vw - var(--space-6))); }

.guide-dialog__heading {
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
}

.guide-dialog__list {
  margin: var(--space-2) 0 0;
  padding-left: var(--space-5);
  list-style: disc;
}

.guide-dialog__list--steps { list-style: decimal; }

.guide-dialog__list li + li { margin-top: var(--space-1); }

/* The kos picker shares the filter dialog's shape: fixed head and foot, a
   body that scrolls between them. */
.filter-dialog,
.picker-dialog,
.guide-dialog {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.filter-dialog__head,
.picker-dialog__head,
.guide-dialog__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  border-bottom: var(--border-hair);
}

/* The body scrolls, so the actions stay reachable with 24 facilities open. */
.filter-dialog__body,
.picker-dialog__body,
.guide-dialog__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding: var(--space-5);
  overflow-y: auto;
}

.filter-dialog__facilities {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding-top: var(--space-4);
  border-top: var(--border-hair);
}

.filter-dialog__foot,
.picker-dialog__foot,
.guide-dialog__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-top: var(--border-hair);
  background: var(--paper);
}

/* The body scrolls on its own: reaching the end of 24 facilities must not
   start scrolling, or bouncing, the page underneath. */
.filter-dialog__body,
.picker-dialog__body,
.guide-dialog__body { overscroll-behavior: contain; }

/* --- How to fill panel ----------------------------------------------------
   One dialog for every field's ⓘ. On a wide screen it sits beside the ⓘ
   (howToPanel.js places it) with no dimmed backdrop: it explains one field
   rather than taking over the page. On a phone it is a sheet like the
   others (the narrow-screen rules below). */
.dialog--howto {
  width: min(360px, calc(100vw - var(--space-6)));
  max-height: min(70vh, 560px);
  padding: 0;
}

.dialog--howto[open] {
  display: flex;
  flex-direction: column;
}

@media (min-width: 641px) {
  .dialog--howto {
    position: fixed;
    inset: auto;
    margin: 0;
  }

  .dialog--howto::backdrop { background: transparent; }
}

.howto {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.howto__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--border-hair);
}

.howto__title {
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
}

.howto__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-4);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.howto__heading {
  margin-bottom: var(--space-1);
  color: var(--muted);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

.howto__shots {
  margin: 0;
  padding-left: var(--space-5);
  list-style: disc;
}

.howto__scores {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
}

.howto__score dt { font-weight: var(--weight-semibold); }
.howto__score dd { margin: 0; }

.howto__benchmark {
  display: block;
  color: var(--muted);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums lining-nums;
}

/* On a phone the wide dialogs become sheets, as on iPhone:
   - anchored to the bottom edge, where the thumb already is
   - rounded top corners
   - a strip of the page left showing above: the sheet is a task inside
     that page, not a new place
   Short content leaves more of the page showing. The foot keeps clear of
   the home indicator where the browser reports one. */
@media (max-width: 640px) {
  .dialog--wide,
  .dialog--howto {
    width: 100%;
    max-width: 100%;
    height: auto;
    max-height: calc(100vh - var(--space-6));
    max-height: calc(100dvh - var(--space-6));
    margin: auto 0 0;
    border-bottom: none;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  }

  /* The head is where a sheet is dragged down to dismiss (sheet.js), so the
     browser must not claim that drag as a scroll. */
  /* A panel is a few short paragraphs: the sheet fits them and leaves the
     field it explains in view above. `auto`, with the dialog pinned to both
     edges, would stretch it to the full height. */
  .dialog--howto,
  .dialog--guide { height: fit-content; }

  .filter-dialog__head,
  .picker-dialog__head,
  .guide-dialog__head,
  .howto__head { touch-action: none; }

  /* The sheet's own close button clears the home indicator. */
  .howto__body { padding-bottom: max(var(--space-4), env(safe-area-inset-bottom)); }

  .filter-dialog__foot,
  .picker-dialog__foot,
  .guide-dialog__foot {
    flex-direction: column-reverse;
    padding-bottom: max(var(--space-4), env(safe-area-inset-bottom));
  }

  .filter-dialog__foot .btn,
  .picker-dialog__foot .btn,
  .guide-dialog__foot .btn { width: 100%; }
}

/* A sheet rises from the edge it is anchored to. `backwards`, not `both`:
   a finished animation that kept filling would override the inline
   transform sheet.js sets while the sheet follows a finger. */
@media (max-width: 640px) and (prefers-reduced-motion: no-preference) {
  .dialog--wide[open],
  .dialog--howto[open] {
    animation: sheet-in var(--duration-slow) var(--ease-out) backwards;
  }
}

@keyframes sheet-in {
  from { transform: translateY(100%); }
  to   { transform: none; }
}

/* --- Compare bar ---------------------------------------------------------
   The selection, held above the work. The slots take a row of their own
   across the bar, with the count and actions beneath: beside the buttons
   each card had about 140px for a name, too little for "Kos Melati
   Residence". Empty slots show how many more kos can be added without the
   user having to remember the limit. */

.compare-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  margin-bottom: var(--space-6);
  padding: var(--space-4);
  background: var(--surface);
  border: var(--border-hair);
  border-radius: var(--radius-md);
}

/* One equal column per slot. The count comes from MAX_COMPARE through
   --slots, so the limit is still written only in constants.js. */
.compare-bar__chips {
  display: grid;
  grid-template-columns: repeat(var(--slots, 3), minmax(0, 1fr));
  gap: var(--space-2);
  flex: 1 1 100%;
  min-width: 0;
}

/* A fixed height and a single line keep every card the same size, filled or
   empty, whatever the kos is called: a long name ends in an ellipsis rather
   than growing its card. 40px is the button height, so the bar reads as one
   set of controls. */
.compare-chip {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  height: 40px;
  min-width: 0;
  padding: 0 var(--space-3);
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  background: var(--paper);
}

.compare-chip__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: var(--weight-semibold);
}

.compare-chip__remove {
  flex-shrink: 0;
  color: var(--alert);
  font-family: var(--font-narrow);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}

.compare-chip__remove:active { text-decoration: underline; }

@media (hover: hover) {
  .compare-chip__remove:hover { text-decoration: underline; }
}

.compare-chip--empty {
  border-style: dashed;
  background: none;
}

/* The count starts the row, under the first card; the buttons end it. */
.compare-bar__actions {
  display: flex;
  flex: 1 1 100%;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
}

.compare-bar__actions > .meta { margin-right: auto; }

/* Three slots across a narrow bar would cut most names short; below the
   breakpoint where the comparison itself stacks, the slots stack too. */
@media (max-width: 900px) {
  .compare-bar__chips { grid-template-columns: minmax(0, 1fr); }
}

/* Stacked on a phone, the reversed column puts the count last. Add kos is
   lifted to the top, above Compare, at the user's request: pick the kos,
   then compare them, top to bottom, as the wide bar reads left to right and
   as focus moves. */
@media (max-width: 640px) {
  .compare-bar { flex-direction: column; align-items: stretch; }
  .compare-bar__actions { flex-direction: column-reverse; align-items: stretch; }
  .compare-bar__actions > .meta { margin-right: 0; }
  .compare-bar__actions .btn { width: 100%; }
  .compare-bar__add { order: 1; }
}

/* --- Sectioned ledger on narrow screens -----------------------------------
   The narrow ledger rules above turn each table body into a card. Here the
   section already is the card, so the body drops its own frame. The column
   header card goes too: every section still carries its column headers for
   screen readers, and the chips in the compare bar hold the Remove buttons.
   Placed last so it wins on source order, not on specificity. */

@media (max-width: 900px) {
  .ledger-sections { overflow-x: visible; }
  .ledger-section { min-width: 0; }
  .ledger-section--columns { display: none; }

  .ledger--sectioned tbody {
    margin-bottom: 0;
    border: none;
    border-radius: 0;
  }

  /* Each label sits flush above its values. The narrow `.ledger__criterion`
     rule above (0,1,0) loses to the desktop `.ledger th` (0,1,1), which left
     12px padding and right and bottom hairlines boxed around every label —
     the specificity trap recorded in docs/DESIGN-SYSTEM.md. The doubled class
     wins without !important, for the Best Match breakdown too. */
  .ledger .ledger__criterion {
    padding: 0 0 var(--space-2);
    border: none;
  }

  /* A figure fits beside its kos name; a paragraph does not. The narrow
     rules above keep each value to one unshrinking line, which ran notes
     off the card while squeezing the name into two lines. A note instead
     sits under its kos name at full width, and the extra space between
     notes keeps three paragraphs from reading as one block. */
  .ledger td.ledger__prose {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-1);
    padding-block: var(--space-2);
  }

  .ledger td.ledger__prose .ledger__cell-label { flex: none; }

  .ledger td.ledger__prose .ledger__cell-value {
    flex-shrink: 1;
    max-width: var(--measure-notes);
  }
}

/* --- Account pages ---------------------------------------------------------
   Log in and create an account: one narrow card, centred. The way in, not a
   destination, so nothing competes with the form. */

.auth {
  display: flex;
  justify-content: center;
  padding-top: var(--space-5);
}

.auth__card {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  width: 100%;
  max-width: 440px;
  padding: var(--space-6) var(--space-5);
  background: var(--surface);
  border: var(--border-hair);
  border-radius: var(--radius-md);
}

.auth__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.auth__submit {
  width: 100%;
  margin-top: var(--space-2);
}

.auth__notice:empty { display: none; }

.auth__alternate { color: var(--muted); }

@media (max-width: 640px) {
  .auth { padding-top: 0; }
  .auth__card { padding: var(--space-5) var(--space-4); }
}

/* --- Touch targets ---------------------------------------------------------
   Apple's default control size on iPhone and iPad is 44×44pt, and a survey
   is recorded on a phone. On a touch screen (pointer: coarse) every control
   reaches --target-min; with a mouse the denser 40px stays.

   - Full-size controls — buttons, fields, choice tiles, compare slots —
     grow to it.
   - Small and text-only controls keep their look, and take an invisible
     hit area of that size centred on them: a 44px-tall button with 13px
     text would read as a mistake.

   Controls sit 12px apart, which Apple suggests around bezelled controls,
   so the enlarged areas do not meet. Placed last so it wins on source
   order, not specificity. */

@media (pointer: coarse) {
  .btn:not(.btn--small),
  .field__control,
  .choice { min-height: var(--target-min); }

  /* The wrapper draws the border, so the control inside is 2px shorter and
     the pair lands on the same 44px as a plain field. */
  .field__group .field__control { min-height: calc(var(--target-min) - 2px); }

  /* The slots match the buttons beside them, as they do at 40px. */
  .compare-chip { height: var(--target-min); }

  .btn--small,
  .compare-chip__remove,
  .field__info,
  .page-head__help { position: relative; }

  .btn--small::after,
  .compare-chip__remove::after,
  .field__info::after,
  .page-head__help::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: max(100%, var(--target-min));
    height: max(100%, var(--target-min));
    transform: translate(-50%, -50%);
  }

  /* Sits on a photo, where there is room for the real size. */
  .uploader__remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--target-min);
    min-height: var(--target-min);
  }

  .survey-card__actions,
  .dialog__actions,
  .lightbox__nav { gap: var(--space-3); }

  /* Leaflet sizes its zoom buttons at 30px even in touch mode. The map is
     pinned by hand mid-visit, so they get the same 44px as everything
     else. Three classes deep so it wins whichever stylesheet loads last —
     Leaflet's arrives on demand. */
  .mappicker .leaflet-touch .leaflet-bar a {
    width: var(--target-min);
    height: var(--target-min);
    line-height: var(--target-min);
  }
}
