:root {
  --primary: #2563a8;
  --primary-dark: #14406e;
  /* Same navy as --primary-dark in light mode (no visual change there) —
     exists purely so dark mode below can brighten just the text-color uses
     of that navy (prices, links, badges) without also brightening
     --primary-dark's *background* uses (button hover, toast, calendar
     selected day, chat bubbles), which need to stay dark enough for white
     text on top to stay readable. */
  --link-blue: #14406e;
  --bg: #f7f6f2;
  --card-bg: #ffffff;
  --text: #232323;
  --muted: #6b6b6b;
  --border: #e2e0d9;
  --danger: #b3261e;
}

/* Dark mode — toggled via the settings-menu switch (app.js), which sets/
   removes this attribute on <html> and persists the choice to
   localStorage. Every color in the site is already driven by these eight
   variables (that's the whole point of having them), so redefining them
   here is enough to re-theme almost everything automatically. --primary
   and --primary-dark are brightened rather than reused as-is: the light
   values are dark navy blues meant to sit on a light/white background, and
   several places use them as plain text color directly on --bg/--card-bg
   (price tags, badges, links) — unchanged, that text would have gone
   almost illegible on a dark background. The brighter pair here still
   reads clearly as text *and* still has enough contrast with white button
   labels where it's used as a background instead. A handful of
   intentionally-fixed-color elements (the Google button, which brand
   guidelines require to stay light; the lightbox, which already has its
   own dark backdrop regardless of page theme) are deliberately left out of
   this and don't need any dark-mode handling. */
:root[data-theme="dark"] {
  /* Tells the browser to use its own dark-appropriate rendering for
     anything author CSS can't fully reach — most relevantly here, a native
     <select>'s dropdown option list and scrollbars, which some browsers
     render with their own light-mode defaults regardless of the select's
     own CSS color/background otherwise. */
  color-scheme: dark;
  --primary: #4d8fd6;
  --primary-dark: #2f6fb8;
  --link-blue: #62a5ea;
  --bg: #1b1a17;
  --card-bg: #262420;
  --text: #f0efe9;
  --muted: #a8a39a;
  --border: #3a372f;
  --danger: #d9564c;
}

/* Native form controls don't inherit page colors on their own — without
   this every input/select/textarea would stay glaring white with black
   text against a dark page. */
input,
select,
textarea {
  background: var(--card-bg);
  color: var(--text);
}

/* One blanket rule instead of hunting down every @keyframes/transition
   individually (card fade-ins, the intro slideshow, spinners, the
   success/favorite-heart pop, the lightbox, skeleton shimmer, toasts,
   hover lifts) — collapses all of them to effectively instant for anyone
   with reduced-motion set at the OS level, which is the standard approach
   recommended for this rather than a per-component opt-out. Spinners still
   convey "loading" as a static shape even without spinning. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  /* Instrument Sans — loaded as a real webfont from Google Fonts (see the
     <link> tags in every page's <head>), not a system-font stack. Swapped
     in from Inter, which by now reads as "generic AI-tool starter kit"
     (it's the default in shadcn/ui, Tailwind templates, v0, etc.) — this
     has more warmth/character while staying just as clean and legible at
     UI text sizes. Like Inter before it, this renders identically for
     every visitor regardless of OS, since it's not relying on whatever
     happens to already be installed. */
  font-family: "Instrument Sans", -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
  line-height: 1.5;
  animation: pageFadeIn 0.45s ease-out;
}

@keyframes pageFadeIn {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.container {
  max-width: 1100px;
  margin: 0 auto;
  padding: 0 20px;
}

/* Most pages have no <footer> at all (only the homepage and the two legal
   pages do), so their last piece of content — a reviews list, a form, a
   table — had nothing stopping it from sitting flush against the bottom
   of the viewport once scrolled all the way down. Scoped to <main> (not
   .container generally, which is also reused for the header's inner
   wrapper) so it applies once, site-wide, instead of needing a per-page
   fix every time this comes up. */
main.container {
  padding-bottom: 60px;
}

.container-narrow {
  max-width: 560px;
}

.container-wide {
  max-width: 1400px;
}

.admin-dashboard-header {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
  margin-top: 20px;
}

.admin-updated-at {
  margin: 0;
  font-size: 0.82rem;
  color: var(--muted);
}

.admin-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 16px;
  margin: 12px 0 40px;
}

.admin-chart-section {
  margin-bottom: 40px;
}

.admin-chart-wrap {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 18px 20px;
}

.admin-stat-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 18px 20px;
}

.admin-stat-value {
  font-size: 1.8rem;
  font-weight: 700;
  color: var(--link-blue);
  margin: 0 0 4px;
}

.admin-stat-label {
  font-size: 0.85rem;
  color: var(--muted);
  margin: 0;
}

.admin-stat-detail {
  font-size: 0.78rem;
  color: var(--muted);
  margin: 4px 0 0;
}

.admin-section {
  margin-bottom: 48px;
}

.admin-section h2 {
  font-size: 1.15rem;
  margin: 0 0 14px;
}

.admin-table-wrap {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: 12px;
}

.admin-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.88rem;
}

.admin-table th,
.admin-table td {
  text-align: left;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

.admin-table th {
  background: var(--bg);
  color: var(--muted);
  font-weight: 600;
  font-size: 0.8rem;
}

.admin-table tr:last-child td {
  border-bottom: none;
}

.admin-table td.admin-cell-wrap {
  white-space: normal;
  max-width: 280px;
}

/* Pins the Actions column to the right edge of the scroll area instead of
   letting it scroll away with the rest of the row — otherwise on the wider
   tables (bookings has 7 columns) it disappears off-screen until you
   scroll all the way right. Needs its own solid background (matching the
   page background these cells actually sit on, not the table's own —
   .admin-table-wrap has no background of its own) so content scrolling
   underneath doesn't show through the sticky column. */
.admin-table th:last-child,
.admin-table td:last-child {
  position: sticky;
  right: 0;
  white-space: nowrap;
  box-shadow: -6px 0 8px -6px rgba(0, 0, 0, 0.15);
}

.admin-table td:last-child {
  background: var(--bg);
  z-index: 1;
}

.admin-table th:last-child {
  z-index: 2;
}

.admin-table .btn-sm {
  margin: 0 4px 0 0;
}

.admin-badge {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
}

.admin-badge-ok {
  background: #e5eef9;
  color: var(--link-blue);
}

.admin-badge-warn {
  background: #fbe9e7;
  color: var(--danger);
}

.site-header {
  /* Matches logo2.png's background so the logo blends into the header
     with no visible edge. Sampled exactly via Digital Color Meter
     (r63 g91 b124), not estimated. */
  background: #3f5b7c;
  padding: 16px 0;
}

.header-inner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

/* Every direct button/link in the header nav row (including the settings
   icon button) shares one fixed height so they line up exactly, regardless
   of whether their content is text or an icon. */
.header-actions .btn {
  height: 42px;
  margin: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* .btn-secondary's default (dark text, light border) assumes a white
   background — fine everywhere else it's used (forms, cards), but
   unreadable against the header's dark background, so it needs light text
   here specifically. Scoped to .header-actions rather than changing
   .btn-secondary itself, which stays dark-on-light everywhere outside the
   header. This also reaches the mobile drawer's cloned nav links (see the
   NOTE above .header-actions .nav-icon-btn below) — reset back to normal
   dark-on-light there, since .settings-menu-panel has a white background,
   not the header's dark one. */
.header-actions .btn-secondary {
  color: #fff;
  border-color: rgba(255, 255, 255, 0.45);
}

.header-actions .btn-secondary:hover {
  background: rgba(255, 255, 255, 0.12);
}

/* "+ Dodaj narzędzie" is the one .btn-primary in the header. Its background
   is pinned to a fixed hex here rather than left on var(--primary) — the
   header itself (.site-header, above) is a fixed navy that doesn't change
   with dark mode, but var(--primary) does (see :root[data-theme="dark"]
   near the top of this file), so leaving it themed made the button drift
   out of sync with the header depending on site theme. The color itself is
   a lightened tint of the header's own navy (not a generic saturated blue)
   so the button reads as "a highlighted version of this header" rather
   than a differently-hued shape dropped on top of it — much less jarring
   than the earlier plain --primary blue. Border thinned down to a light
   hairline just to separate the edges, not to draw attention on its own. */
.header-actions .btn-primary {
  background: #657c96;
  border: 1px solid rgba(255, 255, 255, 0.45);
}

.header-actions .btn-primary:hover {
  background: #4b6081;
  border-color: rgba(255, 255, 255, 0.7);
}

/* Unread-message count badge, injected next to the "Wiadomości" label (see
   prepareMessagesNavBadges + updateUnreadBadges in app.js). Present on both
   the header link and its mobile-drawer clone. */
.nav-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  margin-left: 6px;
  border-radius: 999px;
  background: var(--danger);
  color: #fff;
  font-size: 0.68rem;
  font-weight: 700;
  line-height: 1;
  vertical-align: middle;
}

/* Author display rule above beats the browser's UA [hidden] rule
   regardless of specificity — same fix needed for .intro-slideshow[hidden]
   and .welcome-gate[hidden] elsewhere in this file. */
.nav-badge[hidden] {
  display: none;
}

/* Collapses "Moje ogłoszenia" / "Moje rezerwacje" / "Ulubione" /
   "Wiadomości" down to icon-only circular buttons in the header row — with
   4 of these plus the auth control and settings gear, the full-text
   version was too wide even on desktop.

   NOTE: .settings-menu-panel (and its cloned .settings-menu-nav copies of
   these same links, see initMobileNavClone in app.js) lives INSIDE
   .header-actions in the DOM — it's nested inside .settings-menu, which is
   itself a header-actions child — so a plain .header-actions .nav-icon-btn
   descendant selector matches the drawer clones too, not just the real
   header buttons. That used to collapse the drawer copies to unlabeled
   42px icon circles as well, making "My listings"/"My bookings"/
   "Favorites"/"Messages" impossible to identify on mobile. Fixed below by
   adding explicit reset rules scoped to .settings-menu-nav, which — at
   equal selector specificity — win the cascade by coming later in the
   file, restoring the normal labeled-button look inside the drawer only. */
.header-actions .nav-icon-btn {
  width: 42px;
  height: 42px;
  padding: 0;
  position: relative;
}

/* Removes the outline from just the real header icon buttons (My bookings/
   Favorites/Messages) — a child combinator on purpose, unlike the
   descendant selector above: the drawer clones aren't a *direct* child of
   .header-actions (they're nested inside .settings-menu), so this doesn't
   reach them and their own border (reset separately below for the drawer,
   where a visible outline is still wanted against the menu's white
   background) is left alone. */
.header-actions > .nav-icon-btn {
  border: none;
}

.header-actions .nav-icon-btn .nav-icon-label {
  display: none;
}

/* Corner notification dot instead of an inline chip, since there's no
   label text next to it to sit beside in this context. */
.header-actions .nav-icon-btn .nav-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  margin-left: 0;
}

.nav-icon-btn .icon {
  width: 20px;
  height: 20px;
}

.logo-block {
  display: flex;
  flex-direction: column;
  gap: 2px;
  text-decoration: none;
}

.logo-img {
  /* The transparent logo file (logotr2.png.001.png, shared with the intro
     slideshow) is exported on a wide canvas with a lot of empty transparent
     space above and below the actual wordmark — at height:28px/width:auto
     that padding was included in the scaling, so the visible letters ended
     up tiny and stretched-looking ("flattened"). Fixing the box to a ratio
     that matches just the lettering (roughly 4.5:1) and using object-fit:
     cover crops that empty padding away instead of scaling it down with
     the rest of the canvas — the logo renders at the same size/crop it had
     as a tightly-cropped flat-background image, just genuinely transparent
     now instead of color-matched to the header background. */
  height: 28px;
  width: 126px;
  object-fit: cover;
  object-position: center;
  display: block;
}

.logo-tagline {
  font-size: 0.7rem;
  font-weight: 400;
  /* var(--muted) is a dark gray, meant for a white header — the header
     background is now dark, so this needs to be light instead. Only used
     inside .site-header, so overriding it directly here is safe. */
  color: rgba(255, 255, 255, 0.75);
}

.auth-nav {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: 24px;
}

.lang-switcher {
  padding: 6px 10px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--card-bg);
  color: var(--text);
  font-size: 0.85rem;
  cursor: pointer;
}

/* Settings hamburger menu (header) — holds the language switcher (and any
   future account-level settings) behind one icon instead of a growing row
   of controls next to the nav links. */
.settings-menu {
  position: relative;
}

/* Just the three lines on the dark header background — no button box —
   matching the transparent/white-on-hover treatment already used for the
   header's other icon buttons (.header-actions .btn-secondary above). */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  margin: 0;
  flex-shrink: 0;
  padding: 0;
  border-radius: 8px;
  border: none;
  background: transparent;
  color: #fff;
  cursor: pointer;
}

.icon-btn:hover {
  background: rgba(255, 255, 255, 0.12);
}

.icon-btn .icon-menu {
  width: 26px;
  height: 26px;
}

.settings-menu-panel {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 200;
  background: var(--card-bg);
  border: none;
  border-left: 1px solid var(--border);
  border-radius: 0;
  box-shadow: -10px 0 32px rgba(0, 0, 0, 0.18);
  padding: 28px 22px;
  width: 300px;
  max-width: 85vw;
  overflow-y: auto;
  /* Slide-out drawer: sits just off the right edge by default and slides
     into view on open. transform handles the motion; visibility keeps it
     out of the tab order while closed without blocking the transition
     (per spec, a `visibility` transition flips to hidden only once the
     paired transform transition finishes, and flips to visible instantly
     on open — so closing still gets to animate out first). */
  transform: translateX(100%);
  visibility: hidden;
  transition: transform 0.28s ease, visibility 0.28s ease;
}

/* The browser's UA stylesheet hides [hidden] elements via `display: none`,
   which would skip the transition entirely. This author rule (which wins
   over the UA rule regardless of specificity, since author styles always
   beat user-agent styles) keeps the panel rendered so the slide-out/in
   above can actually animate; the transform + visibility above do the
   real hiding instead. */
.settings-menu-panel[hidden] {
  display: block;
}

.settings-menu-panel:not([hidden]) {
  transform: translateX(0);
  visibility: visible;
}

/* Dim backdrop behind the drawer, faded in/out alongside the slide. Sits
   in the panel's own stacking context (z-index: -1 relative to the panel,
   not the page) so it layers above normal page content but behind the
   drawer itself. Stops at `right: 300px` — matching the drawer's own
   width — rather than covering the full viewport: a negative-z-index
   child still paints on top of its own element's background (that's the
   stacking-order rule), so if this extended under the drawer too, the
   dim tint would wash out the drawer's white background along with it.
   Non-interactive — clicks pass through to the page, which already
   bubbles up and closes the menu via the existing document click
   listener in app.js. */
.settings-menu-panel::before {
  content: "";
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 300px;
  z-index: -1;
  background: rgba(15, 23, 32, 0);
  pointer-events: none;
  transition: background 0.28s ease;
}

.settings-menu-panel:not([hidden])::before {
  background: rgba(15, 23, 32, 0.35);
}

.settings-menu-title {
  font-weight: 700;
  font-size: 0.85rem;
  margin: 0 0 10px;
  color: var(--text);
}

.settings-menu-label {
  display: block;
  font-size: 0.8rem;
  color: var(--muted);
  margin-bottom: 6px;
}

.settings-menu-panel .lang-switcher {
  width: 100%;
}

/* Dark mode switch, injected by initThemeToggle() (app.js) right below the
   language switcher. */
.settings-menu-theme {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

.theme-toggle-label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: 0.9rem;
  color: var(--text);
  cursor: pointer;
}

.theme-toggle-switch {
  position: relative;
  display: inline-block;
  width: 40px;
  height: 22px;
  flex-shrink: 0;
}

.theme-toggle-switch input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  z-index: 1;
}

.theme-toggle-track {
  position: absolute;
  inset: 0;
  background: var(--border);
  border-radius: 999px;
  transition: background 0.15s ease;
}

.theme-toggle-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--card-bg);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  transition: transform 0.15s ease;
}

.theme-toggle-switch input:checked ~ .theme-toggle-track {
  background: var(--primary);
}

.theme-toggle-switch input:checked ~ .theme-toggle-track .theme-toggle-thumb {
  transform: translateX(18px);
}

.theme-toggle-switch input:focus-visible ~ .theme-toggle-track {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

.settings-menu-legal {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.settings-menu-admin {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.settings-menu-admin a {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--link-blue);
  text-decoration: none;
}

.settings-menu-admin a:hover {
  text-decoration: underline;
}

.settings-menu-legal a {
  font-size: 0.8rem;
  color: var(--muted);
  text-decoration: none;
}

.settings-menu-legal a:hover {
  color: var(--link-blue);
  text-decoration: underline;
}

/* Holds clones of the header's page-nav links (My listings, Messages,
   + Add tool, etc — see the JS clone step in app.js) so they're reachable
   from the hamburger drawer once the mobile media query above hides the
   originals from the header row. display: none by default (desktop never
   needs this, since the real links are visible right in the header there);
   the mobile media query flips it back to a visible column of full-width
   buttons, styled to match .settings-menu-legal's own separator treatment. */
.settings-menu-nav {
  display: none;
}

@media (max-width: 700px) {
  .settings-menu-nav {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-bottom: 16px;
    padding-bottom: 16px;
    border-bottom: 1px solid var(--border);
  }

  .settings-menu-nav .btn {
    width: 100%;
    text-align: center;
  }

  /* Undo the header-only overrides that leak in here because the drawer
     panel is DOM-nested inside .header-actions (see the long NOTE above
     .header-actions .nav-icon-btn). Equal specificity to the rules being
     undone, so source order — these come later in the file — decides the
     winner. Without this, the drawer copies of "My listings"/"My
     bookings"/"Favorites"/"Messages" rendered as unlabeled 42px icon
     circles with invisible white-on-white text, which is why they were
     impossible to find on mobile. */
  .settings-menu-nav .btn-secondary {
    color: var(--text);
    border-color: var(--border);
  }

  .settings-menu-nav .btn-secondary:hover {
    background: var(--bg);
  }

  .settings-menu-nav .nav-icon-btn {
    width: 100%;
    height: auto;
    padding: 10px 18px;
    position: static;
    justify-content: flex-start;
    gap: 10px;
  }

  .settings-menu-nav .nav-icon-btn .nav-icon-label {
    display: inline;
  }

  /* Icon first (its natural DOM position — it's the first thing inside
     each <a>), label right after it, both hugging the left edge via the
     justify-content: flex-start above. Previously the icon was pushed to
     the far right via order + margin-left: auto instead, which read
     backwards for a menu row. */
  .settings-menu-nav .nav-icon-btn .nav-badge {
    position: static;
    top: auto;
    right: auto;
    margin-left: 6px;
  }
}

/* Sits over the welcome-gate hero video, top-right — same corner as the
   header's switcher would occupy, since the header itself is hidden while
   the gate is showing. */
.lang-switcher-gate {
  position: absolute;
  top: 16px;
  right: 16px;
  z-index: 3;
  background: rgba(255, 255, 255, 0.92);
  /* Fixed dark text, not var(--text) — this switcher floats over the video
     with its own permanently near-white background regardless of site
     theme, same reasoning as the welcome-logo-light fix. Without this it
     inherited .lang-switcher's var(--text), which turns near-white in dark
     mode and made the text invisible against this element's own
     always-white background. */
  color: #232323;
}

.auth-nav a.btn {
  min-width: 150px;
  text-align: center;
}

/* Mobile header: the full nav row (My listings / Messages / + Add tool /
   login / register / greeting / hamburger) is too much for a phone-width
   line and used to wrap into a messy multi-row stack. Below this breakpoint,
   the page-navigation links move out of the header entirely and into the
   hamburger drawer instead (see .settings-menu-nav + the JS clone in app.js
   that populates it) — the header itself keeps only the logo, a compact
   auth control, and the hamburger, so it always fits on one tidy line. */
@media (max-width: 700px) {
  .header-actions > a.btn {
    display: none;
  }

  .auth-nav {
    margin-left: 0;
    gap: 6px;
  }

  .auth-nav a.btn {
    min-width: 0;
    padding: 8px 12px;
    font-size: 0.85rem;
  }

  .logo-tagline {
    display: none;
  }
}

/* Floating "+" button — on mobile the header's "+ Dodaj narzędzie" link is
   hidden (folded into the hamburger drawer, see above), so the browse,
   favorites, and my-listings grids would otherwise have no visible way to
   add a listing without opening the menu first. Desktop keeps using the
   header button, so this stays hidden until the same breakpoint that hides
   it. Present only on those three pages' markup (not site-wide), matching
   where someone is actually likely to want to add a listing while browsing. */
.fab-add-btn {
  display: none;
}

@media (max-width: 700px) {
  .fab-add-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    position: fixed;
    right: 20px;
    bottom: 20px;
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: var(--primary);
    color: #fff;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32);
    z-index: 90;
    text-decoration: none;
  }

  /* favorites.html / my-listings.html start this hidden and only reveal it
     once app.js confirms the visitor is logged in (see loadFavorites /
     loadMyListings) — same [hidden]-beats-author-style fix needed
     everywhere else in this file an element's own `display` rule would
     otherwise beat the browser's UA `[hidden] { display: none }`. */
  .fab-add-btn[hidden] {
    display: none;
  }

  .fab-add-btn:active {
    background: var(--primary-dark);
  }

  .fab-add-btn .icon {
    width: 26px;
    height: 26px;
  }
}

/* Replaces the old header "Hi {name}" + avatar greeting — that link (and
   its avatar-only-on-mobile fallback) lived directly in the header row;
   this lives inside the settings menu instead, injected by renderAuthNav()
   only while logged in, right above the legal links / admin link. */
.settings-menu-profile {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.settings-menu-profile a {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
}

/* The leading icon/avatar isn't the same size for every row (a 22px round
   avatar for "My profile" vs. a plain 14px svg icon for "My listings" and
   "Balance"), so with only a fixed gap after it, each row's text started
   at a different x position — visually messy. .settings-menu-icon (see
   renderAuthNav, app.js) wraps the icon/avatar in a fixed-width, centered
   slot so the text after it lines up in a clean column regardless of
   which icon a given row uses, without resizing/distorting the icon or
   avatar itself the way forcing a width directly onto them would. */
.settings-menu-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  flex-shrink: 0;
}

.settings-menu-profile a .icon {
  width: 20px;
  height: 20px;
}

/* Second link (Saldo) added below the profile link — a little breathing
   room so they don't read as one run-on row. */
.settings-menu-profile a + a {
  margin-top: 10px;
}

.settings-menu-profile a:hover {
  color: var(--link-blue);
}

.btn {
  display: inline-block;
  padding: 10px 18px;
  border-radius: 8px;
  font-weight: 600;
  text-decoration: none;
  border: none;
  cursor: pointer;
  font-size: 0.95rem;
  transition: background 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}

.btn-primary {
  background: var(--primary);
  color: #fff;
}

.btn-primary:hover {
  background: var(--primary-dark);
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.btn-primary:active {
  transform: translateY(0);
  box-shadow: none;
}

.btn-secondary {
  background: transparent;
  color: var(--text);
  border: 1px solid var(--border);
}

.btn-danger {
  background: var(--danger);
  color: #fff;
}

.btn-danger:hover {
  /* Relative darken instead of a second hardcoded red — keeps the hover
     shade in sync with --danger automatically, including the brighter
     value dark mode uses for text-contrast reasons (see the --danger
     comment in :root[data-theme="dark"]). */
  filter: brightness(0.85);
}

.btn-sm {
  padding: 6px 12px;
  font-size: 0.85rem;
  margin-top: 8px;
}

.listing-card-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 8px;
}

.listing-card-actions .btn-sm {
  margin-top: 0;
}

.btn-block {
  width: 100%;
}

/* "or" separator between the email/password form and the Google button on
   login.html / register.html — a line through the middle with the word
   floating on top, same idea as most auth screens use. */
.auth-divider {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 18px 0;
  color: var(--muted);
  font-size: 0.85rem;
}

.auth-divider::before,
.auth-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* Google's own brand guidelines call for a plain white/light button with
   the full-color "G" mark and dark text — not a colored button — so this
   deliberately doesn't reuse .btn-primary/.btn-secondary's look. */
.btn-google {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  background: #fff;
  color: #3c4043;
  border: 1px solid var(--border);
}

.btn-google:hover {
  background: #f7f7f7;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
}

.google-g-icon {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
}

.welcome-gate {
  display: flex;
  min-height: 100vh;
  width: 100vw;
  position: relative;
  left: 50%;
  right: 50%;
  margin-left: -50vw;
  margin-right: -50vw;
}

/* The class rule above sets its own `display`, which otherwise overrides the
   browser's default [hidden] { display: none } at equal specificity — so
   toggling the hidden attribute in JS silently did nothing. This rule wins
   on specificity and actually hides it. */
.welcome-gate[hidden] {
  display: none;
}

.welcome-gate-visual {
  flex: 1;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px;
  /* Solid fallback color, visible for the instant before the video loads
     (or if it fails to) — same dark blue the gradient overlay below fades
     into, so there's no flash of a mismatched color. */
  background: var(--primary-dark);
  overflow: hidden;
  box-shadow: 10px 0 30px rgba(0, 0, 0, 0.3);
  z-index: 1;
}

.welcome-gate-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}

/* Grain + a blue-tinted darkening gradient sit on top of the video (not
   blended onto it directly), keeping the chips/logo/text legible over
   whatever's happening in the footage underneath while still reading as
   "Rentuj blue" rather than a random stock clip. */
.welcome-gate-video-overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.35'/%3E%3C/svg%3E"),
    linear-gradient(135deg, rgba(8, 20, 35, 0.45) 0%, rgba(20, 64, 110, 0.35) 45%, rgba(56, 142, 210, 0.2) 100%);
  background-blend-mode: overlay;
}

/* Full-bleed layer behind the intro slideshow's content (logo, cards,
   controls) that holds the ambient cursor-repel dots. Negative z-index so
   it paints above .intro-slideshow's own gradient background but below
   every in-flow child (those default to stacking above a negatively
   z-indexed sibling regardless of DOM order), and pointer-events: none so
   it never intercepts clicks meant for the buttons/dots/arrows above it. */
.intro-slideshow-dots {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}

/* Ambient field of small dots scattered across the intro slideshow's blue
   background, which drift away from the cursor as it gets close and ease
   back to their home position once it moves off — a bit of life on what
   was otherwise a fairly flat panel of color. initCursorDots() (app.js)
   creates them once at fixed "home" positions and drives the repel/ease-back
   motion itself every frame via a plain lerp (not CSS transitions — a
   second, competing easing layer on top of the JS one would just fight it
   and look laggy), so this only needs to place and size the dot itself.
   Skipped entirely for prefers-reduced-motion rather than relying on the
   global animation-duration override, since continuous per-frame repulsion
   is exactly the kind of motion that setting exists to avoid. */
.cursor-dot {
  position: absolute;
  top: 0;
  left: 0;
  width: 2px;
  height: 2px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.22);
  z-index: 2;
  pointer-events: none;
  will-change: transform;
}

.welcome-gate-slogan-stack {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: 22px;
  width: 440px;
  max-width: 100%;
}

/* Anonymous intro slideshow (index.html) — a step-by-step explainer shown
   before the welcome gate on every logged-out visit. Full-viewport
   overlay, same "breaks out of .container" trick isn't needed since this
   uses position:fixed rather than negative margins. */
.intro-slideshow {
  /* Pins this screen to its own fixed blue palette regardless of the site's
     dark-mode setting — :root[data-theme="dark"] overrides --primary and
     --primary-dark globally (see near the top of this file), which used to
     leak into this gradient and the "Zaczynajmy" button (.btn-primary
     reads the same variables) while the card/arrows/skip button below stay
     on their own hardcoded hex colors — the two would drift out of sync
     the moment dark mode was on. Redeclaring both variables here overrides
     them for this element and everything inside it (custom properties
     inherit down the DOM), without needing to hunt down every var(--...)
     usage in this section individually. */
  --primary: #2563a8;
  --primary-dark: #14406e;
  position: fixed;
  inset: 0;
  z-index: 500;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 32px;
  background: linear-gradient(160deg, var(--primary-dark) 0%, var(--primary) 100%);
  color: #fff;
  padding: 40px 24px;
  text-align: center;
}

/* Same fix as .welcome-gate[hidden] below: this rule's own `display: flex`
   above would otherwise beat the browser's [hidden] { display: none },
   since author styles always win over the UA stylesheet regardless of
   specificity — leaving the intro visible even when JS sets `hidden`. */
.intro-slideshow[hidden] {
  display: none;
}

.intro-logo {
  position: absolute;
  top: 60px;
  left: 50%;
  transform: translateX(-50%);
  height: 84px;
  width: auto;
}

.intro-skip {
  position: absolute;
  top: 20px;
  right: 20px;
  /* Solid opaque approximation of the old rgba(255,255,255,0.12) — see
     .intro-slide comment further down for why this needed to stop being
     transparent. */
  background: #38679a;
  border: 1px solid rgba(255, 255, 255, 0.35);
  color: #fff;
  padding: 8px 16px;
  border-radius: 999px;
  font-size: 0.85rem;
  cursor: pointer;
}

.intro-skip:hover {
  background: #4f78a5;
}

.intro-slides {
  position: relative;
  width: 100%;
  max-width: 460px;
  min-height: 380px;
}

/* Each slide is styled as its own card (border + rounded corners) rather
   than plain text floating on the gradient, and the active/inactive states
   double as a card-shuffle transition: the inactive position (shrunk,
   dropped slightly, tilted) reads as "the next card waiting in the deck",
   so swapping which slide has .is-active looks like a card being dealt to
   the front while the previous one drops back into the stack. Odd/even
   tilt in opposite directions so the "deck" doesn't look perfectly
   uniform. */
.intro-slide {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 36px 30px;
  border: 1.5px solid rgba(255, 255, 255, 0.55);
  border-radius: 10px;
  /* Solid, opaque approximation of the old rgba(255,255,255,0.05)-over-gradient
     look — was translucent, which let the cursor-dot field underneath
     (z-index: -1) show through the card. */
  background: #28598c;
  box-shadow: 0 16px 34px rgba(0, 0, 0, 0.2);
  opacity: 0;
  transform: translateY(16px) scale(0.88) rotate(5deg);
  transition: opacity 0.5s ease, transform 0.55s cubic-bezier(0.34, 1.45, 0.64, 1);
  pointer-events: none;
}

.intro-slide:nth-child(even) {
  transform: translateY(16px) scale(0.88) rotate(-5deg);
}

.intro-slide.is-active {
  opacity: 1;
  transform: translateY(0) scale(1) rotate(0deg);
  pointer-events: auto;
  z-index: 2;
}

.intro-slide-icon {
  width: 68px;
  height: 68px;
  margin-bottom: 22px;
  color: #fff;
}

.intro-slide.is-active .intro-slide-icon {
  animation: introIconPop 0.5s ease;
}

@keyframes introIconPop {
  0% {
    transform: scale(0.5) rotate(-6deg);
    opacity: 0;
  }
  100% {
    transform: scale(1) rotate(0);
    opacity: 1;
  }
}

.intro-slide-icon svg {
  width: 100%;
  height: 100%;
}

.intro-slide h2 {
  font-size: 1.4rem;
  margin: 0 0 12px;
}

.intro-slide p {
  font-size: 1rem;
  line-height: 1.55;
  color: rgba(255, 255, 255, 0.85);
  max-width: 380px;
  margin: 0;
}

.intro-controls {
  display: flex;
  align-items: center;
  gap: 22px;
}

.intro-arrow {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.4);
  /* Solid opaque approximation of the old rgba(255,255,255,0.08) — see
     .intro-slide comment above for why this needed to stop being transparent. */
  background: #2f6094;
  color: #fff;
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

.intro-arrow:hover {
  background: #4a75a2;
}

.intro-arrow:disabled {
  opacity: 0.3;
  cursor: default;
  background: #2f6094;
}

.intro-arrow[hidden] {
  visibility: hidden;
}

.intro-dots {
  display: flex;
  gap: 9px;
}

.intro-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.35);
  border: none;
  padding: 0;
  cursor: pointer;
  transition: background 0.25s ease, transform 0.25s ease;
}

.intro-dot.is-active {
  background: #fff;
  transform: scale(1.35);
}

.intro-get-started {
  animation: introFadeUp 0.4s ease;
}

@keyframes introFadeUp {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (max-width: 560px) {
  .intro-slideshow {
    /* Switched off centering on mobile — with justify-content:center, how
       much air ends up between the (absolutely-positioned, fixed-height)
       logo and the top of the card depended on the phone's screen height,
       and on shorter screens the centered block landed close enough to
       read as cramped. padding-top here is a deterministic clearance
       instead: logo's own top offset (60px) + its height (84px) + a
       comfortable gap, so there's always real space below the logo
       regardless of viewport height. */
    justify-content: flex-start;
    padding-top: 170px;
  }

  .intro-slides {
    min-height: 300px;
  }

  .intro-slide h2 {
    font-size: 1.2rem;
  }

  .intro-slide p {
    font-size: 0.92rem;
  }
}

/* The ambient cursor-repel dot field is a mouse-driven effect — there's no
   cursor on a touch device, so it'd just sit static (and the density that
   looks right on a big screen is packed tighter on a small one). Simplest
   to just not render it at all below the site's usual mobile breakpoint. */
@media (max-width: 700px) {
  .intro-slideshow-dots {
    display: none;
  }
}

.slogan-chip-link {
  display: block;
  text-decoration: none;
  border-radius: 14px;
}

.slogan-chip-link:focus-visible {
  outline: 3px solid #ffffff;
  outline-offset: 3px;
}

.slogan-chip {
  width: 100%;
  height: auto;
  display: block;
  transition: transform 0.2s ease, filter 0.2s ease;
}

.slogan-chip-link:hover .slogan-chip,
.slogan-chip-link:focus-visible .slogan-chip {
  transform: scale(1.04);
  filter: drop-shadow(0 8px 18px rgba(0, 0, 0, 0.35));
}

.welcome-gate-panel {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
}

.welcome-gate-inner {
  max-width: 380px;
  width: 100%;
  margin: 0 auto;
  text-align: center;
}

@media (max-width: 800px) {
  .welcome-gate {
    flex-direction: column;
    min-height: auto;
  }

  .welcome-gate-visual {
    min-height: 200px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
  }

  .welcome-gate-slogan-stack {
    gap: 14px;
  }
}

.welcome-logo {
  height: 64px;
  margin-bottom: 24px;
}

/* logo3.png has a flat white background baked in (no sandbox available to
   actually strip it out). The panel sits on --bg, which is light in light
   mode, so multiply blends the white away into it — white pixels vanish,
   only the logo's own colors show. That trick relies on the background
   actually being light, though: against dark mode's near-black --bg it
   used to multiply the logo's own colors down toward black too, reading as
   "the logo is basically invisible/very dark". Two logo images instead,
   swapped by theme via the rules below, rather than trying to fix one
   image for both — .welcome-logo-dark reuses the header's own transparent
   white wordmark (logotr2.png.001.png), which needs no blend-mode trick at
   all since it's genuinely transparent already. */
.welcome-logo-light {
  width: auto;
  border-radius: 6px;
  mix-blend-mode: multiply;
}

.welcome-logo-dark {
  display: none;
  /* Same object-fit crop as .logo-img in the header — this file has extra
     transparent canvas padding baked in around the wordmark, so a plain
     width:auto scale leaves it looking tiny; cropping to the header's own
     ~4.5:1 ratio at this height keeps it properly sized instead. */
  width: 288px;
  object-fit: cover;
  object-position: center;
}

:root[data-theme="dark"] .welcome-logo-light {
  display: none;
}

:root[data-theme="dark"] .welcome-logo-dark {
  display: inline-block;
}

.welcome-gate h1 {
  font-size: 1.6rem;
  margin: 0 0 10px;
}

.welcome-gate p {
  color: var(--muted);
  margin: 0 0 28px;
}

.welcome-gate-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 18px;
}

.link-btn {
  background: none;
  border: none;
  color: var(--muted);
  text-decoration: underline;
  cursor: pointer;
  font-size: 0.9rem;
  padding: 0;
}

.intro {
  padding: 32px 0 16px;
}

.intro h1 {
  font-size: 2.3rem;
  line-height: 1.2;
  margin-bottom: 8px;
}

.intro p {
  color: var(--muted);
  margin: 0;
}

@media (max-width: 600px) {
  .intro h1 {
    font-size: 1.7rem;
  }
}

.filters {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  padding: 8px 0 24px;
}

.filters input,
.filters select {
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid var(--border);
  font-size: 0.95rem;
}

/* Custom chevron instead of the browser's native one — the native arrow
   sits way out at the box's own default inset, which reads as "sliding
   off to the right" on a wide select (city/category/sort all stretch via
   flex). This one is drawn as two angled gradients rather than a fixed-
   color image, so it's pinned a fixed, tight distance from the right edge
   no matter how wide the box gets, and currentColor keeps it in sync with
   light/dark mode automatically. Applies to every <select class="lang-
   switcher"> site-wide too, for the same reason. */
.filters select,
.lang-switcher {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  padding-right: 28px;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 16px) center, calc(100% - 11px) center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

.filters input {
  flex: 1;
  min-width: 200px;
}

/* Search takeover, desktop only (see the mobile override below — on a
   stacked single-column layout, growing/shrinking sideways doesn't apply).
   Every direct filter control eases its size/fade with the same curve, so
   clicking into the search box reads as one coordinated motion rather than
   several unrelated ones. */
@media (min-width: 701px) {
  .filters > * {
    transition: flex-grow 0.4s cubic-bezier(0.4, 0, 0.2, 1),
      flex-basis 0.4s cubic-bezier(0.4, 0, 0.2, 1),
      min-width 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.35s ease,
      color 0.15s ease, border-color 0.25s ease, box-shadow 0.25s ease;
  }

  .filters:has(#searchFilter:focus) #searchFilter {
    flex-grow: 3;
    outline: none;
    border-color: var(--primary);
    box-shadow: 0 0 0 3px rgba(37, 99, 168, 0.15);
  }

  /* City, category, and sort all shrink toward the edge and fade back —
     still present (not display: none, so the collapse itself animates
     instead of popping), just visibly out of the way while searching.
     color: transparent (city's placeholder needs its own rule, since
     ::placeholder doesn't inherit color from the input) hides the text/
     selected-option label at that width — left showing, it just wraps or
     gets clipped mid-word at 42px, which reads as broken rather than
     "minimized". The box outline stays visible so it's still obviously
     there, just out of the way. */
  .filters:has(#searchFilter:focus) #cityFilter,
  .filters:has(#searchFilter:focus) #categoryFilter,
  .filters:has(#searchFilter:focus) #sortFilter {
    flex-grow: 0;
    flex-basis: 42px;
    min-width: 42px;
    opacity: 0.32;
    color: transparent;
  }

  .filters:has(#searchFilter:focus) #cityFilter::placeholder {
    color: transparent;
  }

  .filters:has(#searchFilter:focus) #resetFilters {
    opacity: 0.55;
  }
}

/* Five controls (search, city, category, sort, reset) is one too many to
   wrap gracefully at phone widths — half-wrapped rows with one lonely
   control on its own line look broken. Below this breakpoint, stack every
   filter to full width in a single tidy column instead of wrapping. */
@media (max-width: 700px) {
  .filters {
    flex-direction: column;
    align-items: stretch;
  }

  .filters input,
  .filters select,
  .filters button {
    width: 100%;
    min-width: 0;
  }
}

.listings-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 18px;
  padding-bottom: 48px;
}

.listing-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  position: relative;
  animation: cardFadeIn 0.35s ease-out;
  transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}

.listing-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 14px 28px rgba(0, 0, 0, 0.1);
  border-color: var(--primary);
}

.listing-card-clickable {
  cursor: pointer;
}

@keyframes cardFadeIn {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.listing-photo {
  width: 100%;
  height: 160px;
  object-fit: cover;
  background: var(--border);
}

/* Fades a photo in once it's actually finished loading instead of popping
   in abruptly — most noticeable on slow mobile connections, where the gray
   placeholder background above holds the layout steady while it waits.
   The .is-loaded class is added by a single document-level "load" listener
   in app.js rather than per-image wiring — see the comment there. */
.fade-img {
  opacity: 0;
  transition: opacity 0.35s ease;
}

.fade-img.is-loaded {
  opacity: 1;
}

.listing-photo-large {
  width: 100%;
  height: auto;
  max-height: 320px;
  object-fit: cover;
  border-radius: 12px;
  margin-bottom: 12px;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.16);
}

.listing-photo-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  font-size: 0.85rem;
}

.listing-body {
  padding: 14px 16px 18px;
  display: flex;
  flex-direction: column;
  flex: 1;
}

.listing-body > .btn {
  margin-top: auto;
  align-self: flex-start;
}

.listing-category {
  display: inline-block;
  font-size: 0.75rem;
  color: var(--link-blue);
  /* Translucent tint instead of a flat light-blue fill — a solid light
     background stayed just as bright in dark mode (nothing here was
     theme-driven), clashing hard against a dark card. rgba lets the badge
     read as a subtle overlay on both a light and a dark card background. */
  background: rgba(37, 99, 168, 0.14);
  padding: 3px 8px;
  border-radius: 6px;
  margin-bottom: 8px;
}

.listing-title {
  margin: 0 0 6px;
  font-size: 1.1rem;
}

.listing-desc {
  color: var(--muted);
  font-size: 0.9rem;
  margin: 0 0 10px;
}

.listing-city,
.listing-price,
.listing-contact,
.listing-owner {
  margin: 4px 0;
  font-size: 0.9rem;
}

.listing-owner {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--text);
  text-decoration: none;
  width: fit-content;
}

a.listing-owner:hover {
  text-decoration: underline;
}

.listing-owner-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--text);
  text-decoration: none;
  font-weight: 600;
}

.listing-owner-link:hover {
  text-decoration: underline;
}

.avatar-thumb {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}

.avatar-thumb-sm {
  width: 22px;
  height: 22px;
}

.avatar-thumb-lg {
  width: 72px;
  height: 72px;
}

.avatar-placeholder {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--muted);
}

.avatar-placeholder .icon-person {
  width: 12px;
  height: 12px;
}

.avatar-thumb-lg.avatar-placeholder .icon-person {
  width: 32px;
  height: 32px;
}

/* .avatar-placeholder sets its own display:inline-flex, which (being an
   author-stylesheet rule) beats the browser's default [hidden]{display:none}
   UA rule regardless of specificity — same class of bug already fixed for
   .fab-add-btn earlier in this file. Toggling the `hidden` attribute via JS
   silently did nothing without this override, so the placeholder icon kept
   showing next to a real uploaded photo. */
.avatar-placeholder[hidden] {
  display: none;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.avatar-edit-wrap {
  position: relative;
  display: inline-block;
  border-radius: 50%;
  cursor: pointer;
}

.avatar-edit-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 12px;
  font-weight: 600;
  opacity: 0;
  transition: opacity 0.15s ease;
  pointer-events: none;
}

.avatar-edit-wrap:hover .avatar-edit-overlay,
.avatar-edit-wrap:focus-within .avatar-edit-overlay {
  opacity: 1;
}

/* Small line-icon set replacing emoji — sized/aligned to sit inline with
   whatever text surrounds them, inheriting that text's color. */
.icon {
  display: inline-block;
  vertical-align: -2px;
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}

/* --- Spinner: used everywhere something is loading (page content, form
   submits). Not a plain full-ring spinner — a conic-gradient arc fades in
   and sweeps around the circle like a brush stroke being painted in a
   circle, chased by a small glowing "brush tip" dot at its leading edge.
   The ring shape itself comes from radial-gradient masking (mask cuts a
   transparent hole from the center out, leaving only a thin annulus of
   the conic-gradient visible) rather than border-based spinners. */
.spinner {
  --spinner-size: 34px;
  display: inline-block;
  width: var(--spinner-size);
  height: var(--spinner-size);
  border-radius: 50%;
  position: relative;
  vertical-align: middle;
  background: conic-gradient(
    from 0deg,
    transparent 0deg,
    transparent 30deg,
    rgba(37, 99, 168, 0.35) 120deg,
    var(--primary) 200deg,
    var(--primary-dark) 265deg,
    transparent 290deg
  );
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 3px));
  mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 3px));
  animation: spinnerSpin 1s linear infinite;
}

.spinner::after {
  content: "";
  position: absolute;
  top: -1px;
  left: 50%;
  width: 6px;
  height: 6px;
  margin-left: -3px;
  border-radius: 50%;
  background: var(--primary-dark);
  box-shadow: 0 0 6px 1.5px rgba(37, 99, 168, 0.65);
}

@keyframes spinnerSpin {
  to {
    transform: rotate(360deg);
  }
}

.spinner-sm {
  --spinner-size: 16px;
}

.spinner-sm::after {
  width: 3.5px;
  height: 3.5px;
  margin-left: -1.75px;
  box-shadow: 0 0 4px 1px rgba(37, 99, 168, 0.6);
}

/* Light variant for spinners sitting on the dark/blue backgrounds of
   .btn-primary buttons, where the default blue-on-transparent gradient
   would have almost no contrast against the button's own blue fill. */
.spinner-light {
  background: conic-gradient(
    from 0deg,
    transparent 0deg,
    transparent 30deg,
    rgba(255, 255, 255, 0.4) 120deg,
    rgba(255, 255, 255, 0.85) 200deg,
    #fff 265deg,
    transparent 290deg
  );
}

.spinner-light::after {
  background: #fff;
  box-shadow: 0 0 5px 1.5px rgba(255, 255, 255, 0.8);
}

/* Sits inline just before a button's own label text. */
.spinner-btn {
  margin-right: 8px;
}

/* Page-level "loading…" rows — spinner + translated text side by side
   instead of a bare line of text. Still used by admin.html (internal-only,
   left as-is); every user-facing page now uses the skeleton system below
   instead, which reads as noticeably faster/more polished at the same
   actual load time since it previews the shape of what's coming. */
.loading-text {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--muted);
}

/* Same fix needed here as .intro-slideshow[hidden], .welcome-gate[hidden],
   .settings-menu-panel[hidden] and .nav-badge[hidden] elsewhere in this
   file: an element's own explicit `display` rule always beats the
   browser's UA `[hidden] { display: none }` rule, regardless of
   specificity — so every page's `xLoading.hidden = true` toggle would
   silently do nothing without this override. */
.loading-text[hidden] {
  display: none;
}

/* === Skeleton loading placeholders ===
   Preview the shape of the content that's about to load (card grids, list
   rows, chat bubbles, form fields, the listing detail layout) instead of a
   generic spinner. Every skeleton element is purely decorative and carries
   aria-hidden="true" in the markup. */

.skeleton {
  position: relative;
  overflow: hidden;
  background: var(--border);
  border-radius: 6px;
}

.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.65), transparent);
  animation: skeletonShimmer 1.4s ease-in-out infinite;
}

@keyframes skeletonShimmer {
  100% {
    transform: translateX(100%);
  }
}

/* Reduced-motion handling for this lives in the single site-wide rule near
   the top of this file instead of a component-specific override — see the
   comment there. */

.skeleton-text {
  height: 14px;
  margin: 6px 0;
}

.skeleton-text-lg {
  height: 20px;
}

.skeleton-text-pill {
  height: 20px;
  width: 70px;
  border-radius: 999px;
  margin-bottom: 8px;
}

.skeleton-w-100 { width: 100%; }
.skeleton-w-90 { width: 90%; }
.skeleton-w-80 { width: 80%; }
.skeleton-w-70 { width: 70%; }
.skeleton-w-60 { width: 60%; }
.skeleton-w-50 { width: 50%; }
.skeleton-w-40 { width: 40%; }
.skeleton-w-30 { width: 30%; }

.skeleton-circle {
  border-radius: 50%;
  flex-shrink: 0;
}

/* Matches .avatar-thumb-lg's own 72px so the hero skeleton (user.html)
   doesn't jump in size once the real avatar swaps in. */
.skeleton-avatar-lg {
  width: 72px;
  height: 72px;
}

/* Card-grid skeleton — mirrors .listing-card's own shell (border, radius,
   background, flex layout) so dimensions match the real cards exactly and
   nothing jumps when the real content swaps in. Deliberately does NOT
   share the .listing-card class itself: renderListings() and friends
   clear old cards via `querySelectorAll(".listing-card").forEach(remove)`
   before appending fresh ones, which would permanently delete these
   skeleton nodes from the DOM on the very first load if they matched that
   selector, leaving every *later* loading state blank. */
.skeleton-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* Standalone skeleton grid (my-listings/favorites/my-bookings/user.html —
   pages where the loading row sits *before* an empty grid div rather than
   inside the real one). Mirrors .listings-grid's own column/gap rules. */
.skeleton-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 18px;
}

.skeleton-grid[hidden] {
  display: none;
}

/* index.html nests its loading row inside the same grid container as the
   real cards, so display:contents un-wraps it and lets the skeleton cards
   become direct grid items instead of one single flex cell. */
.skeleton-inline-grid {
  display: contents;
}

.skeleton-inline-grid[hidden] {
  display: none;
}

/* Row-based lists (messages inbox) — mirrors .thread-item's own shape. */
.skeleton-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px;
  border-radius: 10px;
  border: 1px solid var(--border);
}

.skeleton-row[hidden] {
  display: none;
}

.skeleton-row + .skeleton-row {
  margin-top: 8px;
}

.skeleton-row-body {
  flex: 1;
  min-width: 0;
}

.skeleton-row-meta {
  width: 40px;
}

/* Chat bubbles (thread.html) */
.skeleton-bubbles[hidden] {
  display: none;
}

.skeleton-bubble-row {
  display: flex;
  margin: 10px 0;
}

.skeleton-bubble-row-mine {
  justify-content: flex-end;
}

.skeleton-bubble {
  height: 36px;
  max-width: 260px;
  border-radius: 14px;
}

/* Detail page (listing.html) — mirrors .listing-detail-top's own
   1.15fr/1fr split and 800px collapse-to-single-column breakpoint. */
.skeleton-detail[hidden] {
  display: none;
}

.skeleton-detail {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  gap: 24px;
}

@media (max-width: 800px) {
  .skeleton-detail {
    grid-template-columns: 1fr;
  }
}

.skeleton-detail-photo {
  height: 320px;
  border-radius: 12px;
}

.skeleton-btn {
  height: 44px;
  border-radius: 8px;
  margin-top: 16px;
}

.skeleton-textarea {
  height: 80px;
  border-radius: 8px;
}

/* Hero (user.html) — avatar circle + a couple of text lines. */
.skeleton-hero {
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 24px 0 8px;
}

.skeleton-hero[hidden] {
  display: none;
}

.skeleton-hero-info {
  flex: 1;
  min-width: 0;
  max-width: 260px;
}

/* Simple stacked form skeleton (profile.html, manage.html). */
.skeleton-form[hidden] {
  display: none;
}

.skeleton-form-row {
  margin: 18px 0;
}

.icon-mail {
  width: 16px;
  height: 16px;
  vertical-align: -3px;
  margin-right: 2px;
}

.icon-check {
  width: 20px;
  height: 20px;
  vertical-align: -4px;
}

.current-avatar-row {
  margin: 12px 0 4px;
}

/* Payouts card on profile.html, and the balance card on balance.html — a
   bordered box separates each visually from surrounding content, since both
   are meaningfully different (and more consequential) actions than plain
   form fields around them. */
.payouts-section,
.balance-card {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 18px 20px;
  margin: 18px 0 26px;
  background: var(--card-bg);
}

.payouts-section h2,
.balance-card h2 {
  margin: 0 0 6px;
  font-size: 1.05rem;
}

.payouts-section p,
.balance-card p {
  margin: 0 0 12px;
  font-size: 0.9rem;
  color: var(--muted);
}

.payouts-status {
  margin-bottom: 12px !important;
}

.payouts-status:empty {
  display: none;
}

.balance-amount {
  font-size: 2.2rem;
  font-weight: 800;
  color: var(--link-blue);
  margin: 4px 0 2px;
}

.balance-pending {
  font-size: 0.85rem;
  color: var(--muted);
  margin: 0 0 16px;
}

.balance-pending:empty {
  display: none;
}

.listing-price {
  font-weight: 700;
  color: var(--link-blue);
}

/* Deposit line on a "My Bookings" card — badge + amount on one row, with
   the auto-release date or claim reason wrapped onto its own line below
   rather than fighting for space on the same line. */
.booking-deposit-info {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 4px 0;
  font-size: 0.9rem;
}

.booking-deposit-info .cal-hint {
  margin: 0;
  width: 100%;
}

.empty-state,
#loadingState {
  grid-column: 1 / -1;
  text-align: center;
  color: var(--muted);
  padding: 40px 0;
}

.add-form {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-bottom: 60px;
}

.add-form label {
  font-weight: 600;
  margin-top: 10px;
  font-size: 0.9rem;
}

.add-form input,
.add-form select,
.add-form textarea {
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid var(--border);
  font-size: 0.95rem;
  font-family: inherit;
}

.add-form button {
  margin-top: 18px;
}

.phone-input-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.phone-flag-preview {
  flex-shrink: 0;
  width: 1.6em;
  height: 1.2em;
  border-radius: 3px;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08);
}

.phone-dial-select {
  flex: 0 0 auto;
  max-width: 150px;
  padding: 10px 8px;
  border-radius: 8px;
  border: 1px solid var(--border);
  font-size: 0.95rem;
  font-family: inherit;
}

.phone-input-row input {
  flex: 1;
  min-width: 0;
}

.form-intro {
  color: var(--muted);
}

/* Minimal pill-outline link style — thin border, rounded corners, no
   underline. Used for secondary form-adjacent links like "forgot
   password" and "create an account" that shouldn't compete visually with
   the primary form button above them. Fixed width (rather than min-width)
   so the two buttons always match exactly regardless of how long their
   translated text is in the active language — the longer of the two
   ("forgot password") sets the width other languages need to fit inside,
   so 260px leaves comfortable room across all 6. */
.outline-pill-btn {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 260px;
  padding: 10px 18px;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--text);
  text-decoration: none;
  font-size: 0.85rem;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.outline-pill-btn:hover {
  background: var(--bg);
  border-color: var(--primary);
  color: var(--link-blue);
}

.outline-pill-btn .icon {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

.form-status {
  margin-top: 12px;
  font-size: 0.9rem;
  min-height: 1.2em;
}

.form-status-success {
  color: var(--link-blue);
}

.form-status-error {
  color: var(--danger);
}

/* Toast/snackbar notifications — showToast() in app.js. Used for transient
   confirmations (profile/listing saved, review posted) that don't need to
   stay on screen, and for anything that used to be a blocking native
   alert() (Stripe/booking/admin action failures), which read as broken on
   an otherwise polished site. Errors that need to stay visible while the
   person fixes something (form validation, login failure) still use the
   inline .form-status-error treatment above instead — a toast that
   vanishes mid-read would be worse there. */
.toast-container {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column-reverse;
  align-items: center;
  gap: 10px;
  z-index: 500;
  width: min(380px, calc(100vw - 32px));
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 13px 16px;
  border-radius: 10px;
  background: var(--text);
  color: #fff;
  font-size: 0.9rem;
  line-height: 1.35;
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.22);
  pointer-events: auto;
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.toast-visible {
  opacity: 1;
  transform: translateY(0);
}

.toast-success {
  background: var(--primary-dark);
}

.toast-error {
  background: var(--danger);
}

/* icon() (app.js) always outputs class="icon icon-<name>", not a
   dedicated .toast-icon class, so size it via descendant selector. */
.toast .icon {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
}

@media (max-width: 480px) {
  .toast-container {
    bottom: 16px;
  }
}

/* Cookie/GDPR notice — a small bottom-left card rather than a full-width
   bar, since it only needs to disclose the one strictly-necessary session
   cookie plus localStorage (see privacy.html's "Cookies and local
   storage" section) rather than ask for granular consent to anything
   optional. Bottom-left keeps it clear of the mobile FAB (bottom-right)
   and out of the way of the centered toast stack. Injected once by
   initCookieBanner() (app.js) and dismissed permanently via localStorage,
   same pattern as the theme toggle. */
.cookie-banner {
  position: fixed;
  left: 16px;
  bottom: 16px;
  right: 16px;
  z-index: 450;
  max-width: 380px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18);
  padding: 16px 18px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  animation: cookieBannerIn 0.3s ease-out;
}

.cookie-banner-text {
  margin: 0;
  font-size: 0.85rem;
  color: var(--text);
  line-height: 1.45;
}

.cookie-banner-text a {
  color: var(--link-blue);
  margin-left: 4px;
}

.cookie-banner-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

.cookie-banner.is-dismissing {
  animation: cookieBannerOut 0.22s ease-in forwards;
}

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

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

.booking-banner {
  padding: 14px 18px;
  border-radius: 10px;
  margin-bottom: 20px;
  font-weight: 600;
  background: var(--card-bg);
  border: 1px solid var(--border);
  color: var(--muted);
}

.booking-banner-success {
  background: #e7f0f9;
  border-color: var(--primary);
  color: var(--link-blue);
}

.cal-selection-pay-btn {
  margin-top: 14px;
  width: 100%;
}

.listing-deposit {
  color: var(--muted);
  font-size: 0.9rem;
}

.cal-deposit-hint {
  margin-top: -4px;
  margin-bottom: 8px;
}

.bookings-list {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-bottom: 24px;
}

.booking-item {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 14px 16px;
  background: var(--card-bg);
}

.booking-item-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

.booking-item-total {
  color: var(--muted);
  font-size: 0.9rem;
  margin-top: 4px;
}

.booking-deposit {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px dashed var(--border);
}

.booking-deposit-released {
  color: var(--link-blue);
}

.booking-deposit-claimed {
  color: var(--danger);
}

.booking-deposit-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 8px;
}

.claim-form {
  margin-top: 12px;
  padding: 12px;
  border-radius: 8px;
  background: var(--bg);
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.claim-form input,
.claim-form textarea {
  width: 100%;
}

.claim-form button {
  margin-top: 4px;
}

.success-panel {
  background: #eaf3fb;
  border: 2px solid var(--primary);
  border-radius: 12px;
  padding: 20px;
  margin-bottom: 60px;
  animation: successPop 0.4s ease-out;
}

.success-panel p {
  margin: 0 0 12px;
}

.success-heading {
  font-size: 1.3rem;
  font-weight: 700;
  color: var(--link-blue);
}

@keyframes successPop {
  from {
    opacity: 0;
    transform: translateY(-8px) scale(0.98);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

.btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.manage-link-box {
  display: flex;
  gap: 8px;
  margin: 12px 0;
}

.manage-link-box input {
  flex: 1;
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid var(--border);
  font-size: 0.85rem;
  background: var(--bg);
  color: var(--text);
}

.current-photos-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 8px 0 16px;
}

.current-photo-item {
  position: relative;
  width: 90px;
  height: 90px;
}

.current-photo-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 8px;
}

.remove-photo-btn {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: none;
  background: var(--danger);
  color: #fff;
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
}

.listing-photo-wrap {
  position: relative;
}

.photo-count-badge {
  position: absolute;
  bottom: 8px;
  right: 8px;
  background: rgba(0, 0, 0, 0.65);
  color: #fff;
  font-size: 0.75rem;
  padding: 2px 7px;
  border-radius: 999px;
}

/* Favorite (heart) button — two placements share the same base look:
   .favorite-btn-card floats over the top-right corner of a card's photo
   (positioned relative to .listing-card, not the thumbnail, so it doesn't
   care whether that card even has a photo), .favorite-btn-inline sits
   in-line next to the title on the listing detail page. */
.favorite-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  flex-shrink: 0;
  padding: 0;
  border: none;
  border-radius: 50%;
  /* Brighter/near-white rather than var(--muted) on purpose — the
     unfavorited heart only needs to read as "an icon is here", and
     --muted's full gray looked too heavy/dark sitting on a light photo
     background, while an earlier, more muted gray (#b3b0a8) then looked
     washed-out/hard to spot instead. Near-white pops against almost any
     photo without needing a solid backing circle. Dark mode's own --muted
     is already light enough on a dark card, so it's left as-is below. */
  background: rgba(255, 255, 255, 0.3);
  color: #f5f4f0;
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
  transition: transform 0.15s ease, background 0.15s ease, color 0.15s ease;
}

:root[data-theme="dark"] .favorite-btn {
  /* var(--muted) read as too dim/tan against a photo in dark mode — this
     sits much closer to white so the outline is actually easy to spot. */
  color: rgba(255, 255, 255, 0.85);
}

.favorite-btn:hover {
  transform: scale(1.08);
  background: rgba(255, 255, 255, 0.55);
}

.favorite-btn .icon-heart {
  width: 18px;
  height: 18px;
  /* The circle behind it is quite transparent now, so the outline stroke
     needs to be heavier and fully opaque (rather than the thin 2px default)
     to still read clearly over varied photo backgrounds. */
  opacity: 1;
  stroke-width: 2.6;
}

/* A warm pink-red, distinct from --danger's more orange-leaning red —
   reads unambiguously as "saved" rather than "warning". */
.favorite-btn.is-favorited {
  color: #e0245e;
}

.favorite-btn.is-favorited .icon-heart {
  fill: currentColor;
  animation: favoriteHeartPop 0.4s ease;
}

@keyframes favoriteHeartPop {
  0% {
    transform: scale(1);
  }
  35% {
    transform: scale(1.35);
  }
  60% {
    transform: scale(0.92);
  }
  100% {
    transform: scale(1);
  }
}

.favorite-btn-card {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 2;
}

.favorite-btn-inline {
  margin-left: auto;
}

.ld-title-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}

.ld-title-row h1 {
  margin: 0;
}

.listing-gallery {
  margin-bottom: 8px;
}

/* Detail page container: give the gallery some breathing room below the
   header, and pull the category badge / title right up against it instead
   of leaving the default browser h1 margin create a big gap. */
#listingDetail {
  padding-top: 44px;
}

#listingDetail #ldCategory {
  margin-bottom: 6px;
}

#listingDetail #ldTitle {
  margin: 0 0 8px;
  font-size: 1.5rem;
}

/* Two-column layout on wide screens (gallery + availability/reviews on the
   left, price/contact/buy-box on the right) — like a typical marketplace
   product page, instead of one long narrow stack that leaves most of a
   desktop screen empty. Grid (not flex) so "below" can be pinned to sit
   directly under the gallery, exactly as wide as the image, rather than
   spanning some unrelated width of its own.
   Collapses to a single column on phones/small tablets via the media query
   below, with the DOM order (media, info, below) giving a sensible mobile
   reading order: photo, then price/contact, then availability/reviews. */
.listing-detail-top {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  /* "info" spans both rows so its own height (which grows once the price
     summary card appears) never inflates row 1 — that inflation was what
     pushed "below" (the calendar) down away from the photo. */
  grid-template-areas:
    "media info"
    "below info";
  column-gap: 40px;
  align-items: start;
}

.listing-detail-media {
  grid-area: media;
  min-width: 0;
}

.listing-detail-info {
  grid-area: info;
  min-width: 0;
}

/* The buy-box column reuses classes shared with the compact card grid
   (.listing-price, .listing-contact, etc.), whose 4px margins are fine on a
   small card but read as cramped in this larger, standalone layout. Add
   proper breathing room between each element here, without touching the
   card styling elsewhere. (Title is excluded on purpose — its snug gap to
   the category badge above is intentional and already has its own rule.) */
.listing-detail-info > * + * {
  margin-top: 16px;
}

/* .cal-hint's own margin-top: 0 (needed elsewhere, e.g. right under a
   heading) has equal specificity and would otherwise cancel the spacing
   rule above for the safety-tip line right after the message button —
   this targeted override wins on specificity regardless of source order. */
.listing-detail-info .cal-hint {
  margin-top: 16px;
}

.listing-detail-below {
  grid-area: below;
  margin-top: 8px;
}

.listing-detail-below > .cal-title:first-child {
  margin-top: 0;
}

@media (max-width: 800px) {
  .listing-detail-top {
    grid-template-columns: 1fr;
    grid-template-areas:
      "media"
      "info"
      "below";
  }
}

.gallery-main {
  position: relative;
}

.gallery-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(0, 0, 0, 0.45);
  color: #fff;
  border: none;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  font-size: 1.2rem;
  cursor: pointer;
}

.gallery-prev {
  left: 10px;
}

.gallery-next {
  right: 10px;
}

.gallery-counter {
  position: absolute;
  bottom: 10px;
  right: 10px;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: 0.75rem;
  padding: 3px 8px;
  border-radius: 999px;
}

.gallery-zoomable {
  cursor: zoom-in;
}

/* Small magnifying-glass hint, centered on the photo, that fades in on
   hover so it's clear the image is clickable without cluttering the
   gallery the rest of the time. */
.gallery-zoom-hint {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0.85);
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.5);
  color: #fff;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.gallery-zoom-hint .icon {
  width: 20px;
  height: 20px;
}

.gallery-main:hover .gallery-zoom-hint {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}

/* --- Lightbox: full-screen zoom overlay, opened from the gallery above.
   .is-open (not [hidden] — see the comment in app.js) toggles it on;
   entrance is a fade of the backdrop plus a slightly separate scale/fade of
   the image itself, so it feels less like a hard cut and more like the
   photo is being lifted off the page. */
.lightbox {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 500;
  align-items: center;
  justify-content: center;
  background: rgba(10, 14, 20, 0.92);
  animation: lightboxFadeIn 0.25s ease;
}

.lightbox.is-open {
  display: flex;
}

@keyframes lightboxFadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

.lightbox-img {
  max-width: min(92vw, 1100px);
  max-height: 86vh;
  width: auto;
  height: auto;
  border-radius: 10px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
  animation: lightboxImgIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
}

@keyframes lightboxImgIn {
  from {
    opacity: 0;
    transform: scale(0.94);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

.lightbox-close {
  position: absolute;
  top: 18px;
  right: 22px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
  font-size: 1.6rem;
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s ease;
}

.lightbox-close:hover {
  background: rgba(255, 255, 255, 0.22);
}

.lightbox-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
  border: none;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  font-size: 1.8rem;
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s ease;
}

.lightbox-nav:hover {
  background: rgba(255, 255, 255, 0.22);
}

.lightbox-prev {
  left: 18px;
}

.lightbox-next {
  right: 18px;
}

.lightbox-counter {
  position: absolute;
  bottom: 22px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
  font-size: 0.8rem;
  padding: 4px 12px;
  border-radius: 999px;
}

/* Small, deliberately unobtrusive "report this" trigger — sits near the
   safety hint on listing.html / next to the rating on user.html, not styled
   like a primary action since reporting is a rare, secondary path, not
   something to visually compete with the message/book buttons. */
.report-trigger-link {
  display: inline-block;
  margin-top: 10px;
  background: none;
  border: none;
  padding: 0;
  font-size: 0.82rem;
  color: var(--muted);
  text-decoration: underline;
  cursor: pointer;
}

.report-trigger-link:hover {
  color: var(--danger);
}

/* Shared report modal (app.js: ensureReportModal/openReportModal) — a
   single lazily-built overlay reused by both listing.html and user.html
   rather than duplicating this markup per page. Higher z-index than the
   lightbox (500) and the intro slideshow (500) since either could
   theoretically still be part of the page underneath it. */
.report-modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(10, 14, 20, 0.6);
  padding: 20px;
}

.report-modal-overlay[hidden] {
  display: none;
}

.report-modal {
  background: var(--card-bg);
  border-radius: 14px;
  padding: 26px 28px;
  width: 100%;
  max-width: 420px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
}

.report-modal h2 {
  margin: 0 0 16px;
  font-size: 1.2rem;
}

.report-modal label {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
  margin: 14px 0 6px;
}

.report-modal select,
.report-modal textarea {
  width: 100%;
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid var(--border);
  font-size: 0.95rem;
  font-family: inherit;
  resize: vertical;
}

.report-modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 20px;
}

/* Body-level scroll lock while the lightbox is open. */
body.lightbox-open {
  overflow: hidden;
}

@media (max-width: 560px) {
  .lightbox-nav {
    width: 40px;
    height: 40px;
    font-size: 1.5rem;
  }

  .lightbox-prev {
    left: 8px;
  }

  .lightbox-next {
    right: 8px;
  }

  .lightbox-close {
    top: 10px;
    right: 10px;
  }
}

.cal-title {
  margin-top: 28px;
  font-size: 1.1rem;
}

/* The "Opinie" (reviews) heading specifically, on both the listing detail
   page and a user's profile — sized up on its own via the data-i18n
   selector rather than bumping .cal-title/.profile-section h2 generally,
   since those classes are shared with other headings ("Dostępność",
   "Ogłoszenia") that weren't part of the ask. */
[data-i18n="ld.reviewsTitle"],
[data-i18n="user.reviewsTitle"] {
  font-size: 1.4rem;
}

.rating-badge {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 0.85rem;
  font-weight: 600;
  color: #b8860b;
}

/* #b8860b ("darkgoldenrod") is fixed rather than theme-driven, so on a
   dark card it read as a muddy brown instead of a gold star — brightened
   here for dark mode only. */
:root[data-theme="dark"] .rating-badge,
:root[data-theme="dark"] .review-stars {
  color: #f6c343;
}

.rating-badge-empty {
  color: var(--muted);
  font-weight: 400;
}

.rating-count {
  color: var(--muted);
  font-weight: 400;
}

.reviews-summary {
  font-size: 0.95rem;
  margin: 0 0 16px;
}

.reviews-list {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-top: 16px;
}

/* On the listing detail page specifically, this list sits right under the
   "submit a review" form — the shared 16px margin-top above, stacked on
   top of the form's own status-line spacing, left a bigger gap than
   intended between the submit button and the reviews themselves. Other
   pages that reuse .reviews-list standalone (like a user's profile) keep
   the normal spacing. */
#reviewsList {
  margin-top: 0;
}

.review-item {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 12px 14px;
}

.review-item-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}

.review-stars {
  color: #b8860b;
  margin-left: auto;
  font-size: 0.95rem;
}

.review-comment {
  margin: 0;
  font-size: 0.9rem;
  color: var(--text);
}

.review-listing-ref {
  margin: 6px 0 0;
  font-size: 0.8rem;
  color: var(--muted);
}

/* --- Public user profile (user.html) --- */
.profile-hero {
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 48px 0 8px;
}

.profile-hero-info h1 {
  margin: 0 0 4px;
  font-size: 1.9rem;
}

/* Scoped to #upAvatar (a user's profile page) rather than bumping
   .avatar-thumb-lg generally, which is also used for the smaller thread
   participant avatar on messages/thread.html. */
#upAvatar .avatar-thumb-lg {
  width: 104px;
  height: 104px;
}

#upAvatar .avatar-thumb-lg.avatar-placeholder .icon-person {
  width: 46px;
  height: 46px;
}

#upLoading .skeleton-avatar-lg {
  width: 104px;
  height: 104px;
}

.profile-hero-joined {
  margin: 0 0 6px;
  font-size: 0.85rem;
  color: var(--muted);
}

.profile-section {
  margin-top: 32px;
}

.profile-section h2 {
  font-size: 1.1rem;
  margin-bottom: 12px;
}

#reviewFormWrap {
  max-width: 420px;
  margin-bottom: 6px;
}

#reviewForm.add-form {
  padding-bottom: 0;
}

/* #reviewStatus is a <p>, which browsers give a default bottom margin —
   that was never reset, so it was quietly stacking with #reviewFormWrap's
   own margin-bottom and #reviewsList's margin-top below, adding up to a
   much bigger gap above the first review than any single rule here
   suggested. */
#reviewForm .form-status {
  margin-bottom: 0;
}

/* --- Messaging (messages.html, thread.html) --- */
.threads-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.thread-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px;
  border-radius: 10px;
  text-decoration: none;
  color: var(--text);
  border: 1px solid var(--border);
}

.thread-item + .thread-item {
  margin-top: 8px;
}

.thread-item:hover {
  background: var(--bg);
}

.thread-item-unread {
  border-color: var(--primary-dark);
  background: #eef5fb;
}

.thread-item-body {
  flex: 1;
  min-width: 0;
}

.thread-item-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
}

.thread-item-listing {
  font-size: 0.8rem;
  color: var(--muted);
}

.thread-item-preview {
  margin: 2px 0 0;
  font-size: 0.9rem;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.thread-item-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  flex-shrink: 0;
}

.thread-item-time {
  font-size: 0.75rem;
  color: var(--muted);
}

.thread-unread-badge {
  background: var(--primary-dark);
  color: #fff;
  font-size: 0.75rem;
  font-weight: 600;
  border-radius: 999px;
  padding: 1px 8px;
}

.thread-context {
  font-size: 0.9rem;
  color: var(--muted);
  margin-bottom: 4px;
}

.thread-participant {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 4px;
  margin: 16px 0 20px;
}

.thread-participant-avatar .avatar-thumb {
  margin: 0 auto 4px;
}

.thread-participant h1 {
  margin: 0;
  font-size: 1.3rem;
}

.thread-participant-email {
  margin: 0;
  color: var(--muted);
  font-size: 0.9rem;
}

.thread-messages {
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-height: 50vh;
  overflow-y: auto;
  padding: 12px 0;
  margin-bottom: 12px;
}

.thread-bubble {
  max-width: 75%;
  padding: 10px 14px;
  border-radius: 14px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  align-self: flex-start;
}

.thread-bubble-mine {
  align-self: flex-end;
  background: var(--primary-dark);
  color: #fff;
}

.thread-bubble-body {
  margin: 0;
  font-size: 0.95rem;
  white-space: pre-wrap;
  word-break: break-word;
}

.thread-bubble-meta {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-top: 4px;
}

.thread-bubble-time {
  font-size: 0.7rem;
  opacity: 0.7;
}

/* Read receipt — only rendered on bubbles the current user sent. Grey
   (translucent white, since it sits on the dark .thread-bubble-mine
   background) until the other person opens the thread, then flips blue.
   Reuses the existing circle-check icon rather than adding a new one. */
.thread-bubble-check {
  display: inline-flex;
  line-height: 0;
}

.thread-bubble-check .icon {
  width: 13px;
  height: 13px;
  color: rgba(255, 255, 255, 0.45);
}

.thread-bubble-check.is-read .icon {
  color: #5ec8f8;
}

.thread-reply-form {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.thread-reply-form textarea {
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid var(--border);
  font-size: 0.95rem;
  font-family: inherit;
  resize: vertical;
}

.cal-hint {
  color: var(--muted);
  font-size: 0.85rem;
  margin: 0 0 12px;
}

.calendar {
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 14px;
  background: var(--card-bg);
  margin-bottom: 40px;
}

.calendar-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-weight: 600;
  margin-bottom: 10px;
}

.cal-nav {
  background: none;
  border: none;
  font-size: 1.1rem;
  cursor: pointer;
  padding: 4px 10px;
  border-radius: 6px;
  color: var(--text);
}

.cal-nav:hover {
  background: var(--bg);
}

.calendar-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 4px;
  text-align: center;
}

.cal-daylabel {
  font-size: 0.75rem;
  color: var(--muted);
  padding: 4px 0;
}

.cal-day {
  padding: 8px 0;
  border-radius: 4px;
  font-size: 0.85rem;
}

.cal-day-empty {
  background: transparent;
}

/* Translucent tints instead of flat light fills — same reasoning as
   .listing-category above: a solid light background stays just as bright
   in dark mode since it isn't theme-driven, so it glared against a dark
   calendar card. rgba reads as a subtle overlay in both themes. */
.cal-day-free {
  background: rgba(37, 99, 168, 0.14);
  color: var(--link-blue);
}

.cal-day-blocked {
  background: rgba(179, 38, 30, 0.14);
  color: var(--danger);
  font-weight: 600;
}

.cal-day-past {
  opacity: 0.4;
}

.cal-day-editable {
  cursor: pointer;
}

.cal-day-editable:hover {
  outline: 2px solid var(--primary);
}

/* Renter-facing "drag to pick your rental dates" selection, distinct from
   the owner's editable/blocking mode above. */
.cal-day-selectable {
  cursor: pointer;
  user-select: none;
}

.cal-day-selectable:hover {
  outline: 2px solid var(--primary);
}

.cal-day-selected {
  background: var(--primary-dark);
  color: #fff;
  font-weight: 600;
}

.cal-selection-summary {
  margin-top: 16px;
  padding: 18px 20px;
  background: var(--card-bg);
  border: 2px solid var(--primary-dark);
  border-radius: 14px;
  box-shadow: 0 4px 16px rgba(20, 64, 110, 0.12);
}

.cal-selection-label {
  margin: 0 0 10px;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--link-blue);
}

.cal-selection-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 0.95rem;
  padding: 4px 0;
}

.cal-selection-row span {
  color: var(--muted);
}

.cal-selection-total {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-top: 10px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
  font-size: 1.05rem;
  font-weight: 600;
}

.cal-selection-total-amount {
  font-size: 1.7rem;
  font-weight: 800;
  color: var(--link-blue);
}

.cal-selection-summary #calClearSelectionBtn {
  margin-top: 12px;
}

.cal-selection-summary-pending {
  border-style: dashed;
  box-shadow: none;
}

.cal-selection-pending-text {
  margin: 0 0 6px;
  font-size: 0.95rem;
}

.cal-selection-summary strong {
  color: var(--link-blue);
}

.danger-zone {
  margin-top: 32px;
  padding-top: 20px;
  border-top: 1px solid var(--border);
  padding-bottom: 60px;
}

.danger-zone h2 {
  font-size: 1.05rem;
  color: var(--danger);
  margin-bottom: 6px;
}

.site-footer {
  border-top: 1px solid var(--border);
  margin-top: 40px;
  padding: 48px 0 0;
  color: var(--muted);
  font-size: 0.9rem;
}

.footer-grid {
  display: grid;
  grid-template-columns: 1.6fr 1fr 1fr;
  gap: 32px;
  padding-bottom: 36px;
}

/* Two logo files swapped by theme, same approach as .welcome-logo-light/
   -dark above: logo3.png is a navy wordmark on a flat white background (no
   sandbox available to strip it), so mix-blend-mode:multiply against the
   footer's own light --bg fakes transparency. logotr2.png.001.png is
   genuinely transparent (white lettering) and only reads correctly on a
   dark background, so it's used for dark mode instead. */
/* Both variants share the exact same box (height + width) so the logo
   doesn't visibly change size when switching light/dark mode — only the
   object-fit differs, since the two source files have different amounts of
   baked-in empty canvas around the wordmark. */
.footer-logo-light,
.footer-logo-dark {
  /* .footer-col is a column flexbox with the default align-items:stretch,
     which was forcing this <img> to the full column width while height
     stayed pinned — that's what actually caused the original "stretched"
     logo, not the image file itself. align-self:flex-start opts the image
     out of the stretch so it sizes to this fixed box instead. */
  align-self: flex-start;
  height: 28px;
  width: 126px;
  margin-bottom: 12px;
  object-position: center;
}

.footer-logo-light {
  /* cover (not contain) so the wordmark actually fills the full box, same
     as .footer-logo-dark — contain was letterboxing logo3.png since its
     native aspect ratio isn't exactly the box's 4.5:1, which made it
     render visibly smaller than the dark-mode logo at the same box size. */
  object-fit: cover;
  mix-blend-mode: multiply;
  /* logo3.png's wordmark is navy, not black — no sandbox available to
     re-export the file itself, so this recolors it instead: grayscale
     drops the navy to a dark gray, then a strong contrast push clamps
     that dark gray to pure black while clamping the already-white
     background back up to pure white (so the multiply trick above still
     hides it against the footer's own background). */
  filter: grayscale(1) contrast(300%);
}

.footer-logo-dark {
  display: none;
  /* logotr2.png.001.png has a lot of empty transparent canvas baked in
     around the wordmark (see .logo-img above) — cropping to this same box
     via object-fit:cover avoids the "stretched/tiny" look a plain scale
     produces. */
  object-fit: cover;
}

:root[data-theme="dark"] .footer-logo-light {
  display: none;
}

:root[data-theme="dark"] .footer-logo-dark {
  display: inline-block;
}

.footer-col-brand p {
  margin: 0 0 16px;
  max-width: 320px;
  line-height: 1.5;
}

.footer-social {
  display: flex;
  gap: 10px;
}

.footer-social-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  color: var(--muted);
  transition: color 0.15s ease;
}

.footer-social-btn:hover {
  color: var(--link-blue);
}

.footer-social-btn .icon {
  width: 17px;
  height: 17px;
}

.footer-col h3 {
  margin: 0 0 14px;
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--text);
}

.footer-col {
  display: flex;
  flex-direction: column;
}

.footer-col a {
  color: var(--muted);
  text-decoration: none;
  margin-bottom: 10px;
}

.footer-col a:hover {
  color: var(--link-blue);
}

.footer-bottom {
  border-top: 1px solid var(--border);
  padding: 18px 0;
  font-size: 0.8rem;
}

.footer-bottom p {
  margin: 0 0 4px;
}

@media (max-width: 700px) {
  .footer-grid {
    grid-template-columns: 1fr 1fr;
    row-gap: 28px;
  }

  .footer-col-brand {
    grid-column: 1 / -1;
  }
}

/* Terms of Service / Privacy Policy body content — headings + paragraphs
   rendered from LEGAL_CONTENT (i18n.js) via legalBody() in app.js. */
.legal-content {
  margin-bottom: 60px;
}

.legal-content .legal-updated {
  color: var(--muted);
  font-size: 0.85rem;
  margin-bottom: 24px;
}

.legal-content h2 {
  font-size: 1.05rem;
  margin: 28px 0 8px;
}

.legal-content p {
  color: var(--text);
  line-height: 1.6;
  margin: 0 0 4px;
}
