:root {
  --color-primary: #E94F37;
  --color-primary-dark: #C93D29;
  --color-primary-soft: #FDE4DE;
  --color-yellow: #F6C85F;
  --color-green: #7FA58A;
  --color-blue: #4C5F8A;
  --color-background: #FFF9F1;
  --color-surface: #FFFFFF;
  --color-text-primary: #2D2926;
  --color-text-secondary: #6B625D;
  --color-border: #E8DED4;

  --radius-sm: 8px;
  --radius-md: 16px;
  --radius-lg: 24px;
  --radius-pill: 999px;

  --duration-fast: 150ms;
  --duration-standard: 250ms;
  --duration-slow: 350ms;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
  --space-12: 48px;
  --space-16: 64px;
}

* { box-sizing: border-box; }

/* A component's own display:flex/inline-flex/etc has the same specificity as
   the [hidden] attribute and, being later in cascade order, silently wins —
   this hit .dish-view-wrap, .dish-edit-wrap, .dish-corner and .link-button
   in turn. Settling it globally instead of per-class. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  overflow-x: hidden;
  /* Without this, a horizontal swipe near the screen edge triggers the
     mobile browser's own "swipe to go back/forward" gesture (showing small
     grey "Åter"/"Nästa" hint buttons) on top of our own tab-swipe gesture —
     since pushState-based navigation gives the browser real history entries
     to swipe through. This disables that native gesture so only our swipe
     handler reacts. Both axes, so a gesture that runs past the end of a
     scroller can't be handed to the page and drag the app around either. */
  overscroll-behavior: none;
}

img, svg { max-width: 100%; }

body {
  background: var(--color-background);
  color: var(--color-text-primary);
  font-family: 'Nunito Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, p { margin: 0; }

h1 { font-size: 30px; line-height: 36px; font-weight: 800; }
h2 { font-size: 24px; line-height: 30px; font-weight: 700; }
h3 { font-size: 20px; line-height: 26px; font-weight: 700; }
.text-help { font-size: 14px; line-height: 20px; color: var(--color-text-secondary); }
.text-body { font-size: 16px; line-height: 24px; }

/* Upload status line under an image/file field's pick button ("Laddar
   upp...", or an error message) — pulled further from the button above it,
   and always capitalized regardless of how the underlying message (which
   can come straight from the server) happened to be cased. */
.upload-status {
  margin-top: var(--space-3);
}
.upload-status::first-letter { text-transform: uppercase; }

a { color: var(--color-blue); }

.link-button {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0;
  background: none;
  border: none;
  color: var(--color-blue);
  font-family: inherit;
  font-weight: 700;
  font-size: 15px;
  text-align: left;
  cursor: pointer;
}
/* The language toggle's flag+label: a plain space between the two spans
   (like backLink's chevron) read as too tight for an icon this shape. */
.lang-toggle-btn { gap: var(--space-2); }

/* ── Layout ─────────────────────────────────────────────────────────── */
.page {
  max-width: 600px;
  margin: 0 auto;
  padding: var(--space-2) 20px var(--space-8);
}
#add-dish-area:not(:empty) { margin-bottom: var(--space-6); }
#banner:not(:empty) { margin-bottom: var(--space-4); }
@media (min-width: 700px) {
  .page { padding-left: 32px; padding-right: 32px; }
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: calc(var(--space-4) - 5px) 20px var(--space-2);
  gap: var(--space-3);
}
@media (min-width: 700px) {
  .topbar { padding-left: 32px; padding-right: 32px; }
}
.topbar .back {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--color-text-primary);
  text-decoration: none;
  font-weight: 700;
  min-height: 44px;
  padding: var(--space-2) 0;
}
.topbar .brand {
  display: inline-flex;
  align-items: center;
  font-weight: 800;
  font-size: 20px;
  color: var(--color-primary-dark);
  text-decoration: none;
}
/* Only the pager's own four topbars — the onboarding screen has no tabs yet
   for its logo to jump to, so it stays a plain (non-clickable) mark there. */
.pager .brand { cursor: pointer; }
.brand-logo {
  display: block;
  height: 48px;
  width: auto;
}

/* ── Focus / a11y ───────────────────────────────────────────────────── */
:focus-visible {
  outline: 3px solid var(--color-blue);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  * { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}

/* ── Buttons ────────────────────────────────────────────────────────── */
button { font-family: inherit; }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 52px;
  padding: var(--space-2) var(--space-6);
  border-radius: var(--radius-md);
  font-size: 17px;
  line-height: 22px;
  font-weight: 700;
  border: none;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color var(--duration-fast) ease, border-color var(--duration-fast) ease, opacity var(--duration-fast) ease;
}
.btn i { flex: none; font-size: 18px; line-height: 1; }
.btn:has(> i:first-child) { padding-left: var(--space-4); }
.btn-primary {
  background: var(--color-primary);
  color: #fff;
}
.btn-primary:hover { background: var(--color-primary-dark); }
.btn-secondary {
  background: var(--color-surface);
  color: var(--color-text-primary);
  border: 1.5px solid var(--color-border);
}
.btn-secondary:hover { border-color: var(--color-text-secondary); }
.btn-secondary.danger { color: var(--color-primary-dark); }
.btn-secondary.danger:hover { border-color: var(--color-primary-dark); }
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.btn-icon {
  min-width: 48px;
  min-height: 48px;
  padding: 0;
  border-radius: var(--radius-md);
  background: transparent;
  border: 1.5px solid var(--color-border);
  color: var(--color-text-primary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.btn-icon i { font-size: 19px; line-height: 1; }
.btn-icon.danger { color: var(--color-primary-dark); }
.btn-icon.primary {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: #fff;
}
.btn-icon.primary:hover { background: var(--color-primary-dark); border-color: var(--color-primary-dark); }
/* Opts a .btn-icon out of the standard 48px/bordered treatment — used where
   the row it sits in should be exactly as tall as a sibling row with no
   button at all (the family-member list: "you" has no button and is
   text-height; other members' delete button was making theirs taller). */
.member-remove-btn {
  min-width: 24px;
  min-height: 24px;
  border: none;
}
.member-remove-btn i { font-size: 16px; }

/* ── Filter bar ─────────────────────────────────────────────────────── */
/* A shared design element, full-width band, aligned with the cards below
   rather than running edge-to-edge. No rounding on the bar itself —
   rounding is reserved for buttons and the recipe cards. */
.filter-bar-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  max-width: 600px;
  margin: 0 auto;
  padding: var(--space-2) 20px;
}
.filter-bar-inner #add-dish-btn { flex: none; }
@media (min-width: 700px) {
  .filter-bar-inner { padding-left: 32px; padding-right: 32px; }
}
/* A button styled to look like the old native <select>, not an actual
   <select> — iOS Safari adds its own "Åter"/"Nästa" (Previous/Next) toolbar
   above any focused native form control when other focusable fields exist
   on the page, and there's no supported way to suppress that from CSS. A
   custom button + our own dropdown (.filter-menu below) sidesteps it. */
.filter-select-wrap { position: relative; flex: 1 1 auto; min-width: 0; max-width: 100%; }
.filter-select {
  display: block;
  width: 100%;
  min-height: 52px;
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
  padding: 0 var(--space-8) 0 var(--space-4);
  border-radius: var(--radius-md);
  background-color: var(--color-surface);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236B625D' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
  background-size: 18px;
  border: 1.5px solid var(--color-border);
  color: var(--color-text-primary);
  font-family: inherit;
  font-weight: 700;
  font-size: 14px;
  cursor: pointer;
}
/* flex:1 1 auto above stretches the dropdown to fill whatever space is left
   next to "Ny maträtt" — harmless on a phone-width bar, but on desktop
   that's most of a 600px row for one dropdown. Capped to roughly that
   button's own width instead. Placed after .filter-select-wrap's own
   max-width:100% (same specificity) to actually win — a media query alone
   doesn't out-rank an equal-specificity rule that comes later in the file. */
@media (min-width: 700px) {
  .filter-select-wrap { max-width: 160px; }
}
.filter-menu {
  position: absolute;
  top: calc(100% + var(--space-1));
  right: 0;
  min-width: 200px;
  max-width: min(280px, 90vw);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: 0 8px 24px rgba(45, 41, 38, 0.18);
  z-index: 50;
  padding: var(--space-2);
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.filter-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  border: none;
  background: transparent;
  font-family: inherit;
  font-size: 15px;
  font-weight: 700;
  color: var(--color-text-primary);
  cursor: pointer;
}
.filter-menu-item:hover, .filter-menu-item:focus-visible { background: var(--color-primary-soft); }
.filter-menu-item[aria-selected="true"] { color: var(--color-primary-dark); background: var(--color-primary-soft); }

/* The gallery-view toggle sits right after the equipment row rather than as
   its last scrollable item — that row is rebuilt from scratch (innerHTML)
   every time the owned-equipment list changes, which would silently wipe out
   a toggle button living inside it, and a button you sometimes have to
   scroll a checkbox row to reach is a worse affordance anyway. */
.equipment-filter-outer { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2); }
.equipment-filter-outer .equipment-filter-wrap { flex: 1; min-width: 0; margin-bottom: 0; }
/* No border on the gallery toggle here — .btn-icon's default 1.5px border
   reads as one filter chip too many next to the equipment row it sits beside.
   Also opts out of .btn-icon's 48px touch-target box (same reasoning as
   .member-remove-btn above): that box was forcing the *whole* row — chips
   included — to 48px tall even though the checkboxes and their labels are
   only ~26px, which read as oversized top/bottom margin around the chips. */
#gallery-toggle-btn {
  border: none;
  min-width: 32px;
  min-height: 32px;
}
/* Equipment quick-filters always stay on one row — rather than wrapping to
   a second line, overflow scrolls horizontally, with a small fading arrow
   hinting there's more to scroll to (hidden once everything fits, or once
   scrolled to the end). */
.equipment-filter-wrap { position: relative; margin-bottom: var(--space-4); }
.equipment-filter-row {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--space-6);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding-right: var(--space-8);
}
.equipment-filter-row::-webkit-scrollbar { display: none; }
.equipment-filter-row label { flex: none; white-space: nowrap; }
.equipment-filter-hint {
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  width: 32px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  pointer-events: none;
  background: linear-gradient(to right, transparent, var(--color-background) 55%);
  color: var(--color-text-secondary);
}
.equipment-filter-hint i { font-size: 14px; }

.category-toggle-input {
  appearance: none;
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  border-radius: var(--radius-sm);
  border: 1.5px solid var(--color-border);
  background: var(--color-surface);
  cursor: pointer;
  position: relative;
  flex: none;
}
.category-toggle-input:checked {
  background: var(--color-primary);
  border-color: var(--color-primary);
}
.category-toggle-input:checked::after {
  content: '';
  position: absolute;
  left: 6px;
  top: 2px;
  width: 5px;
  height: 10px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

/* ── Cards ──────────────────────────────────────────────────────────── */
.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  box-shadow: 0 1px 3px rgba(45, 41, 38, 0.06);
}
.settings-family-card h2 { margin-bottom: var(--space-4); }
.settings-subsection { margin-top: var(--space-4); }
.settings-subsection:not(:first-of-type) {
  margin-top: var(--space-6);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-border);
}
.settings-subsection h3 {
  font-size: 14px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-secondary);
  margin: 0 0 var(--space-3);
}
.friend-card {
  display: flex;
  cursor: pointer;
  transition: border-color var(--duration-fast) ease, box-shadow var(--duration-fast) ease;
}
.friend-card:hover, .friend-card:focus-visible {
  border-color: var(--color-primary);
  box-shadow: 0 2px 6px rgba(45, 41, 38, 0.1);
}

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

/* Gallery view — Våra recept with the toggle switched over: every dish that
   has a photo, two columns, no gap in either direction (a scrollable wall of
   photos, not a list), so a photo runs edge-to-edge into its neighbors
   rather than sitting in its own little card. .dish-thumb is otherwise sized
   for the fixed 76px column of a list row; here it fills its own grid cell
   completely, portrait (4:5) and unrounded, same as its neighbors. */
.dish-list-gallery {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0;
}
.dish-list-gallery .dish-thumb {
  width: 100%;
  height: auto;
  /* A bit taller than square — most food photos are shot portrait anyway, so
     this crops less than a 1:1 grid would. */
  aspect-ratio: 4 / 5;
  min-height: 0;
  flex: initial;
  border-radius: 0;
}

.dish-card {
  position: relative;
  padding: 0;
  overflow: hidden;
}
.dish-card .dish-view-wrap { align-items: stretch; gap: 0; position: relative; }
.dish-thumb {
  position: relative;
  width: 76px;
  min-height: 96px;
  height: auto;
  background: var(--color-primary-soft);
  color: var(--color-primary-dark);
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  overflow: hidden;
}
/* position:absolute takes the img out of flow so a portrait-oriented
   source image's own aspect ratio can't inflate the flex row's
   hypothetical cross size before the stretch pass runs — without this,
   tall images made the whole card taller even though object-fit:cover
   ends up cropping it to the same box either way. */
.dish-thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.dish-thumb i { font-size: 24px; line-height: 1; }
.dish-thumb-clickable { cursor: pointer; }
.dish-thumb-dots {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 6px;
  display: flex;
  justify-content: center;
  gap: 4px;
  pointer-events: none;
}
.dish-thumb-dots span {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.85);
  box-shadow: 0 0 2px rgba(45, 41, 38, 0.6);
}
/* Right padding is the corner button's reserved 48px + right:8px inset —
   plus one more --space-2 so a long, wrapped .dish-name has actual breathing
   room instead of its widest line running flush into the button's edge. */
.dish-body { display: flex; flex-direction: column; flex: 1; min-width: 0; padding: var(--space-4) 64px var(--space-4) var(--space-3); }
.dish-edit-wrap { padding: var(--space-4); }

/* Lunch/middag toggle — two segments of what used to be separate .btn-primary
   /.btn-secondary buttons, now sharing one pill so they read as a single
   control rather than two unrelated actions. The wrapper carries the border
   and the rounding (clipped with overflow: hidden); the buttons themselves
   are square inside it, with a hairline divider between them. */
.meal-toggle {
  display: flex;
  margin-bottom: var(--space-4);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.meal-toggle-btn {
  flex: 1;
  min-width: 0;
  border-radius: 0;
  background: var(--color-surface);
  color: var(--color-text-primary);
}
.meal-toggle-btn:first-child { border-right: 1.5px solid var(--color-border); }
.meal-toggle-btn.active { background: var(--color-primary); color: #fff; }

/* Matveckan's card is styled exactly like the recipe list's own dish-card —
   the weekday is a heading above it rather than a label inside it, and the
   drag handle used to swap two days sits outside the card, to its right. */
/* #week-list is a .stack (gap: var(--space-4)) — .week-day's own
   margin-bottom used to stack on top of that gap instead of replacing it,
   nearly tripling the visual distance between days. This overrides the
   stack's gap directly instead, so there's exactly one gap value. */
#week-list.stack { gap: var(--space-2); }
.weekday-heading {
  font-size: 14px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-secondary);
  margin: 0 0 var(--space-1) var(--space-1);
}
.week-day-row { align-items: stretch; }
.week-day-row .dish-card { flex: 1; min-width: 0; }
/* A day's card stacks two corner buttons (status on top, the swap button
   below it) where the recipe list only ever has one, so it needs a taller
   thumb floor to fit them without touching — but shrinking the buttons
   themselves (below) keeps that floor close to the list's own 96px instead
   of needing app-wide 48px .btn-icon boxes twice over. */
.week-day-row .dish-thumb { min-height: 96px; }
.week-day-row .dish-corner { min-width: 34px; min-height: 34px; }
.week-day-row .dish-corner i { font-size: 15px; }
/* Right padding stays the default 56px (not a tighter override) — the
   empty-day card carries the same bottom-right swap button as a filled one
   now, and needs the same gutter clearance for it. */
.dish-card-empty-day { cursor: pointer; }
.dish-card-empty-day:hover .dish-name,
.dish-card-empty-day:focus-visible .dish-name { color: var(--color-primary-dark); }
.drag-handle {
  flex: none;
  border-color: transparent;
  background: transparent;
  color: var(--color-text-secondary);
  cursor: grab;
  touch-action: none;
}
.drag-handle-outside { align-self: center; min-width: 24px; padding-left: 4px; padding-right: 4px; }
.drag-ghost {
  position: fixed;
  z-index: 1000;
  opacity: 0.9;
  pointer-events: none;
  box-shadow: 0 8px 24px rgba(45, 41, 38, 0.3);
}
.drag-source { opacity: 0.35; }
.drop-target-hover { border-color: var(--color-primary); box-shadow: 0 0 0 2px var(--color-primary); }
.dish-name {
  font-size: 17px;
  font-weight: 700;
  margin-bottom: 2px;
  overflow-wrap: anywhere;
  display: inline-block;
  color: var(--color-text-primary);
  text-decoration: none;
}
a.dish-name:hover { text-decoration: underline; }

/* Status/close-save sit top-right, out of the normal flow so they read as
   a fixed card-level action rather than a list item. */
.dish-corner {
  position: absolute;
  right: var(--space-2);
  border-color: transparent;
  background: transparent;
}
.dish-corner-top { top: var(--space-2); }
.dish-corner-center { top: 50%; transform: translateY(-50%); }
/* The edit-mode close/save button swaps into this same top-right slot (see
   closeEditBtn in render.js) but sits noticeably closer to the corner than
   the status icon it replaces — closer to the card's own edge reads more
   like "leave this screen" than a same-weight sibling action. */
.dish-corner-close { top: var(--space-1); right: var(--space-1); }
/* Matveckan-only: sits under the status/thumb button, so a day's card can
   swap its dish without opening the full edit form. Hidden while that same
   card's edit form is open — floating over the "Ta bort receptet" button
   down there reads as broken, and the swap popup would fight the edit form
   for the modal layer if both landed open at once. */
.dish-corner-bottom { bottom: var(--space-2); }
.dish-card:has(.dish-edit-wrap:not([hidden])) .dish-corner-bottom { display: none; }
.dish-corner-status { border-radius: 50%; }
/* Same status colors as .status-badge, restated here with matching
   specificity so they win over .dish-corner's transparent background
   regardless of source order. */
.dish-corner-status.status-funkar { background: #E7EFEA; color: #4E7460; }
.dish-corner-status.status-funkar_ibland { background: #FCF0D6; color: #8A6A1B; }
.dish-corner-status.status-funkar_inte { background: #FBE1EA; color: #B0507A; }
.dish-corner-status.status-otestad { background: #E7E5E3; color: var(--color-text-secondary); }
/* Shrunk well below .btn-icon's 48px touch-target box — that box's own
   padding put visual daylight between the glyph and the edge even once the
   box itself sat at the same offset as the status icon's box. margin-bottom
   then pulls it in from .dish-body's padding-bottom (var(--space-4)) so the
   *glyph* ends up level with the status icon's, not just the invisible box. */
.dish-expand-toggle {
  display: flex;
  align-self: center;
  align-items: center;
  justify-content: center;
  min-width: 0;
  min-height: 0;
  padding: 2px var(--space-2);
  margin-top: auto;
  margin-bottom: calc(-1 * var(--space-1) - 8px);
  border-color: transparent;
  background: transparent;
}
/* A sibling of the thumb+name row rather than nested inside it, so it
   naturally spans the full card width — the divider reads edge-to-edge,
   already meeting the thumbnail without any extra bleed. padding-left
   lines the actual content back up with the name above it. */
.dish-expand {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
  padding-top: var(--space-3);
  padding-bottom: var(--space-4);
  padding-left: var(--space-3);
  padding-right: 56px;
  border-top: 1px solid var(--color-border);
}
.dish-expand .recipe-text { margin: 0; }
/* 2px more air above "Ändra" than the flex gap already gives every other
   pair of .dish-expand children — it's the one action button in a stack of
   read-only content, so a touch more separation reads better. */
.dish-edit-open-btn { margin-top: 2px; }
/* Only shown when a dish has more than one photo — the collapsed card only
   ever has room for the starred one, so this is the way to reach the rest
   without going into edit mode. */
.dish-expand-gallery { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.dish-expand-gallery-thumb {
  width: 56px;
  height: 56px;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: transparent;
  cursor: pointer;
}
.dish-expand-gallery-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.dish-categories {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  margin-top: var(--space-1);
}
.dish-category-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: #FCF0D6;
  color: #8A6A1B;
  padding: 2px 10px;
  border-radius: var(--radius-pill);
  font-size: 13px;
  font-weight: 700;
}
.dish-friend-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: #E9EDF4;
  color: var(--color-blue);
  padding: 2px 10px;
  border-radius: var(--radius-pill);
  font-size: 13px;
  font-weight: 700;
}
.dish-friend-badge i { font-size: 13px; line-height: 1; }
.dish-category-badge i { font-size: 13px; line-height: 1; }

/* status badges — color is never the only signal; icon + text always present */
.status-badge {
  display: inline-flex;
  align-self: flex-start;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  margin-top: var(--space-1);
  border-radius: var(--radius-pill);
  font-size: 13px;
  font-weight: 700;
}
.status-badge i { font-size: 13px; line-height: 1; }
.status-funkar { background: #E7EFEA; color: #4E7460; }
.status-funkar_ibland { background: #FCF0D6; color: #8A6A1B; }
.status-funkar_inte { background: #FBE1EA; color: #B0507A; }
.status-otestad { background: #E7E5E3; color: var(--color-text-secondary); }

.recipe-text {
  white-space: pre-wrap;
  font-size: 15px;
  line-height: 1.6;
  margin-top: var(--space-2);
  color: var(--color-text-primary);
}

/* ── Forms ──────────────────────────────────────────────────────────── */
.field { margin-bottom: var(--space-4); }
.field label {
  display: block;
  font-weight: 700;
  font-size: 14px;
  margin-bottom: var(--space-2);
}
.field input, .field textarea {
  width: 100%;
  min-height: 48px;
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  padding: var(--space-3) var(--space-4);
  font-size: 16px;
  font-family: inherit;
  color: var(--color-text-primary);
}
.field input { padding: 0 var(--space-4); }
.field textarea { resize: vertical; }
.field input:focus-visible, .field textarea:focus-visible { border-color: var(--color-blue); }
.field-error {
  color: var(--color-primary-dark);
  font-size: 14px;
  margin-top: var(--space-1);
}

/* ── Empty / status states ──────────────────────────────────────────── */
.empty-state {
  text-align: center;
  padding: var(--space-12) var(--space-4);
  color: var(--color-text-secondary);
}
.empty-state h3 { color: var(--color-text-primary); margin-bottom: var(--space-2); }

.banner {
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
  font-size: 15px;
}
.banner-error { background: var(--color-primary-soft); color: var(--color-primary-dark); }
.banner-info { background: #E9EDF4; color: var(--color-blue); }
.banner-success { background: #E7EFEA; color: #4E7460; }

/* ── Toast ──────────────────────────────────────────────────────────────
   Transient confirmations (see toast() in api.js). Fixed and out of flow, so
   showing one never moves the page, and pointer-events stay off the stack so
   the app underneath is still usable — only the toast itself takes a tap, to
   dismiss early. Sits above the bottom nav, clear of the home indicator. */
.toast-stack {
  position: fixed;
  left: 0;
  right: 0;
  bottom: calc(76px + env(safe-area-inset-bottom));
  z-index: 200;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: 0 var(--space-4);
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  max-width: 100%;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--color-text-primary);
  color: #FFF9F1;
  font-size: 15px;
  font-weight: 700;
  text-align: center;
  box-shadow: 0 6px 20px rgba(45, 41, 38, 0.28);
  cursor: pointer;
  animation: toast-in var(--duration-fast) ease;
  transition: opacity var(--duration-standard) ease, transform var(--duration-standard) ease;
}
.toast-success { background: #4E7460; }
.toast-info { background: var(--color-blue); }
.toast-error { background: var(--color-primary-dark); }
.toast-leaving {
  opacity: 0;
  transform: translateY(8px);
}
@keyframes toast-in {
  from { opacity: 0; transform: translateY(8px); }
}
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; transition: none; }
}

/* ── QR box ─────────────────────────────────────────────────────────── */
.qr-box {
  text-align: center;
  padding: var(--space-6);
}
.qr-box img {
  width: 200px;
  height: 200px;
  border: 1px solid var(--color-border);
  background: #fff;
}
.qr-code-text {
  font-size: 22px;
  font-weight: 800;
  letter-spacing: 0.08em;
  margin-top: var(--space-3);
}

/* ── Bottom nav ─────────────────────────────────────────────────────── */
.bottom-nav {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  background: var(--color-surface);
  border-top: 1px solid var(--color-border);
  display: flex;
  padding-bottom: env(safe-area-inset-bottom);
  z-index: 10;
}
.bottom-nav a {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 60px;
  text-decoration: none;
  color: var(--color-text-secondary);
  font-size: 12px;
  font-weight: 700;
  border-top: 3px solid transparent;
}
.bottom-nav a i { font-size: 22px; line-height: 1; }
.bottom-nav a[aria-current="page"] {
  color: var(--color-primary-dark);
  border-top-color: var(--color-primary);
}
/* On a wide screen, four tabs spread edge-to-edge with flex:1 read as a
   sparse mobile tab bar stretched across a desktop window — group them
   together instead, aligned under .page's own (now much narrower) content
   column rather than the full browser width. The bar's own background/
   border-top still spans edge-to-edge underneath, same shelf as on mobile. */
@media (min-width: 700px) {
  body.app-shell .bottom-nav {
    justify-content: center;
    gap: var(--space-12);
  }
  body.app-shell .bottom-nav a {
    flex: none;
    min-width: 88px;
  }
}

/* ── Single-page tab pager ─────────────────────────────────────────────
   The four tabs live in one document (index.html) as horizontally-arranged
   panels. Each panel is its own vertical-scroll column, so the topbar
   scrolls away within a panel exactly as it did when each tab was its own
   page. Scoped to body.app-shell so the standalone deep-link pages
   (connect/join/van-lista) keep the normal document-scroll model.

   Swipe-between-tabs is DISABLED (overflow-x: hidden) — it competed with the
   cards' own horizontal swipes and felt annoying. The pager structure is
   kept intact and tabs are switched programmatically from the bottom nav
   (app-shell.js sets scrollLeft, which still works under overflow:hidden).
   To re-enable swiping, set overflow-x back to auto. */
body.app-shell {
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  padding-bottom: 0;
  display: flex;
  flex-direction: column;
  /* touch-action is set by lockPageZoom() (viewport.js) rather than here, so
     the shell and the friend's-list page get it from one place. */
}
/* Nothing in the shell is supposed to scroll the document — the panels do all
   the scrolling. Saying so on <html> as well means a stray overflowing element
   can never make the whole app draggable/pinch-zoomable inside its window,
   which is what popups used to do in the installed Android app. Scoped with
   :has() so the deep-link pages, which have no .app-shell, keep the normal
   document-scroll model. */
html:has(body.app-shell) { overflow: hidden; }
/* In the shell the nav is a normal flex child at the bottom of the column,
   NOT position:fixed. A fixed bottom bar hides behind the iOS Safari toolbar
   here because the document itself never scrolls (the panels do), so Safari
   never retracts that toolbar. As an in-flow flex row inside the 100dvh
   column it's always visible, above the home-indicator safe area. */
body.app-shell .bottom-nav {
  position: static;
  flex-shrink: 0;
  /* lockPageZoom() (viewport.js) sets pan-x pan-y on <html> and <body> so the
     panels underneath can still scroll — Blink computes touch-action as the
     intersection over the ancestor chain, so that permissive value reaches
     every descendant that doesn't narrow it, the nav included. The nav has
     no scrolling of its own to allow, so touching it was free to be read as
     a page-level pan — on the browsers where body's own scroll extent is
     still misreported (see viewport.js), that pan had somewhere to go,
     dragging the nav along with the finger instead of it staying docked.
     none here narrows the intersection back down for just this element,
     the same way the drag handle and the lightbox photo already do. */
  touch-action: none;
}
.pager {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
  overflow-x: hidden;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.pager::-webkit-scrollbar { display: none; }
.panel {
  flex: 0 0 100%;
  width: 100%;
  height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  /* A panel scrolled to its end must not hand the leftover gesture to the
     page — that's another way to drag the app off its own viewport. */
  overscroll-behavior: contain;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  -webkit-overflow-scrolling: touch;
}
/* The equipment filter is its own horizontal scroller — keep its overscroll
   from chaining out to page the tabs while the user is scrolling it. */
.equipment-filter-row { overscroll-behavior-x: contain; }

/* ── Dialog ─────────────────────────────────────────────────────────────
   Popups are modal <dialog> elements in the browser's top layer (see
   overlay.js). The element itself is stretched over the whole viewport and
   made transparent — the dimming comes from ::backdrop — so it doubles as the
   tap-outside-to-close surface while the UA keeps it inside the window no
   matter what the app shell underneath is doing.

   Sizes are in dvh, and the safe-area insets keep a popup clear of the status
   bar / home indicator / Android system bars in the installed app. Content
   never grows past the viewport: the popup scrolls inside itself instead. */
dialog.modal {
  position: fixed;
  inset: 0;
  /* The real size is set in pixels by fitToViewport() (see viewport.js):
     Samsung Internet resolves both viewport units *and* these percentages
     against a device-pixel viewport, making the popup ~3× too big. These are
     the fallback for engines that resolve them correctly — there they agree
     with what the JS sets. */
  width: 100%;
  max-width: 100%;
  height: 100%;
  max-height: 100%;
  margin: 0;
  border: 0;
  background: transparent;
  color: inherit;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  padding:
    calc(var(--space-4) + env(safe-area-inset-top))
    calc(var(--space-4) + env(safe-area-inset-right))
    calc(var(--space-4) + env(safe-area-inset-bottom))
    calc(var(--space-4) + env(safe-area-inset-left));
}
/* The backdrop deliberately does NOT set touch-action: none. It used to, to
   stop the page zooming behind an open popup — but that also meant a popup
   that did land off-screen could not be panned to, since every touch hit the
   modal and it refused to pan. Leaving browser gestures alone on the backdrop
   keeps a way back. The photo lightbox still does its own zooming
   (attachPinchZoom), and .dialog-box below keeps its own scrolling. */
dialog.modal:not([open]) { display: none; }
dialog.modal::backdrop { background: rgba(45, 41, 38, 0.5); }
/* No top layer on browsers without <dialog> (see overlay.js): there the
   element is a plain fixed overlay, so it needs the dimming and a stacking
   order of its own. */
dialog.modal[data-fallback] {
  background: rgba(45, 41, 38, 0.5);
  z-index: 100;
}

.dialog-box {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  padding: var(--space-6);
  max-width: 400px;
  width: 100%;
  max-height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-y;
  box-shadow: 0 8px 24px rgba(45, 41, 38, 0.18);
}
.dialog-box p { margin-bottom: var(--space-4); }

/* First-run tutorial (three steps, one .dialog-box reused for all of them —
   see maybeShowTutorial() in app-shell.js). */
.tutorial-box { position: relative; text-align: center; }
.tutorial-skip {
  position: absolute;
  top: var(--space-4);
  right: var(--space-4);
  font-size: 14px;
}
/* background-image rather than <img src>: until a real illustration file
   exists at this path, a missing background-image just leaves the plain
   background colour showing, not a broken-image glyph. */
.tutorial-illustration {
  height: 180px;
  margin: var(--space-8) 0 var(--space-4);
  border-radius: var(--radius-lg);
  background-color: var(--color-primary-soft);
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
}
.tutorial-box h2 { margin-bottom: var(--space-2); }
.tutorial-box .text-body { color: var(--color-text-secondary); }
.tutorial-dots { display: flex; justify-content: center; gap: var(--space-2); margin: var(--space-2) 0 var(--space-4); }
.tutorial-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--color-border); }
.tutorial-dot.active { background: var(--color-primary); }
.tutorial-box .btn { width: 100%; }

.image-lightbox-modal { cursor: zoom-out; }
/* Explicit width/height (not just max-*) so .image-lightbox-img's own
   max-height:100% has something to resolve against — a percentage height on
   a child means nothing unless the parent's own height is set, not just
   bounded. */
.image-lightbox-wrap {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  width: 100%;
  height: 100%;
}
.image-lightbox-img {
  flex: 1 1 auto;
  min-height: 0;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: var(--radius-lg);
  box-shadow: 0 8px 24px rgba(45, 41, 38, 0.3);
  cursor: default;
  /* attachPinchZoom anchors its maths on the element's centre, and the popup
     clips the zoomed photo to the visible window rather than letting it spill
     off-screen (dialog.modal has overflow:hidden). */
  transform-origin: 50% 50%;
  will-change: transform;
}
.image-lightbox-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(45, 41, 38, 0.55);
  border-color: transparent;
  color: #fff;
  min-width: 44px;
  min-height: 44px;
}
.image-lightbox-nav:hover { background: rgba(45, 41, 38, 0.75); }
.image-lightbox-prev { left: var(--space-2); }
.image-lightbox-next { right: var(--space-2); }
.image-lightbox-dots { display: flex; flex: none; gap: 8px; }
.image-lightbox-dots span {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.4);
}
.image-lightbox-dots span.active { background: #fff; }

.content-lightbox-box {
  max-width: 560px;
}
.content-lightbox-box h3 { font-size: 19px; }
.content-lightbox-box .recipe-text { margin-bottom: var(--space-4); }
.dish-picker-search {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding: 0 var(--space-3);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  color: var(--color-text-secondary);
}
.dish-picker-search input {
  flex: 1;
  min-width: 0;
  border: none;
  outline: none;
  min-height: 44px;
  font-size: 16px;
  font-family: inherit;
  background: transparent;
}
.dish-picker-list.stack { margin-top: var(--space-3); gap: var(--space-2); }
.dish-picker-row {
  align-items: center;
  width: 100%;
  text-align: left;
  background: var(--color-surface);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-2);
  cursor: pointer;
  font-family: inherit;
}
.dish-picker-row .dish-thumb { width: 48px; min-height: 48px; height: 48px; border-radius: var(--radius-sm); }
.dish-picker-row .dish-name { font-size: 15px; margin: 0; flex: 1; font-weight: 700; }
.dish-picker-row-selected { border-color: var(--color-primary); background: var(--color-primary-soft); }
.dish-picker-row-selected i { color: var(--color-primary-dark); font-size: 18px; }
.weekday-picker-btn { justify-content: space-between; width: 100%; }
.weekday-picker-current {
  font-size: 14px;
  font-weight: 400;
  color: var(--color-text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 55%;
}
.content-lightbox-pdf {
  width: 100%;
  /* A flat pixel height rather than a viewport unit, for the same reason as
     .modal above. The popup scrolls, so a too-tall preview is harmless. */
  height: 320px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  margin-bottom: var(--space-3);
}

/* ── Multi-image field (edit mode's gallery, and the add form's own draft
   version of it) ──────────────────────────────────────────────────── */
.multi-image-grid {
  display: flex;
  flex-wrap: wrap;
  /* Taller row-gap than column-gap — the rotate button still pokes out 10px
     below each cell (star/remove sit inside the image now, so they no
     longer need the extra clearance themselves). */
  gap: var(--space-4) var(--space-3);
  margin-bottom: var(--space-3);
}
.multi-image-grid:empty { display: none; margin-bottom: 0; }
.multi-image-cell { position: relative; width: 96px; height: 96px; }
.multi-image-cell img {
  width: 96px;
  height: 96px;
  object-fit: cover;
  display: block;
  border: 1px solid var(--color-border);
}
.multi-image-star, .multi-image-remove, .multi-image-rotate {
  position: absolute;
  min-width: 32px;
  min-height: 32px;
  background: var(--color-surface);
  box-shadow: 0 1px 3px rgba(45, 41, 38, 0.2);
}
/* Star and remove sit inside the photo's own edges (not overlapping past
   its border, unlike the single-image field's remove button) — a small
   inset from each top corner. */
.multi-image-star { top: 6px; left: 6px; }
.multi-image-remove { top: 6px; right: 6px; }
.multi-image-rotate { bottom: -10px; left: 50%; transform: translateX(-50%); }
.multi-image-star i, .multi-image-remove i, .multi-image-rotate i { font-size: 15px; line-height: 1; }
.multi-image-star.starred { color: var(--color-yellow); }
.multi-image-star:disabled { opacity: 1; cursor: default; }

/* ── File field ─────────────────────────────────────────────────────── */
.file-field-preview {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.file-field-preview:empty { display: none; margin-bottom: 0; }
.file-field-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 0 var(--space-4);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-blue);
  font-weight: 700;
  text-decoration: none;
}
.file-field-link i { font-size: 17px; line-height: 1; }

/* ── Misc ───────────────────────────────────────────────────────────── */
.stack { display: flex; flex-direction: column; gap: var(--space-4); }
.row { display: flex; align-items: center; gap: var(--space-3); }
.spread { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.muted { color: var(--color-text-secondary); }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
/* <input type=file>'s native control can ignore width:1px, so it needs a
   clipping wrapper rather than just being shrunk — and it must stay near its
   natural position (not translated far off-canvas) or some mobile browsers
   (notably iOS Safari) silently refuse to honor a programmatic .click() on it. */
.file-input-wrap {
  position: relative;
  display: inline-block;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
.file-input-hidden {
  position: absolute;
  top: 0;
  left: 0;
  opacity: 0;
}

/* ── Accounts, sign-in and household switching ──────────────────────────
   The account card sits at the top of Inställningar and has two moods: a
   plain receipt when you are signed in, and a warning when the profile is
   temporary — which is a real risk, not a nag, since such a list lives in
   exactly one browser. .card-warn is the same card with the yellow already
   used elsewhere for "worth reading" rather than the red reserved for
   destructive actions. */
.card-warn {
  border-color: var(--color-yellow);
  background: #FFFCF3;
}
.card-warn .text-help { color: #8A5C10; }
.card-warn i { color: #B4741A; }

.account-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex: none;
  border-radius: var(--radius-pill);
  background: var(--color-primary-soft);
  color: var(--color-primary-dark);
}
/* An email is the one string here with no natural wrap point, so it gets the
   leftover width and an ellipsis rather than pushing the row sideways. */
.account-email {
  font-weight: 700;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* One row per household in the switcher. A button, not a radio: tapping it
   is an action (it reloads into that household), not a form value. */
.household-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 56px;
  padding: var(--space-2) var(--space-3);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text-primary);
  font-family: inherit;
  font-size: 16px;
  text-align: left;
  cursor: pointer;
}
.household-row:hover:not(:disabled) { border-color: var(--color-text-secondary); }
/* The active one is disabled, so the usual dimmed :disabled look would read
   as "unavailable" for the row that is in fact the current one. */
.household-row:disabled { cursor: default; opacity: 1; }
.household-row.is-active {
  border-color: var(--color-primary);
  background: var(--color-primary-soft);
}
.household-mark { flex: none; color: var(--color-text-secondary); }
.household-row.is-active .household-mark { color: var(--color-primary-dark); }
.household-name { flex: 1; min-width: 0; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.household-badge { flex: none; color: var(--color-primary-dark); }

/* "eller" between two alternatives, with a rule running out to each side. */
.divider {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--color-text-secondary);
  font-size: 14px;
}
.divider::before, .divider::after {
  content: "";
  height: 1px;
  flex: 1;
  background: var(--color-border);
}

/* Google's own button doesn't always fill its slot: signed out, GIS renders
   text that happens to stretch close to full width; signed into a Google
   account in the browser, it renders a narrower personalized chip instead,
   which the slot's default align-items:stretch has nothing to act on — the
   chip just sits at the left edge. Centering instead of stretching this one
   slot works in both cases regardless of the button's actual width. */
.signin-slot-google { align-items: center; }

/* Onboarding steps stack their own children; these two just name the gap so
   the markup doesn't carry inline styles for it. */
.stack.gap16 { gap: var(--space-4); }
.stack.gap24 { gap: var(--space-6); }

/* ── Onboarding hero (first-run login screen) ────────────────────────────
   The one screen shown before there's any household to theme around — a
   warm radial glow behind the brand mark instead of the flat background the
   rest of the app uses, and pill-shaped buttons to match the mark's own
   rounded lettering. Scoped to .onboarding-hero-content rather than .btn
   globally, so every other screen keeps the app's usual --radius-md. */
#onboarding-screen {
  min-height: 100vh;
  background: radial-gradient(ellipse 640px 420px at 50% -40px, #FFFCF2, var(--color-background) 65%);
}
/* Vertically centers .page's content (the topbar is hidden on this screen —
   see renderOnboardingHero()) within the viewport instead of just starting
   at the top. Stays on through the create-account/create-or-join screens
   reached from here, which keep this same header; turned back off for the
   signed-in-with-no-household entry point and the starter-dishes welcome
   screen, which can run long enough that centering would push its top
   off-screen. */
#onboarding-screen.onboarding-screen-centered {
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* The hero/start-choice screen shouldn't scroll. app-shell.js pins
   #onboarding-screen's own width/height to the real viewport once via
   fitToViewport() (see there for why) — clipping to that pinned box only
   when this class is present is what actually stops the scroll; without it
   (create-or-join, the welcome screen) content taller than the pinned box
   just overflows the box visibly and the page scrolls as normal. */
#onboarding-screen.onboarding-screen-no-scroll {
  overflow: hidden;
}
/* Docked to the bottom edge regardless of scroll — left:0/right:0 rather
   than width:100% for the same reason .toast-stack uses that pair (see
   viewport.js): on the affected browsers a percentage width resolves
   against an inflated device-pixel viewport, not the real one. The
   fitToViewport() call in app-shell.js corrects it the same way, to the
   real documentElement.clientWidth. */
.onboarding-copyright {
  position: fixed;
  left: 0;
  right: 0;
  bottom: env(safe-area-inset-bottom);
  padding: var(--space-2) 0;
  text-align: center;
  font-size: 12px;
  color: var(--color-text-secondary);
  pointer-events: none;
}
/* Matches .page's own max-width — one column width across the whole
   interface, onboarding included, not just the main app's four panels. */
.onboarding-hero-header, .onboarding-hero-content, #welcome {
  max-width: 600px;
  margin-left: auto;
  margin-right: auto;
}
.onboarding-hero-header {
  text-align: center;
  align-items: center;
  margin-bottom: var(--space-8);
}
/* logo.png is the wordmark, not the square icon — sized by width with
   height:auto to keep its own proportions, no crop/radius/shadow needed for
   a mark that's meant to sit directly on the page background. */
.onboarding-hero-image {
  width: 280px;
  max-width: 100%;
  height: auto;
}
/* The bolt easter egg: a transparent hotspot over roughly where the bolt
   sits in logo.png (the right ~quarter of the image, reaching from near the
   top — the burst lines — down to just above the wordmark's baseline), and
   the speech bubble it pops up above the whole logo. */
.logo-wrap { position: relative; display: inline-block; }
.logo-bolt-hotspot {
  position: absolute;
  top: 0;
  right: 0;
  left: 76%;
  bottom: 12%;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
}
.logo-bubble {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-bottom: var(--space-3);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  background: var(--color-surface);
  border: 1.5px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-4);
  box-shadow: 0 8px 20px rgba(45, 41, 38, 0.18);
  font-weight: 700;
  font-size: 15px;
  white-space: nowrap;
  color: var(--color-text-primary);
  opacity: 0;
  pointer-events: none;
}
.logo-bubble::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 7px solid transparent;
  border-top-color: var(--color-surface);
}
.logo-bubble.visible {
  animation: logo-bubble-pop 320ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
@keyframes logo-bubble-pop {
  0% { opacity: 0; transform: translateX(-50%) translateY(8px) scale(0.7); }
  60% { opacity: 1; transform: translateX(-50%) translateY(-4px) scale(1.05); }
  100% { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .logo-bubble.visible { animation: none; opacity: 1; transform: translateX(-50%); }
}
.onboarding-hero-header .text-body { color: var(--color-text-secondary); }
.onboarding-hero-content .btn { border-radius: var(--radius-pill); }
/* Opts a button back out of the pill above — same corners as .btn-primary
   everywhere else in the app (Matveckan, Lista). Extra class beats the rule
   above on specificity, not source order. */
.onboarding-hero-content .btn.btn-standard-radius { border-radius: var(--radius-md); }
/* Radius no longer needs setting here — .field input's own base rule is
   --radius-md now, matching everywhere else in the app. */
.onboarding-hero-content .field input { padding-left: var(--space-6); }
.onboarding-footer { text-align: center; margin-top: var(--space-2); }

/* The one-time code field. Wide letter-spacing and a large tabular face so six
   digits read as six separate digits — misreading one costs a whole round trip
   through the mailbox.

   Selector has to out-specify `.field input`, which sets font-size: 16px and
   would otherwise win on specificity over a lone class. */
.field input.code-input {
  font-size: 28px;
  min-height: 60px;
  font-weight: 800;
  letter-spacing: 0.32em;
  text-align: center;
  font-variant-numeric: tabular-nums;
  /* Letter-spacing is applied after the last glyph too, which shifts centred
     text visually left by half a space. */
  text-indent: 0.32em;
}

/* The invite link, printed in full under the join QR. It is the thing the user
   is being asked to send, so it has to be readable and selectable — not tucked
   behind a copy button that fails silently when the clipboard is refused.
   break-all because a URL has no spaces to wrap at and would otherwise widen
   the whole settings panel. */
.invite-link {
  display: block;
  margin-top: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text-secondary);
  font-size: 13px;
  line-height: 1.5;
  text-align: center;
  text-decoration: none;
  word-break: break-all;
  /* Tapping is prevented in JS (it would just reopen the app here), so don't
     advertise it as a link to follow — but keep it selectable. */
  cursor: text;
  user-select: all;
}
