/* ==========================================================================
   Travl — public site
   Depends on: bootstrap 5.3 (CDN) + travl-tokens.css
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Base
   -------------------------------------------------------------------------- */

html { scroll-behavior: smooth; }

body {
  font-family: var(--font-body);
  background-color: var(--paper);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;

  /* The parked mobile drawer sits ~8px past the right edge and would otherwise
     give every phone a horizontal scrollbar. `clip` suppresses that without
     making body a scroll container — `hidden` does, and that silently kills
     position: sticky on the six pages whose sidebars depend on it. The hidden
     line is the fallback for browsers without `clip`. */
  overflow-x: hidden;
  overflow-x: clip;
}

/* Fraunces carries a real optical-size axis. Letting it run means a 96px hero
   and a 20px card title read as different cuts of one voice, not one scaled. */
h1, h2, h3, h4, h5, h6,
.h1, .h2, .h3, .h4, .h5, .h6 {
  font-family: var(--font-display);
  font-variation-settings: 'SOFT' 20, 'WONK' 0;
  letter-spacing: -.015em;
  line-height: 1.12;
  font-weight: 600;
}

/* WONK — the swashed g/y/j — only earns its place at display size. */
.display-1, .display-2, .display-3,
.tv-hero__title {
  font-family: var(--font-display);
  font-variation-settings: 'SOFT' 20, 'WONK' 1;
  letter-spacing: -.025em;
  line-height: 1.02;
  font-weight: 600;
}

a { text-decoration: none; transition: color .2s var(--ease); }
img { max-width: 100%; height: auto; }

:focus-visible {
  outline: 2px solid var(--primary-500);
  outline-offset: 3px;
  border-radius: 3px;
}

::selection { background: var(--primary-500); color: #fff; }

/* Prices and any figure the eye scans down a column must not jitter. */
/* `.tabular` itself lives in travl-tokens.css — both shells need it. */
.tv-price, td.text-end { font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------------------
   2. Layout rhythm
   -------------------------------------------------------------------------- */

.tv-section { padding-block: var(--section-y); }

.tv-section--paper   { background-color: var(--paper); }
.tv-section--surface { background-color: var(--surface); }
.tv-section--tint    { background-color: var(--surface-2); }

.tv-section--slab {
  background-color: var(--primary-900);
  color: rgba(255, 255, 255, .82);
  position: relative;
  overflow: hidden;
}
.tv-section--slab h1, .tv-section--slab h2, .tv-section--slab h3,
.tv-section--slab h4, .tv-section--slab h5, .tv-section--slab h6 { color: #fff; }
.tv-section--slab .tv-eyebrow { color: var(--gold-400); }

/* --------------------------------------------------------------------------
   3. Eyebrow — the numbered editorial section label
   -------------------------------------------------------------------------- */

.tv-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  font-family: var(--font-body);
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--gold-600);
  margin-bottom: 1rem;
}

.tv-eyebrow::after {
  content: '';
  display: block;
  width: 2.5rem;
  height: 1px;
  background: currentColor;
  opacity: .55;
}

.tv-section-head { margin-bottom: 3rem; }
.tv-section-head__copy { max-width: 46rem; }
.tv-section-head__title { font-size: clamp(1.875rem, 4vw, 3rem); margin-bottom: .75rem; }
.tv-section-head__lead { color: var(--muted); font-size: 1.0625rem; }

/* --------------------------------------------------------------------------
   4. Songket lattice — the belah ketupat motif
   -------------------------------------------------------------------------- */

.tv-lattice { position: relative; }

.tv-lattice::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: .06;
  background-image: linear-gradient(45deg, transparent 48%, rgba(168,121,42,.32) 49%, rgba(168,121,42,.32) 51%, transparent 52%), linear-gradient(-45deg, transparent 48%, rgba(168,121,42,.32) 49%, rgba(168,121,42,.32) 51%, transparent 52%);
  background-size: 48px 48px;
  background-size: 48px 48px;
}

.tv-section--slab.tv-lattice::before { opacity: .09; }

/* A single hairline divider carrying the same brass. */
.tv-rule {
  height: 1px;
  background: linear-gradient(90deg, var(--gold-600), transparent);
  border: 0;
  opacity: .4;
}

/* --------------------------------------------------------------------------
   5. Buttons
   -------------------------------------------------------------------------- */

.btn {
  --bs-btn-padding-y: .75rem;
  --bs-btn-padding-x: 1.5rem;
  --bs-btn-border-radius: var(--radius-pill);
  --bs-btn-font-weight: 600;
  transition: all .24s var(--ease);
}

/* `.btn-clay` moved to travl-tokens.css: the admin needs it too. */

.btn-primary {
  --bs-btn-bg: var(--primary-500);
  --bs-btn-border-color: var(--primary-500);
  --bs-btn-hover-bg: var(--primary-700);
  --bs-btn-hover-border-color: var(--primary-700);
  --bs-btn-active-bg: var(--primary-900);
  --bs-btn-active-border-color: var(--primary-900);
}

.btn-outline-ink {
  --bs-btn-color: var(--ink);
  --bs-btn-border-color: var(--line);
  --bs-btn-hover-color: var(--primary-700);
  --bs-btn-hover-bg: var(--primary-50);
  --bs-btn-hover-border-color: var(--primary-300);
}

/* Ghost link with an underline that grows from the left. */
.tv-link {
  position: relative;
  color: var(--primary-700);
  font-weight: 600;
  padding-bottom: 2px;
}
.tv-link::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  width: 0; height: 1px;
  background: var(--gold-600);
  transition: width .3s var(--ease);
}
.tv-link:hover::after { width: 100%; }

/* --------------------------------------------------------------------------
   6. Cards
   -------------------------------------------------------------------------- */

.tv-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  transition: transform .24s var(--ease), box-shadow .24s var(--ease);
}
.tv-card:hover { transform: translateY(-6px); box-shadow: var(--shadow-lg); }

/*
 * Stretching is for a grid of products, where every card in a row should end
 * at the same line. It was written as a blanket `height: 100%`, which also hit
 * every column that stacks two cards: each one claimed the full column height,
 * so the pair ran to twice the column and the overflow painted straight over
 * the footer. Account profile and booking detail both did this.
 *
 * `:only-child` is the actual condition — one card owning its column stretches,
 * a stacked pair keeps its natural height.
 */
.row > [class*="col"] > .tv-card:only-child,
.row > [class*="col"] > a.tv-card:only-child {
  height: 100%;
}

/* The account sidebar is a menu, not a panel to be matched. Stretching it to
   the height of a long form left a card that was two-thirds empty. It hugs its
   links and follows the page down instead. */
.row > [class*="col"] > .tv-account-nav:only-child {
  height: auto;
  position: sticky;
  top: calc(var(--nav-h) + 1rem);
}

/* Airbnb's rule: the image bleeds to the card edge with zero padding between
   it and the text block beneath. This one detail does most of the work. */
.tv-card__media {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--surface-2);
}
.tv-card__media img,
.tv-card__media picture {
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform .6s var(--ease);
}
.tv-card__media picture img { transition: inherit; }
.tv-card:hover .tv-card__media img { transform: scale(1.06); }

.tv-card__body { padding: 1.125rem 1.25rem 1.25rem; flex: 1 1 auto; display: flex; flex-direction: column; }
.tv-card__eyebrow { font-size: .75rem; color: var(--muted); margin-bottom: .375rem; }
.tv-card__title { font-size: 1.125rem; margin-bottom: .5rem; }
.tv-card__title a { color: var(--primary-700); }
.tv-card__title a:hover { color: var(--clay-600); }
.tv-card__meta { font-size: .8125rem; color: var(--muted); display: flex; flex-wrap: wrap; gap: .75rem; }
.tv-card__foot {
  margin-top: auto; padding-top: .875rem;
  display: flex; align-items: flex-end; justify-content: space-between; gap: .75rem;
  border-top: 1px solid var(--line);
}

.tv-price {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.25rem;
  color: var(--clay-600);
  line-height: 1.1;
}
.tv-price__from { display: block; font-family: var(--font-body); font-size: .6875rem; font-weight: 500; color: var(--muted); text-transform: uppercase; letter-spacing: .08em; }
.tv-price__strike { font-size: .875rem; color: var(--muted); text-decoration: line-through; font-weight: 400; }

/* Badge chips overlaid on the card image. */
.tv-card__badges { position: absolute; top: .75rem; left: .75rem; display: flex; flex-direction: column; align-items: flex-start; gap: .375rem; z-index: var(--z-card); }

.tv-chip {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .3rem .625rem;
  font-size: .6875rem; font-weight: 600;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, .94);
  color: var(--primary-700);
  backdrop-filter: blur(6px);
  box-shadow: var(--shadow-sm);
}
.tv-chip--success { background: var(--success); color: #fff; }
.tv-chip--gold    { background: var(--gold-600); color: #fff; }
.tv-chip--clay    { background: var(--clay-600); color: #fff; }

/* Wishlist heart, top-right of the media. */
.tv-card__wish-form { position: absolute; top: .75rem; right: .75rem; z-index: var(--z-card); line-height: 0; }

.tv-card__wish {
  width: 36px; height: 36px;
  display: grid; place-items: center;
  border: 0; border-radius: 50%;
  background: rgba(255, 255, 255, .92);
  color: var(--ink-soft);
  transition: all .2s var(--ease);
}
.tv-card__wish:hover, .tv-card__wish.is-active { background: var(--clay-600); color: #fff; }

.tv-rating { display: inline-flex; align-items: center; gap: .25rem; font-size: .8125rem; font-weight: 600; color: var(--ink); }
.tv-rating i { color: var(--gold-400); }
.tv-rating__count { color: var(--muted); font-weight: 400; }

/* Payment-method cards on checkout: a card-like radio option that avoids
   the flex / overflow conflict of stacking .tv-card and .form-check on the
   same element.  The whole card is clickable via its label. */
.tv-pm-card {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: .5rem;
  padding: .75rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  cursor: pointer;
  transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
}
.tv-pm-card:hover {
  border-color: var(--primary-300);
  box-shadow: var(--shadow-sm);
}
.tv-pm-card .form-check-input {
  margin-top: .1875rem;
  flex-shrink: 0;
}
.tv-pm-card .form-check-input:checked + .form-check-label {
  color: var(--ink);
}
.tv-pm-card .form-check-label {
  flex: 1;
  margin: 0;
  font-weight: 400;
  padding-top: 0;
}

/* --------------------------------------------------------------------------
   7. Arch mask — the Malay window, for homestay and host portraits
   -------------------------------------------------------------------------- */

.tv-arch { border-radius: var(--radius-arch); overflow: hidden; }
.tv-arch img { width: 100%; height: 100%; object-fit: cover; }

/* On a card the arch has to be matted, not clipped. Putting the arch radius on
   the media box clipped the box's own background too, so the corners outside
   the dome fell through to the card's white surface and read as four torn
   wedges. Insetting the image instead makes the shape deliberate. */
.tv-card__media.tv-arch {
  /* Square, not 4/3: a 200px dome on a landscape box is a squashed half-circle
     with four large empty corners. On a square the arch reads as the window it
     is meant to be. */
  aspect-ratio: 1 / 1;
  border-radius: 0;
  padding: .75rem .75rem 0;
  background: var(--surface);
}
.tv-card__media.tv-arch img,
.tv-card__media.tv-arch picture {
  border-radius: var(--radius-arch);
  overflow: hidden;
}

.tv-arch--sm { border-radius: 120px 120px var(--radius) var(--radius); }

/* --------------------------------------------------------------------------
   8. Navigation — transparent over hero, solid on scroll
   -------------------------------------------------------------------------- */

.tv-utility {
  /* Positioned so its dropdowns sit above the sticky nav. Without this the bar
     is static, its menus inherit Bootstrap's 1000, and the nav's 1030 paints
     the Sign in / Sign up buttons straight over the open currency list. */
  position: relative;
  z-index: var(--z-utility);
  background: var(--primary-900);
  color: rgba(255, 255, 255, .72);
  font-size: .8125rem;
  padding-block: .5rem;
}
.tv-utility a { color: rgba(255, 255, 255, .72); }
.tv-utility a:hover { color: #fff; }

/* The bar sets a near-white link colour for itself, and every .dropdown-item is
   an <a> — so the currency and language menus were rendering white text on a
   light surface, and the selected row came out in Bootstrap's default blue. */
.tv-utility .dropdown-menu {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: .375rem;
  min-width: 11rem;
}
.tv-utility .dropdown-item {
  color: var(--ink);
  border-radius: var(--radius-sm);
  padding: .5rem .75rem;
  font-size: .8125rem;
  font-weight: 500;
}
.tv-utility .dropdown-item:hover,
.tv-utility .dropdown-item:focus {
  background: var(--primary-50);
  color: var(--primary-700);
}
.tv-utility .dropdown-item.active,
.tv-utility .dropdown-item:active {
  background: var(--primary-500);
  color: #fff;
}
.tv-utility .dropdown-item.active .text-body-secondary { color: rgba(255, 255, 255, .75) !important; }

/* The brand lockup. One image, so it needs no colour rules of its own; what it
   does need is protection from the link and image defaults around it. The nav
   measures its own height into --nav-h, and an image with no intrinsic box would
   make that measurement race the decode, so the height is set in the markup. */
.tv-brand { text-decoration: none; flex-shrink: 0; }
.tv-brand:hover { text-decoration: none; opacity: .9; }
.tv-brand img { display: block; max-width: 100%; }

.tv-nav {
  position: sticky; top: 0; z-index: var(--z-nav);
  padding-block: 1rem;
  background: transparent;
  transition: background .3s var(--ease), box-shadow .3s var(--ease), padding .3s var(--ease);
}
/* A label never breaks mid-phrase: "Tur & Aktivitas" on two lines is what made
   the whole bar look broken before the breakpoint moved. */
.tv-nav .nav-link { color: rgba(255, 255, 255, .88); font-weight: 500; white-space: nowrap; }
.tv-nav .nav-link:hover, .tv-nav .nav-link.active { color: #fff; }

.tv-nav.is-stuck {
  background: var(--primary-700);
  box-shadow: 0 2px 20px rgba(7, 47, 41, .18);
  padding-block: .625rem;
}

/* Between xl and xxl the container is 1140px and the Indonesian labels need
   about 1150 — ten pixels short. Tightening the link padding in just that band
   buys 96px, which is the difference between a bar that fits and one that
   either wraps onto two lines or pushes the sign-up button off the screen. */
@media (min-width: 1200px) and (max-width: 1399.98px) {
  .tv-nav .nav-link { padding-inline: .5rem !important; }
}

/* Pages without a hero photo start solid. */
.tv-nav--solid { background: var(--primary-700); }

.tv-nav .dropdown-menu {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: .375rem;
}
.tv-nav .dropdown-item { border-radius: var(--radius-sm); padding: .5rem .75rem; }
.tv-nav .dropdown-item:hover,
.tv-nav .dropdown-item:focus { background: var(--primary-50); color: var(--primary-700); }

/* Cart count. Clay is the accent for the one thing that moves a sale forward,
   and on this bar nothing else competes for it. */
.tv-nav__badge {
  position: absolute;
  top: -2px; right: -4px;
  min-width: 18px; height: 18px;
  padding-inline: 4px;
  display: grid; place-items: center;
  border-radius: var(--radius-pill);
  background: var(--clay-600);
  color: #fff;
  font-size: .6875rem;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   9. Hero + overlapping search card
   -------------------------------------------------------------------------- */

.tv-hero {
  position: relative;
  /* Its own stacking context. Without it the negative-z media and scrim escape
     to the root, and any transform or opacity added to an ancestor later would
     make the photo vanish and drop white type onto cream. */
  isolation: isolate;
  z-index: 0;
  min-height: clamp(560px, 78vh, 820px);
  display: flex; align-items: center;
  margin-top: calc(-1 * var(--nav-h));
  padding-top: var(--nav-h);
  color: #fff;
}
/* A ground colour under the photo: if the image fails, white type still lands
   on something dark rather than on --paper. */
.tv-hero__media { position: absolute; inset: 0; z-index: -2; background: var(--primary-900); }
.tv-hero__media img { width: 100%; height: 100%; object-fit: cover; }
.tv-hero::after { content: ''; position: absolute; inset: 0; z-index: var(--z-behind); background: var(--scrim); }

.tv-hero__title { font-size: clamp(2.5rem, 6.5vw, 5rem); color: #fff; margin-bottom: 1rem; }
.tv-hero__lead { font-size: clamp(1rem, 1.6vw, 1.1875rem); color: rgba(255, 255, 255, .88); max-width: 38rem; }

/* The search card breaks the hero's lower boundary by half its own height. */
.tv-searchbar {
  --field-h: 2.75rem;
  position: relative;
  z-index: var(--z-overlap);
  margin-top: -3.5rem;
  background: var(--surface);
  border-radius: var(--radius-xl);
  box-shadow: var(--shadow-lg);
  padding: 1.25rem;
}

/* Every cell is a column: label pinned to the top, control to the bottom. The
   four controls have four different intrinsic heights — a Tom Select wrapper, a
   text input, a stepper and a button — so without one shared control height
   they sat on four different baselines. */
.tv-searchbar__field,
.tv-searchbar__submit {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}

.tv-searchbar label {
  font-size: .6875rem; font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted);
  margin-bottom: .25rem;
}

.tv-searchbar .form-control,
.tv-searchbar .form-select {
  border: 0;
  padding: 0;
  font-weight: 500;
  box-shadow: none;
  background: transparent;
  min-height: var(--field-h);
  height: var(--field-h);
  display: flex;
  align-items: center;
}
.tv-searchbar .form-control:focus,
.tv-searchbar .form-select:focus { box-shadow: none; outline: 2px solid var(--primary-500); outline-offset: 2px; }

/* Tom Select replaces the <select> with its own markup and copies the original
   classes onto a wrapper, so none of the rules above reached the real control:
   the destination field kept a border, a background and a 12px indent while
   every field beside it had been stripped bare. */
.tv-searchbar .ts-wrapper { width: 100%; }
.tv-searchbar .ts-wrapper.form-select,
.tv-searchbar .ts-wrapper.form-control {
  border: 0;
  background: transparent;
  box-shadow: none;
  padding: 0;
}
.tv-searchbar .ts-wrapper.form-select .ts-control,
.tv-searchbar .ts-wrapper.form-control .ts-control {
  border: 0;
  background: transparent;
  padding: 0;
  min-height: var(--field-h);
  height: var(--field-h);
  display: flex;
  align-items: center;
  font-weight: 500;
}
.tv-searchbar .ts-control::after { display: none; }
.tv-searchbar .ts-control .item { color: var(--ink); }

/* The Tom Select panel is a shared surface — the filter rails on the catalogue
   pages use it too — so it is themed globally rather than only in the hero. */
.ts-dropdown {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  margin-top: .5rem;
  padding: .375rem;
  overflow: hidden;
}
.ts-dropdown .option {
  border-radius: var(--radius-sm);
  padding: .5rem .75rem;
  font-size: .875rem;
  color: var(--ink);
}
.ts-dropdown .option.active { background: var(--primary-50); color: var(--primary-700); }
.ts-dropdown .option[aria-selected="true"] { font-weight: 600; }
.ts-dropdown .no-results { padding: .5rem .75rem; color: var(--muted); font-size: .875rem; }

/* Everywhere else the control has to line up with the plain inputs beside it —
   on the filter rails it was 38px tall next to 50px form fields. */
.ts-wrapper.form-select,
.ts-wrapper.form-control { padding: 0; }
.ts-wrapper.form-select .ts-control,
.ts-wrapper.form-control .ts-control {
  padding: .75rem 1rem;
  border-radius: var(--radius-sm);
  /* 50px border box, matching .form-control beside it. */
  min-height: calc(3.125rem - 2px);
  background: transparent;
}

.tv-searchbar .tv-stepper { height: var(--field-h); }

.tv-searchbar__submit .btn { height: var(--field-h); }

/* Hairline dividers only where the fields actually sit side by side. */
@media (min-width: 992px) {
  .tv-searchbar__field + .tv-searchbar__field {
    border-left: 1px solid var(--line);
    padding-left: 1.5rem;
  }
  /* The submit is the end of the row, not another field — no divider. */
  .tv-searchbar__submit { padding-left: 1rem; }
}

/* Category tabs sitting above the search card. */
.tv-cat-tabs { display: flex; flex-wrap: wrap; gap: .5rem; }
.tv-cat-tabs button {
  border: 1px solid rgba(255, 255, 255, .35);
  background: rgba(255, 255, 255, .1);
  color: #fff;
  border-radius: var(--radius-pill);
  padding: .5rem 1.125rem;
  font-size: .875rem; font-weight: 600;
  backdrop-filter: blur(6px);
  transition: all .2s var(--ease);
}
.tv-cat-tabs button:hover { background: rgba(255, 255, 255, .2); }
.tv-cat-tabs button.is-active { background: #fff; color: var(--primary-700); border-color: #fff; }

/* --------------------------------------------------------------------------
   10. Stats slab
   -------------------------------------------------------------------------- */

.tv-stat { text-align: center; }
.tv-stat__value { font-family: var(--font-display); font-size: clamp(2.25rem, 4.5vw, 3.5rem); font-weight: 600; color: #fff; line-height: 1; font-variant-numeric: tabular-nums; }
.tv-stat__label { font-size: .8125rem; letter-spacing: .08em; text-transform: uppercase; color: var(--gold-400); margin-top: .625rem; }

/* --------------------------------------------------------------------------
   11. Footer
   -------------------------------------------------------------------------- */

.tv-footer { background: var(--primary-900); color: rgba(255, 255, 255, .68); position: relative; overflow: hidden; }
.tv-footer h5,
.tv-footer__heading { color: #fff; font-size: .8125rem; letter-spacing: .12em; text-transform: uppercase; font-family: var(--font-body); font-weight: 700; margin-bottom: 1.25rem; }
.tv-footer a { color: rgba(255, 255, 255, .68); }
.tv-footer a:hover { color: var(--gold-400); }
.tv-footer__bottom { border-top: 1px solid rgba(255, 255, 255, .12); }

/* The newsletter field sits on --primary-900. Without an explicit placeholder
   colour the browser default is dark grey on near-black — unreadable. */
.tv-footer .tv-footer__input {
  background-color: rgba(255, 255, 255, .08);
  border-color: rgba(255, 255, 255, .16);
  color: #fff;
}
.tv-footer .tv-footer__input::placeholder { color: rgba(255, 255, 255, .55); }
.tv-footer .tv-footer__input:focus {
  background-color: rgba(255, 255, 255, .12);
  border-color: var(--gold-400);
  color: #fff;
  box-shadow: none;
}

/* --------------------------------------------------------------------------
   12. Floating WhatsApp
   -------------------------------------------------------------------------- */

.tv-wa {
  position: fixed; right: 1.25rem; bottom: 1.25rem; z-index: var(--z-fab);
  width: 54px; height: 54px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: #25D366; color: #fff;
  font-size: 1.5rem;
  box-shadow: 0 8px 24px rgba(37, 211, 102, .4);
  transition: transform .24s var(--ease);
}
.tv-wa:hover { transform: scale(1.08); color: #fff; }

/* Clear of the sticky booking bar, which is fixed to bottom: 0 below 992px. */
@media (max-width: 991.98px) {
  body:has(.tv-stickybar) .tv-wa { bottom: 5.25rem; }
}

/* Mobile sticky booking bar on detail pages. */
.tv-stickybar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: var(--z-stickybar);
  background: var(--surface);
  border-top: 1px solid var(--line);
  box-shadow: 0 -4px 20px rgba(7, 47, 41, .1);
  padding: .75rem 1rem;
}
@media (min-width: 992px) { .tv-stickybar { display: none; } }

/* --------------------------------------------------------------------------
   13. Forms
   -------------------------------------------------------------------------- */

.form-label { font-size: .8125rem; font-weight: 600; color: var(--ink-soft); }

.form-control, .form-select {
  border-color: var(--line);
  border-radius: var(--radius-sm);
  padding: .75rem 1rem;
  background-color: var(--surface);
}
.form-control:focus, .form-select:focus {
  border-color: var(--primary-300);
  box-shadow: 0 0 0 .2rem rgba(23, 117, 102, .14);
}

/* Pax stepper */
.tv-stepper { display: inline-flex; align-items: center; gap: .75rem; }
.tv-stepper button {
  width: 32px; height: 32px;
  display: grid; place-items: center;
  border: 1px solid var(--line);
  background: var(--surface);
  border-radius: 50%;
  color: var(--primary-700);
  transition: all .2s var(--ease);
}
.tv-stepper button:hover:not(:disabled) { border-color: var(--primary-500); background: var(--primary-50); }
.tv-stepper button:disabled { opacity: .4; cursor: not-allowed; }
.tv-stepper output { min-width: 2ch; text-align: center; font-weight: 600; font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------------------
   14. Long-form copy from the CMS
   --------------------------------------------------------------------------
   .tv-prose wraps every admin-authored rich-text block on the site — tour and
   stay descriptions, journal articles, static pages. It had no rules at all
   until now, so all of that rendered at browser defaults, and a pasted table
   ran off the side of the page.
   -------------------------------------------------------------------------- */

.tv-prose { max-width: 68ch; color: var(--ink); }

.tv-prose > * + * { margin-top: 1.15em; }
.tv-prose p { margin-bottom: 0; }

.tv-prose h2, .tv-prose h3, .tv-prose h4 { margin-top: 2em; margin-bottom: .6em; }
.tv-prose h2 { font-size: 1.625rem; }
.tv-prose h3 { font-size: 1.25rem; }
.tv-prose h4 { font-size: 1.0625rem; }
.tv-prose > :first-child { margin-top: 0; }

.tv-prose a { color: var(--primary-700); text-decoration: underline; text-underline-offset: .18em; }
.tv-prose a:hover { color: var(--clay-600); }

.tv-prose ul, .tv-prose ol { padding-left: 1.35rem; }
.tv-prose li + li { margin-top: .4em; }
.tv-prose li::marker { color: var(--gold-600); }

.tv-prose blockquote {
  margin-inline: 0;
  padding: .25rem 0 .25rem 1.25rem;
  border-left: 2px solid var(--gold-600);
  font-family: var(--font-display);
  font-size: 1.0625rem;
  color: var(--ink-soft);
}

.tv-prose img { border-radius: var(--radius); }
.tv-prose figure figcaption { font-size: .8125rem; color: var(--muted); margin-top: .5rem; }
.tv-prose hr { border: 0; height: 1px; background: var(--line); margin-block: 2em; }

.tv-prose code {
  font-size: .875em;
  background: var(--surface-2);
  padding: .15em .4em;
  border-radius: var(--radius-sm);
  color: var(--primary-900);
}
.tv-prose pre { overflow-x: auto; padding: 1rem; background: var(--surface-2); border-radius: var(--radius); }
.tv-prose pre code { background: none; padding: 0; }

/* An editor cannot be asked to remember that a wide table breaks a phone. */
.tv-prose table { display: block; width: 100%; overflow-x: auto; border-collapse: collapse; }
.tv-prose th, .tv-prose td { border: 1px solid var(--line); padding: .5rem .75rem; text-align: left; }
.tv-prose th { background: var(--surface-2); font-weight: 600; }

/* --------------------------------------------------------------------------
   15. Status pills
   --------------------------------------------------------------------------
   Booking pages use these; they lived only in travl-admin.css, which the
   public layout never loads, so every status rendered as bare text.
   -------------------------------------------------------------------------- */

.tv-status {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .25rem .625rem;
  font-size: .75rem; font-weight: 600;
  border-radius: var(--radius-pill);
}
.tv-status::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.tv-status--success   { background: rgba(46, 125, 91, .12);  color: #246147; }
.tv-status--warning   { background: rgba(201, 138, 18, .14); color: #8A5E0C; }
.tv-status--danger    { background: rgba(179, 64, 47, .12);  color: #8E3325; }
.tv-status--info      { background: rgba(42, 111, 151, .12); color: #1F5372; }
.tv-status--secondary { background: #F1F3F5; color: #5B6470; }
.tv-status--dark      { background: rgba(7, 47, 41, .1);     color: var(--primary-900); }

/* --------------------------------------------------------------------------
   16. Page heads and section rhythm
   --------------------------------------------------------------------------
   There were four inline <h1> clamp recipes and five inline padding-block
   values scattered across the inner pages. One home for both.
   -------------------------------------------------------------------------- */

.tv-page-head { margin-bottom: 2rem; }
.tv-page-head__title { font-size: clamp(1.75rem, 4vw, 2.75rem); margin-bottom: .5rem; }
.tv-page-head__lead { color: var(--muted); font-size: 1.0625rem; margin-bottom: 0; max-width: 60ch; }

/* Catalogue and article pages start under a solid nav rather than a hero, so
   they need a smaller opening gap than the full section rhythm. */
.tv-section--head { padding-block: 3rem 2rem; }   /* catalogue header band */
.tv-section--body { padding-block: 2rem 4rem; }   /* results, detail bodies */
.tv-section--panel { padding-block: 2.5rem 4rem; } /* cart, checkout, payment */
.tv-section--top { padding-top: 3rem; }            /* opens under a solid nav */

/* --------------------------------------------------------------------------
   17. Carousel rails
   --------------------------------------------------------------------------
   Swiper clips at its own edge, so a card lifting 6px on hover and casting a
   48px shadow was guillotined on three sides. The padding gives the shadow
   room and the matching negative margin keeps the slides aligned with the
   container the heading sits in.
   -------------------------------------------------------------------------- */

.tv-rail {
  position: relative;
  /* The horizontal pull can never exceed the container's own 12px gutter, or
     the rail hangs past the viewport edge on a phone and clips the peeking
     card in the wrong place. */
  padding: 1.25rem .75rem;
  margin: -1.25rem -.75rem;
}

@media (min-width: 576px) {
  .tv-rail { padding-inline: 1.25rem; margin-inline: -1.25rem; }
}

.tv-rail .swiper-slide { height: auto; }

.tv-rail__nav { display: flex; gap: .5rem; justify-content: flex-end; margin-top: 1rem; }

.tv-rail__btn {
  width: var(--tap-min); height: var(--tap-min);
  display: grid; place-items: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
  color: var(--primary-700);
  font-size: 1.25rem;
  transition: all .2s var(--ease);
}
.tv-rail__btn:hover:not(:disabled) { border-color: var(--primary-500); background: var(--primary-50); }
.tv-rail__btn:disabled { opacity: .35; cursor: not-allowed; }

/* --------------------------------------------------------------------------
   18. Detail pages
   --------------------------------------------------------------------------
   These lived in three separate inline <style> blocks, one per detail view,
   with .tv-detail__hero defined identically in all three.
   -------------------------------------------------------------------------- */

.tv-detail__hero { aspect-ratio: 16 / 7; border-radius: var(--radius-xl); overflow: hidden; }
.tv-detail__hero img,
.tv-detail__hero picture { width: 100%; height: 100%; object-fit: cover; display: block; }

.tv-itinerary { position: relative; padding-left: 1.75rem; }
.tv-itinerary::before {
  content: ''; position: absolute; left: 7px; top: .5rem; bottom: .5rem;
  width: 1px; background: var(--line);
}
.tv-itinerary__step { position: relative; padding-bottom: 1.5rem; }
.tv-itinerary__step::before {
  content: ''; position: absolute; left: -1.75rem; top: .35rem;
  width: 15px; height: 15px; border-radius: 50%;
  background: var(--surface); border: 2px solid var(--gold-600);
}

.tv-unit { border: 1px solid var(--line); border-radius: var(--radius); padding: 1.25rem; }
.tv-unit + .tv-unit { margin-top: 1rem; }

/* A fare-table row is a form, not a table row: five controls that have to stay
   legible when the leg names are long and the screen is 390px wide. */
.tv-fare {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 9rem 10rem auto auto;
  align-items: center;
  gap: .75rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem 1.25rem;
  background: var(--surface);
}

.tv-fare__route { font-weight: 600; color: var(--primary-700); }
.tv-fare__route i { color: var(--gold-600); }
.tv-fare__price { text-align: right; }
.tv-fare__price .tv-price { font-size: 1.125rem; }
.tv-fare__action { min-width: 9.5rem; }

@media (max-width: 991.98px) {
  .tv-fare {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }
  .tv-fare__leg { grid-column: 1 / -1; }
  .tv-fare__price { grid-column: 1 / -1; text-align: left; }
  .tv-fare__action { grid-column: 1 / -1; }
}

@media (max-width: 575.98px) {
  .tv-fare { grid-template-columns: minmax(0, 1fr); padding: 1rem; }
}

.tv-departure { border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem 1.25rem; }
.tv-departure + .tv-departure { margin-top: .75rem; }
.tv-departure--sold { opacity: .55; background: var(--surface-2); }

/* top offset clears the sticky nav */
.tv-booking { position: sticky; top: 6rem; }
@media (max-width: 991.98px) { .tv-booking { position: static; } }

/* --------------------------------------------------------------------------
   19. Flash region
   --------------------------------------------------------------------------
   Fixed rather than inline: the hero is pulled up under the nav by a negative
   margin, so a block placed above it in flow is simply covered.
   -------------------------------------------------------------------------- */

.tv-toasts {
  position: fixed;
  top: 1rem; right: 1rem;
  z-index: var(--z-toast);
  width: min(26rem, calc(100vw - 2rem));
  display: grid;
  gap: .5rem;
  pointer-events: none;
}
.tv-toasts:empty { display: none; }
.tv-toasts .alert { pointer-events: auto; margin-bottom: 0; }

/* Filter rail group label. Was a 13-declaration inline style attribute on an
   <h3>, repeated by every group on all three catalogue pages. */
.tv-filter-label {
  display: block;
  margin-bottom: .5rem;
  font-family: var(--font-body);
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
}

/* --------------------------------------------------------------------------
   19b. Description lists
   --------------------------------------------------------------------------
   The booking summaries pair a label and a figure in a narrow sticky column.
   Bare `justify-content: between` let a long currency string collide with its
   own label; the gap and the shrink rule keep them apart.
   -------------------------------------------------------------------------- */

.tv-datalist__row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
}
.tv-datalist__row > dt { min-width: 0; }
.tv-datalist__row > dd { flex-shrink: 0; text-align: right; white-space: nowrap; }

/* --------------------------------------------------------------------------
   20. Empty states
   -------------------------------------------------------------------------- */

.tv-empty-state {
  position: relative;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: 3rem 1.5rem;
  text-align: center;
}
.tv-empty-state__icon { font-size: 2.5rem; color: var(--line); line-height: 1; }
.tv-empty-state__title { font-size: 1.125rem; margin: 1rem 0 .5rem; }
.tv-empty-state__lead { color: var(--muted); margin-bottom: 1.25rem; }
.tv-empty-state > * { position: relative; z-index: 1; }

/* --------------------------------------------------------------------------
   19c. Destination chat
   --------------------------------------------------------------------------
   A shared room, so the log reads as a transcript rather than a messaging app:
   your own messages sit right and tinted, everyone else's left on the surface.
   -------------------------------------------------------------------------- */

.tv-chat {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  overflow: hidden;
}

.tv-chat__log {
  list-style: none;
  margin: 0;
  padding: 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-height: 18rem;
  max-height: 60vh;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.tv-chat__msg { display: flex; gap: .75rem; align-items: flex-start; }
.tv-chat__msg.is-mine { flex-direction: row-reverse; }

.tv-chat__avatar {
  flex: 0 0 36px;
  width: 36px; height: 36px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--primary-50);
  color: var(--primary-700);
  font-size: .75rem; font-weight: 700;
}
.tv-chat__avatar--lg { width: 44px; height: 44px; flex-basis: 44px; font-size: 1.125rem; }

.tv-chat__bubble {
  max-width: 42ch;
  padding: .625rem .875rem;
  border-radius: var(--radius);
  background: var(--surface-2);
}
.tv-chat__msg.is-mine .tv-chat__bubble { background: var(--primary-50); }

.tv-chat__who {
  margin: 0 0 .125rem;
  font-size: .6875rem; font-weight: 700;
  letter-spacing: .04em;
  color: var(--primary-700);
  display: flex; gap: .5rem; align-items: baseline;
}
.tv-chat__who time { font-weight: 400; color: var(--muted); font-variant-numeric: tabular-nums; }

.tv-chat__body { margin: 0; font-size: .9375rem; overflow-wrap: anywhere; white-space: pre-line; }

/* Deliberately quiet: reporting should be findable, not the loudest thing in
   the bubble. It surfaces on hover and whenever it has keyboard focus. */
.tv-chat__report { margin: .375rem 0 0; }
.tv-chat__report-btn {
  border: 0;
  background: none;
  padding: 0;
  font-size: .6875rem;
  font-weight: 600;
  color: var(--muted);
  opacity: 0;
  transition: opacity .2s var(--ease), color .2s var(--ease);
}
.tv-chat__msg:hover .tv-chat__report-btn,
.tv-chat__report-btn:focus-visible { opacity: 1; }

/* There is no hover on a phone, so the quiet treatment would hide reporting
   from exactly the people most likely to need it. Padding (cancelled by an
   equal negative margin) clears the 24px floor without moving the bubble. */
.tv-chat__report-btn {
  padding: .25rem .375rem;
  margin: -.25rem -.375rem;
}
@media (hover: none) {
  .tv-chat__report-btn { opacity: 1; }
}
.tv-chat__report-btn:hover { color: var(--clay-600); }
.tv-chat__report-btn:disabled { opacity: 1; color: var(--success); cursor: default; }

/* Touch has no hover, so there it is simply always visible but understated. */
@media (hover: none) {
  .tv-chat__report-btn { opacity: .65; }
}

.tv-chat__empty {
  margin: auto;
  text-align: center;
  color: var(--muted);
}
.tv-chat__empty i { font-size: 2rem; color: var(--line); display: block; margin-bottom: .5rem; }

.tv-chat__compose {
  display: flex;
  gap: .625rem;
  align-items: flex-end;
  padding: .875rem 1rem;
  border-top: 1px solid var(--line);
  background: var(--surface);
}
.tv-chat__compose textarea {
  resize: none;
  max-height: 8rem;
  min-height: var(--tap-min);
  padding-block: .625rem;
}
.tv-chat__compose .btn { flex-shrink: 0; min-height: var(--tap-min); }

.tv-chat__closed {
  margin: 0;
  padding: .875rem 1rem;
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: .875rem;
}



@media (max-width: 575.98px) {
  .tv-chat__log { padding: 1rem; max-height: 55vh; }
  .tv-chat__bubble { max-width: 100%; }
}

/* --------------------------------------------------------------------------
   20a. Load more
   -------------------------------------------------------------------------- */

.tv-loadmore {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .625rem;
  margin-top: 2.5rem;
}
.tv-loadmore__count { margin: 0; font-size: .8125rem; color: var(--muted); }

/* --------------------------------------------------------------------------
   20b. Tap targets
   --------------------------------------------------------------------------
   WCAG 2.5.8 and both platform HIGs want ~44px. Several controls here are
   deliberately smaller than that visually — a 36px heart, a 32px stepper
   button — so the hit area is grown with a pseudo-element instead of the box,
   leaving the desktop layout untouched.
   -------------------------------------------------------------------------- */

.tv-tap::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: var(--tap-min); height: var(--tap-min);
  transform: translate(-50%, -50%);
}

.tv-card__wish,
.tv-stepper button,
.tv-rail__btn { position: relative; }

.tv-card__wish::after,
.tv-stepper button::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: var(--tap-min); height: var(--tap-min);
  transform: translate(-50%, -50%);
}

/* Footer link lists are navigation, not prose: 23px rows are a mis-tap on a
   phone. They get the floor too, centred so the rhythm still reads. */
.tv-footer a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
}
.tv-footer__bottom a { min-height: 0; }
.tv-footer .rounded-circle { min-width: var(--tap-min); justify-content: center; }

/* Standalone controls simply grow: they have the room. */
.tv-cat-tabs button { min-height: var(--tap-min); }

.tv-nav .btn-link,
.tv-nav .btn-sm { min-width: var(--tap-min); min-height: var(--tap-min); }

/* The brand lockup is the largest tap target on the bar; it should not be the
   shortest. */
.tv-nav > .container a { min-height: var(--tap-min); }

/* The utility bar's currency and language pickers are 23px tall. The bar is
   deliberately slim, so the hit area grows instead of the box — the same
   pseudo-element trick used on the wishlist heart. */
.tv-utility .btn,
.tv-utility .dropdown > .btn { position: relative; }
.tv-utility .btn::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 100%;
  min-width: var(--tap-min);
  height: var(--tap-min);
  transform: translate(-50%, -50%);
}

/* The drawer's close button is the escape route from a full-screen overlay;
   32px is not enough, and there is plenty of room in the header. */
.offcanvas .btn-close {
  /* Bootstrap leaves .btn-close on content-box, so its padding would land on
     top of the size set here and make a 60px button. */
  box-sizing: border-box;
  width: var(--tap-min);
  height: var(--tap-min);
  padding: 0;
  background-size: 1rem;
}

/* A section's "view all" is a control, not prose, so it gets the same floor.
   Inline links inside a paragraph are exempt under WCAG 2.5.8 and unchanged. */
.tv-section-head .tv-link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
}

/* --------------------------------------------------------------------------
   21. Accessibility
   -------------------------------------------------------------------------- */

.visually-hidden-focusable:focus {
  position: fixed;
  top: 1rem; left: 1rem;
  z-index: var(--z-skip);
  padding: .75rem 1.25rem;
  background: var(--surface);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lg);
}

/* Honouring this is a few lines, and it is the difference between motion that
   feels considered and motion that makes people ill. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }

  .tv-card:hover { transform: none; }
  .tv-card:hover .tv-card__media img { transform: none; }
}

/* The status number on an error page: present, but never the loudest thing —
   the sentence above it is what actually helps. */
.tv-empty-state__code {
  margin: .75rem 0 0;
  font-family: var(--font-display);
  font-size: 2.5rem;
  font-weight: 600;
  line-height: 1;
  color: var(--line);
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   Nav icon buttons
   --------------------------------------------------------------------------
   The 44px floor from the tap-target pass applied to every .btn-link on the
   bar, so a 20px heart sat in a 44px box: the heart and the bag ended up ~32px
   apart, and the cart badge — anchored to the padded box, not the glyph —
   floated well clear of the icon it belongs to. The glyph box shrinks to its
   own size; the 44px target comes back as a pseudo-element, exactly as it does
   on the wishlist heart and the stepper.
   -------------------------------------------------------------------------- */

.tv-nav .tv-nav__icon {
  position: relative;
  min-width: 0;
  width: 2rem;
  padding: 0;
  justify-content: center;
  font-size: 1.125rem;
}

.tv-nav .tv-nav__icon::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--tap-min);
  height: var(--tap-min);
  transform: translate(-50%, -50%);
}

/* Anchored to the glyph now, so it reads as belonging to the bag. */
.tv-nav__icon .tv-nav__badge {
  top: -1px;
  right: -2px;
}

/* --------------------------------------------------------------------------
   19d. Departure rooms
   --------------------------------------------------------------------------
   A room is one item on one day, so the date leads: it is what tells two
   otherwise identical rooms apart at a glance.
   -------------------------------------------------------------------------- */

.tv-chat__rooms { list-style: none; margin: 0; padding: 0; display: grid; gap: .75rem; }

.tv-chat__room {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1rem 1.25rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--ink);
  text-decoration: none;
  transition: border-color .2s var(--ease), transform .2s var(--ease);
}
.tv-chat__room:hover {
  color: var(--ink);
  border-color: var(--primary-300);
  transform: translateY(-2px);
}

.tv-chat__room-date {
  flex: 0 0 3.25rem;
  display: grid;
  place-items: center;
  padding: .4rem 0;
  border-radius: var(--radius);
  background: var(--primary-50);
  color: var(--primary-700);
  line-height: 1.1;
}
.tv-chat__room-day { font-size: 1.25rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.tv-chat__room-mon { font-size: .625rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }

.tv-chat__room-main { flex: 1 1 auto; min-width: 0; }
.tv-chat__room-name { display: block; font-weight: 600; }
.tv-chat__room-meta { display: block; font-size: .8125rem; color: var(--muted); }
.tv-chat__room-go { flex-shrink: 0; color: var(--gold-600); }

/* The departure line under the room title. */
.tv-chat__departure {
  display: flex;
  align-items: center;
  gap: .4rem;
  font-size: .875rem;
  color: var(--muted);
}

/* Staff are unmistakable: advice from the operator is not advice from a
   stranger, and a moderator asking someone to stop should look like one. */
.tv-chat__badge {
  font-size: .5625rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  padding: .1rem .35rem;
  border-radius: var(--radius-pill);
}
.tv-chat__badge--staff { background: var(--gold-600); color: #fff; }
.tv-chat__badge--admin { background: var(--primary-700); color: #fff; }
.tv-chat__avatar--staff { background: var(--gold-600); color: #fff; }
.tv-chat__avatar--admin { background: var(--primary-700); color: #fff; }

/* Quoted reply inside a bubble. */
.tv-chat__quote {
  display: block;
  margin-bottom: .375rem;
  padding: .3rem .5rem;
  border-left: 3px solid var(--primary-300);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: rgba(23, 117, 102, .06);
  text-decoration: none;
  color: inherit;
}
.tv-chat__quote:hover { background: rgba(23, 117, 102, .12); color: inherit; }
.tv-chat__quote-who { display: block; font-size: .625rem; font-weight: 700; color: var(--primary-700); }
.tv-chat__quote-body { display: block; font-size: .75rem; color: var(--muted); overflow-wrap: anywhere; }

/* The overflow menu. Quiet until the message is hovered or focused, but always
   present on touch, where there is no hover to reveal it with. */
.tv-chat__menu { align-self: center; flex-shrink: 0; }
.tv-chat__menu-btn {
  border: 0;
  background: none;
  padding: .25rem;
  line-height: 1;
  color: var(--muted);
  border-radius: 50%;
  opacity: 0;
  transition: opacity .2s var(--ease), background-color .2s var(--ease);
  position: relative;
}
.tv-chat__menu-btn::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: var(--tap-min); height: var(--tap-min);
  transform: translate(-50%, -50%);
}
.tv-chat__msg:hover .tv-chat__menu-btn,
.tv-chat__menu-btn:focus-visible,
.tv-chat__menu.show .tv-chat__menu-btn { opacity: 1; }
.tv-chat__menu-btn:hover { background: var(--surface-2); color: var(--ink); }

@media (hover: none) {
  .tv-chat__menu-btn { opacity: .55; }
}

/* The "replying to X" strip above the composer. */
.tv-chat__replying {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  padding: .5rem 1rem;
  border-top: 1px solid var(--line);
  background: var(--primary-50);
  font-size: .8125rem;
  color: var(--primary-700);
}
.tv-chat__replying-x {
  border: 0;
  background: none;
  padding: .25rem;
  line-height: 1;
  color: var(--primary-700);
  border-radius: 50%;
  position: relative;
}
.tv-chat__replying-x::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: var(--tap-min); height: var(--tap-min);
  transform: translate(-50%, -50%);
}

/* A reported message, once this viewer has flagged it. */
.tv-chat__msg.is-reported .tv-chat__bubble { opacity: .6; }

/* Breadcrumb links are navigation rather than prose, so the 24px floor from
   WCAG 2.5.8 applies to them; Bootstrap leaves them at the line height. */
.breadcrumb-item > a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}
