/* ==========================================================================
   TERRAVANA — COMPONENTS
   Patterns used in three or more places. Anything used once or twice stays
   inside its own section file until it earns promotion to this file.
   Depends on tokens.css + base.css.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. ICONS
   -------------------------------------------------------------------------- */

.tv-icon {
  width: 1.25em;
  height: 1.25em;
  flex: none;
  vertical-align: middle;
}
.tv-icon--sm { width: 1em;    height: 1em; }
.tv-icon--lg { width: 1.75em; height: 1.75em; }
.tv-icon--xl { width: 2.25em; height: 2.25em; }

/* --------------------------------------------------------------------------
   2. BUTTONS
   -------------------------------------------------------------------------- */

.tv-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--tv-space-2);
  min-height: 52px; /* Above the 44px WCAG floor; scaled up with the header. */
  padding: var(--tv-space-4) var(--tv-space-7);
  border: 1px solid transparent;
  border-radius: var(--tv-radius-sm);
  font-family: var(--tv-font-body);
  font-size: var(--tv-fs-body);
  font-weight: var(--tv-fw-semibold);
  letter-spacing: var(--tv-ls-button);
  line-height: 1;
  text-transform: uppercase;
  text-decoration: none;
  text-align: center;
  cursor: pointer;
  transition:
    background-color var(--tv-dur-fast) var(--tv-ease),
    border-color     var(--tv-dur-fast) var(--tv-ease),
    color            var(--tv-dur-fast) var(--tv-ease),
    transform        var(--tv-dur-fast) var(--tv-ease),
    box-shadow       var(--tv-dur-fast) var(--tv-ease);
}

.tv-btn:hover  { transform: translateY(-2px); }
.tv-btn:active { transform: translateY(0); }

.tv-btn .tv-icon {
  width: 1.1em;
  height: 1.1em;
  transition: transform var(--tv-dur-fast) var(--tv-ease);
}
.tv-btn:hover .tv-icon { transform: translateX(3px); }

/* Primary — forest fill. The hero and section CTAs. */
.tv-btn--primary {
  background-color: var(--tv-forest);
  border-color: var(--tv-forest);
  color: var(--tv-on-dark);
  box-shadow: var(--tv-shadow-sm);
}
.tv-btn--primary:hover {
  background-color: var(--tv-forest-deep);
  border-color: var(--tv-forest-deep);
  color: var(--tv-on-dark);
  box-shadow: var(--tv-shadow-md);
}

/* Accent — gold fill. Header CTA and the closing conversion band only, so the
   accent keeps its weight instead of appearing on every button.

   The label is forest-deep rather than white. Warm Gold is a light colour, so
   ivory on it is 2.16:1 — unreadable. Forest-deep on gold is 5.6:1. On hover
   the ground darkens to gold-deep and the label has to invert to ivory
   (5.7:1); it cannot stay dark, because forest on gold-deep is only 2.1:1. */
.tv-btn--accent {
  background-color: var(--tv-gold);
  /* A deeper edge around the fill. Gold against ivory is 2.16:1, so a gold
     border leaves the button with no boundary the page can be told apart
     from; this gives it a 5.7:1 edge without dimming the fill. */
  border-color: var(--tv-gold-deep);
  color: var(--tv-forest-deep);
  box-shadow: var(--tv-shadow-sm);
}
.tv-btn--accent:hover {
  background-color: var(--tv-gold-deep);
  border-color: var(--tv-gold-deep);
  color: var(--tv-on-dark);
  box-shadow: var(--tv-shadow-md);
}

/* Ghost — on light grounds. The border takes the same deep gold as the label
   instead of the accent: at 2.16:1 a plain gold outline is not a visible
   boundary, and the outline is what identifies this as a button. */
.tv-btn--ghost {
  background-color: transparent;
  border-color: var(--tv-gold-deep);
  color: var(--tv-gold-deep);
}
/* The border holds its deep step through the hover — lightening it here would
   weaken the boundary at the moment the control is being used. */
.tv-btn--ghost:hover {
  background-color: var(--tv-gold);
  border-color: var(--tv-gold-deep);
  color: var(--tv-forest-deep);
}

/* Ghost light — on forest grounds and image overlays. */
.tv-btn--ghost-light {
  background-color: transparent;
  border-color: var(--tv-on-dark-border);
  color: var(--tv-on-dark);
}
.tv-btn--ghost-light:hover {
  background-color: var(--tv-on-dark);
  border-color: var(--tv-on-dark);
  color: var(--tv-forest);
}

/* Text link with a sliding arrow — the "Learn more" in every card. */
.tv-btn--text {
  min-height: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--tv-gold-deep);
  text-transform: uppercase;
}
/* Darkens toward the primary on hover. Lightening to the accent would drop
   the link to 2.16:1 at the moment it is being pointed at. */
.tv-btn--text:hover {
  transform: none;
  color: var(--tv-forest);
}

.tv-btn--lg    { min-height: 62px; padding: var(--tv-space-5) var(--tv-space-8); font-size: var(--tv-fs-body); }
.tv-btn--sm    { min-height: 40px; padding: var(--tv-space-3) var(--tv-space-5); font-size: var(--tv-fs-caption); }
.tv-btn--block { width: 100%; }

.tv-btn-group {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tv-space-4);
  align-items: center;
}

/* --------------------------------------------------------------------------
   3. RULES & DIVIDERS
   The gold hairline with a centred leaf — the guideline's recurring motif.
   -------------------------------------------------------------------------- */

.tv-rule {
  display: flex;
  align-items: center;
  gap: var(--tv-space-4);
  margin-block: var(--tv-space-5);
  color: var(--tv-gold);
}
.tv-rule::before,
.tv-rule::after {
  content: "";
  flex: 1;
  height: 1px;
  background-color: var(--tv-rule-gold);
}
.tv-rule .tv-icon { color: var(--tv-gold); }

.tv-rule--start::before { display: none; }
.tv-rule--short { max-width: 220px; }

/* Short gold tick under an eyebrow or heading. Purely decorative — the eyebrow
   text carries the meaning — so the light accent is fine here. */
.tv-tick {
  display: block;
  width: 44px;
  height: 2px;
  margin-bottom: var(--tv-space-5);
  background-color: var(--tv-gold);
}
.tv-section-head--center .tv-tick { margin-inline: auto; }

/* --------------------------------------------------------------------------
   4. MEDIA
   -------------------------------------------------------------------------- */

.tv-media {
  position: relative;
  overflow: hidden;
  border-radius: var(--tv-radius-image);
  background-color: var(--tv-surface-sunken);
}
.tv-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--tv-dur-slow) var(--tv-ease);
}

/* The organic top-arch crop. Used on feature and card imagery so the brand
   does not read as a generic rounded-card template. */
.tv-media--arch { border-radius: var(--tv-radius-arch); }

.tv-media--ratio-43 { aspect-ratio: 4 / 3; }
.tv-media--ratio-11 { aspect-ratio: 1 / 1; }
.tv-media--ratio-45 { aspect-ratio: 4 / 5; }
.tv-media--ratio-32 { aspect-ratio: 3 / 2; }

/* Offset copper frame behind a feature image. */
.tv-media--framed { isolation: isolate; }
.tv-media--framed::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--tv-rule-gold);
  border-radius: inherit;
  pointer-events: none;
}

/*
 * .tv-media-frame drew an offset copper outline behind the image via a
 * ::before. That is gone — the wrapper is now a plain block and carries no
 * styling at all, which is why there is no rule for it here.
 *
 * The <div class="tv-media-frame"> is still in the section markup (home/05,
 * about/02, service/02) and in the built page content. It is inert: an
 * unstyled div around the media. Removing it means editing those files and
 * rebuilding the pages, which is not worth doing for markup that costs
 * nothing. Restoring the outline means putting the rule back here, not
 * changing the markup.
 */

/* --------------------------------------------------------------------------
   5. CARDS
   -------------------------------------------------------------------------- */

.tv-card {
  display: flex;
  flex-direction: column;
  background-color: var(--tv-surface);
  border: 1px solid var(--tv-rule-soft);
  border-radius: var(--tv-radius-md);
  overflow: hidden;
  transition:
    transform  var(--tv-dur-base) var(--tv-ease),
    box-shadow var(--tv-dur-base) var(--tv-ease),
    border-color var(--tv-dur-base) var(--tv-ease);
}

.tv-card:hover {
  transform: translateY(-4px);
  border-color: var(--tv-rule);
  box-shadow: var(--tv-shadow-md);
}
.tv-card:hover .tv-media img { transform: scale(1.04); }

.tv-card__media { aspect-ratio: 4 / 3; }

.tv-card__body {
  display: flex;
  flex-direction: column;
  flex: 1;
  gap: var(--tv-space-3);
  padding: var(--tv-card-pad);
}

.tv-card__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  margin-bottom: var(--tv-space-2);
  border-radius: var(--tv-radius-pill);
  background-color: var(--tv-gold-wash);
  /* Deep gold, not the accent: the glyph carries meaning here, and the accent
     on this wash is about 2:1. */
  color: var(--tv-gold-deep);
}

.tv-card__title {
  margin: 0;
  font-family: var(--tv-font-body);
  font-size: var(--tv-fs-h4);
  font-weight: var(--tv-fw-semibold);
  line-height: var(--tv-lh-snug);
  color: var(--tv-heading);
}

.tv-card__text {
  margin: 0;
  font-size: var(--tv-fs-body);
  color: var(--tv-muted);
  max-width: none;
}

.tv-card__foot { margin-top: auto; padding-top: var(--tv-space-4); }

/* Whole-card link without nesting interactive elements. */
.tv-card__link::after {
  content: "";
  position: absolute;
  inset: 0;
}
.tv-card { position: relative; }

/* --------------------------------------------------------------------------
   6. CHECK LIST
   -------------------------------------------------------------------------- */

.tv-checklist { list-style: none; margin: 0; padding: 0; }

.tv-checklist li {
  display: flex;
  align-items: flex-start;
  gap: var(--tv-space-3);
  margin-bottom: var(--tv-space-4);
  font-size: var(--tv-fs-body);
}
.tv-checklist li:last-child { margin-bottom: 0; }

.tv-checklist .tv-icon {
  margin-top: 0.25em;
  padding: 4px;
  border-radius: var(--tv-radius-pill);
  background-color: var(--tv-gold-wash);
  color: var(--tv-gold-deep);
  box-sizing: content-box;
  width: 0.85em;
  height: 0.85em;
}

/* --------------------------------------------------------------------------
   7. FORMS  (Contact Form 7 lives inside .tv-form)
   -------------------------------------------------------------------------- */

/*
 * The gap has to be declared on CF7's <form>, not only on .tv-form.
 *
 * .tv-form is the wrapper the shortcode sits in, so its only in-flow child is
 * the .wpcf7 div CF7 renders — the rows are grandchildren, inside <form>, and
 * a grid gap does not reach them. The result is 24px of space around the form
 * and none at all between its rows, which is why "Email" and "What kind of
 * work?" sat hard against the inputs above them.
 *
 * Both hidden children of the form — the hidden-fields fieldset and the
 * response output — are display: none (see wp-overrides.css), so they create
 * no track and no stray gap.
 */
.tv-form,
.tv-form .wpcf7-form { display: grid; gap: var(--tv-space-5); }

.tv-form__row {
  display: grid;
  gap: var(--tv-space-5);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (max-width: 767px) {
  .tv-form__row { grid-template-columns: minmax(0, 1fr); }
}

.tv-field { display: grid; gap: var(--tv-space-2); }

.tv-field__label {
  font-size: var(--tv-fs-caption);
  font-weight: var(--tv-fw-medium);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--tv-muted);
}
/* The required marker is information, so it takes the readable gold. */
.tv-field__label .tv-req { color: var(--tv-gold-deep); }

.tv-form input[type="text"],
.tv-form input[type="email"],
.tv-form input[type="tel"],
.tv-form input[type="url"],
.tv-form input[type="date"],
.tv-form select,
.tv-form textarea {
  width: 100%;
  padding: var(--tv-space-3) var(--tv-space-4);
  background-color: var(--tv-surface);
  border: 1px solid var(--tv-rule);
  border-radius: var(--tv-radius-sm);
  color: var(--tv-text);
  font-size: var(--tv-fs-body);
  font-weight: var(--tv-fw-regular);
  line-height: 1.5;
  transition:
    border-color var(--tv-dur-fast) var(--tv-ease),
    box-shadow   var(--tv-dur-fast) var(--tv-ease);
}

/* height, not just min-height: CF7 emits rows="10" when the form tag does not
   say otherwise, which renders ~310px tall, and a min-height floor never
   brings that down. resize stays vertical so it can still be dragged open. */
.tv-form textarea { height: 140px; min-height: 140px; resize: vertical; }

.tv-form input:focus,
.tv-form select:focus,
.tv-form textarea:focus {
  outline: none;
  /* A focus boundary has to clear 3:1. The accent manages 2.16:1 on the ivory
     ground behind these fields, so focus takes the deep step. */
  border-color: var(--tv-gold-deep);
  box-shadow: 0 0 0 3px var(--tv-gold-wash);
}

/* opacity: 1 because Firefox dims placeholders by default, which would apply
   a second lightening on top of the token. */
.tv-form ::placeholder { color: var(--tv-placeholder); opacity: 1; }

.tv-form select {
  appearance: none;
  padding-right: var(--tv-space-7);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235D6B55' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--tv-space-4) center;
  background-size: 18px;
}

/* CF7's own wrappers, normalised. */
.wpcf7-form-control-wrap { display: block; }
.wpcf7 .wpcf7-not-valid-tip {
  margin-top: var(--tv-space-2);
  color: var(--tv-error);
  font-size: var(--tv-fs-caption);
}
.wpcf7 .wpcf7-not-valid { border-color: var(--tv-error) !important; }

.wpcf7 form .wpcf7-response-output {
  margin: var(--tv-space-4) 0 0;
  padding: var(--tv-space-4) var(--tv-space-5);
  border: 1px solid var(--tv-rule);
  border-left-width: 2px;
  border-radius: var(--tv-radius-sm);
  font-size: var(--tv-fs-body);
}
.wpcf7 form.invalid .wpcf7-response-output,
.wpcf7 form.unaccepted .wpcf7-response-output { border-left-color: var(--tv-error); }
.wpcf7 form.sent .wpcf7-response-output       { border-left-color: var(--tv-success); }

.wpcf7-spinner { margin-left: var(--tv-space-3); }

/* --------------------------------------------------------------------------
   8. PROSE  (blog post content)
   -------------------------------------------------------------------------- */

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

.tv-prose > * + * { margin-top: var(--tv-space-5); }

.tv-prose h2 {
  margin-top: var(--tv-space-8);
  font-family: var(--tv-font-display);
  font-weight: var(--tv-fw-display);
  font-size: var(--tv-fs-h2);
}
.tv-prose h3 { margin-top: var(--tv-space-7); }

.tv-prose img,
.tv-prose figure { border-radius: var(--tv-radius-md); }

.tv-prose figcaption {
  margin-top: var(--tv-space-3);
  font-size: var(--tv-fs-caption);
  color: var(--tv-muted);
}

.tv-prose ul,
.tv-prose ol { padding-left: var(--tv-space-5); }
.tv-prose li { margin-bottom: var(--tv-space-3); }

.tv-prose a { color: var(--tv-link); }

/* --------------------------------------------------------------------------
   9. HEADER
   -------------------------------------------------------------------------- */

.tv-header {
  position: fixed;
  inset: 0 0 auto;
  z-index: var(--tv-z-header);
  transition:
    background-color var(--tv-dur-base) var(--tv-ease),
    box-shadow       var(--tv-dur-base) var(--tv-ease),
    transform        var(--tv-dur-base) var(--tv-ease);
}

.tv-header__inner {
  display: flex;
  align-items: center;
  gap: var(--tv-space-6);
  height: var(--tv-header-h);
  /* Wider than a body section so the logo sits near the frame edge, and so
     the hero headline below it lines up with the logo. */
  max-width: var(--tv-container-wide);
  transition: height var(--tv-dur-base) var(--tv-ease);
}

/* Solid state — the default, and what the overlay collapses into on scroll. */
.tv-header,
.tv-header.is-scrolled {
  background-color: var(--tv-ivory);
  box-shadow: var(--tv-shadow-header);
}
.tv-header.is-scrolled .tv-header__inner { height: var(--tv-header-h-scrolled); }

/* Overlay state — transparent over a full-bleed hero, until scrolled. */
.tv-header--overlay:not(.is-scrolled) {
  background-color: transparent;
  box-shadow: none;
}

/*
 * Reversing the nav to ivory must apply to TOP-LEVEL items only. The dropdown
 * panel keeps its own ivory background whatever the header is doing, so a
 * blanket `.tv-nav__link` rule here paints the submenu ivory-on-ivory and the
 * links vanish. Hence the explicit child chain.
 */
.tv-header--overlay:not(.is-scrolled) .tv-nav__list > .tv-nav__item > .tv-nav__link,
.tv-header--overlay:not(.is-scrolled) .tv-nav__list > .tv-nav__item > .tv-nav__toggle,
.tv-header--overlay:not(.is-scrolled) .tv-phone,
.tv-header--overlay:not(.is-scrolled) .tv-nav-toggle { color: var(--tv-on-dark); }

.tv-header--overlay:not(.is-scrolled) .tv-nav__list > .tv-nav__item > .tv-nav__link:hover,
.tv-header--overlay:not(.is-scrolled) .tv-phone:hover { color: var(--tv-gold-light); }

.tv-logo { flex: none; display: block; }
.tv-logo__img {
  width: auto;
  height: 58px;
  transition: height var(--tv-dur-base) var(--tv-ease);
}
.tv-header.is-scrolled .tv-logo__img { height: 46px; }

/*
 * The logo has to shrink on a phone, and nothing here did.
 *
 * The mark is 1068x174, so it is 6.14 times as wide as it is tall and its width
 * is set entirely by its height. At 58px that is 356px of logo. A 390px phone
 * offers 390 minus 40px of gutter minus the 44px menu button, which is 306px,
 * so the header needed 440px to lay out and the menu button was pushed off the
 * screen entirely. There was no way to navigate the site on a phone.
 *
 * 40px keeps the whole header inside the narrowest common viewport: 245px of
 * logo plus the button plus gutters is 329px, inside 360px.
 */
@media (max-width: 560px) {
  .tv-logo__img,
  .tv-header.is-scrolled .tv-logo__img { height: 40px; }
}

@media (max-width: 380px) {
  .tv-logo__img,
  .tv-header.is-scrolled .tv-logo__img { height: 34px; }
}

/* Reversed logo swaps in while the overlay header is transparent.
 *
 * Scoped to .tv-header. Unscoped this also hid the footer's reversed logo,
 * which is the only logo the footer has — tv_logo('white') was rendering and
 * then being display:none'd, so the footer looked like it had no logo at all
 * rather than a broken one. */
.tv-header .tv-logo--white { display: none; }
.tv-header--overlay:not(.is-scrolled) .tv-logo--default { display: none; }
.tv-header--overlay:not(.is-scrolled) .tv-logo--white   { display: block; }

/* --- primary nav --- */

.tv-nav { margin-left: auto; }

.tv-nav__list {
  display: flex;
  align-items: center;
  gap: var(--tv-space-6);
  margin: 0;
  padding: 0;
  list-style: none;
}

.tv-nav__item { position: relative; margin: 0; }

.tv-nav__link {
  display: inline-block;
  padding: var(--tv-space-2) 0;
  color: var(--tv-heading);
  /* Uppercase and tracked, matching the mockup's navigation. */
  font-size: var(--tv-fs-body);
  font-weight: var(--tv-fw-semibold);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
}
.tv-nav__link::after {
  content: "";
  display: block;
  width: 0;
  height: 1.5px;
  margin-top: 3px;
  /* This underline marks the current page, so it is state rather than
     decoration and takes the readable step. */
  background-color: var(--tv-gold-deep);
  transition: width var(--tv-dur-fast) var(--tv-ease);
}
.tv-nav__link:hover::after,
.tv-nav__item.is-current > .tv-nav__link::after { width: 100%; }

/* Over the hero the header is transparent and the links are ivory, where the
   deep gold would disappear — it lifts to the light step there. */
.tv-header--overlay:not(.is-scrolled) .tv-nav__link::after {
  background-color: var(--tv-gold-light);
}

/*
 * The caret was a ~21px target — under the 44px minimum, so a click that
 * looked on-target often missed and the dropdown appeared not to respond.
 * (It was opening on hover regardless, which masked the real cause.)
 *
 * z-index keeps it above the link's animated underline pseudo-element, which
 * otherwise sits over the caret's lower edge and swallows those clicks.
 */
.tv-nav__toggle {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  /* Negative margin so the bigger hit area does not push the nav apart. */
  margin-inline: calc(var(--tv-space-3) * -1);
  color: inherit;
  vertical-align: middle;
}
.tv-nav__toggle .tv-icon {
  width: 0.9em;
  height: 0.9em;
  transition: transform var(--tv-dur-fast) var(--tv-ease);
}
.tv-nav__toggle[aria-expanded="true"] .tv-icon { transform: rotate(180deg); }

/*
 * The caret follows the panel, not the pointer's exact target.
 *
 * These are deliberately the same two conditions that open the dropdown above
 * (:hover and :focus-within on the parent item), so the arrow and the panel can
 * never disagree — hovering the link, the caret, or the gap between them turns
 * the arrow and opens the menu together, and leaving turns it back. The
 * transition on .tv-nav__toggle .tv-icon animates both directions already.
 *
 * Desktop only. In the drawer the submenu opens on aria-expanded alone, so a
 * hover rule there would turn the arrow without anything opening.
 */
.tv-nav .tv-nav__item--has-children:hover > .tv-nav__toggle .tv-icon,
.tv-nav .tv-nav__item--has-children:focus-within > .tv-nav__toggle .tv-icon {
  transform: rotate(180deg);
}

.tv-nav__submenu {
  position: absolute;
  top: calc(100% + var(--tv-space-3));
  left: 0;
  z-index: var(--tv-z-drawer);
  min-width: 250px;
  margin: 0;
  padding: var(--tv-space-3);
  list-style: none;
  background-color: var(--tv-ivory);
  border: 1px solid var(--tv-rule-soft);
  border-radius: var(--tv-radius-md);
  box-shadow: var(--tv-shadow-lg);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px);
  transition:
    opacity    var(--tv-dur-fast) var(--tv-ease),
    transform  var(--tv-dur-fast) var(--tv-ease),
    visibility var(--tv-dur-fast) var(--tv-ease);
}

/*
 * Hover bridge.
 *
 * The pointer has to travel from the nav link down into the panel. Across a
 * real gap the parent's :hover drops the moment the cursor leaves the link and
 * the menu closes before it can be reached — the classic dropdown failure.
 *
 * This was first solved with a transparent border-top on the panel, which was
 * wrong: the border box is what box-shadow and border-radius are drawn around,
 * so the shadow floated above the panel and the top corners lost their radius.
 * An invisible pseudo-element covers the gap instead, extending the hover hit
 * area without touching the panel's own box model.
 *
 * It inherits visibility from the panel, so while the menu is closed the bridge
 * is inert and cannot swallow clicks on whatever sits beneath it.
 */
.tv-nav__submenu::before {
  content: "";
  position: absolute;
  top: calc(var(--tv-space-3) * -1);
  left: 0;
  right: 0;
  height: var(--tv-space-3);
}

.tv-nav__item--has-children:hover > .tv-nav__submenu,
.tv-nav__item--has-children:focus-within > .tv-nav__submenu,
.tv-nav__toggle[aria-expanded="true"] + .tv-nav__submenu {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

/* An explicitly opened menu must stay open once the pointer leaves the item,
   otherwise clicking the caret and moving away closes it again. */
.tv-nav__toggle[aria-expanded="true"] + .tv-nav__submenu {
  pointer-events: auto;
}

.tv-nav__submenu .tv-nav__item { width: 100%; }

.tv-nav__submenu .tv-nav__link {
  display: block;
  width: 100%;
  padding: var(--tv-space-3) var(--tv-space-4);
  border-radius: var(--tv-radius-sm);
  color: var(--tv-text);
  white-space: normal;
  transition: background-color var(--tv-dur-fast) var(--tv-ease);
}
.tv-nav__submenu .tv-nav__link::after { display: none; }
.tv-nav__submenu .tv-nav__link:hover {
  background-color: var(--tv-gold-wash);
  color: var(--tv-gold-deep);
}

/* --- header actions --- */

.tv-header__actions {
  display: flex;
  align-items: center;
  gap: var(--tv-space-5);
  flex: none;
}

.tv-phone {
  display: inline-flex;
  align-items: center;
  gap: var(--tv-space-2);
  color: var(--tv-heading);
  font-size: var(--tv-fs-body);
  font-weight: var(--tv-fw-medium);
  text-decoration: none;
  white-space: nowrap;
}
.tv-phone .tv-icon { color: var(--tv-gold-deep); }

.tv-nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-right: calc(var(--tv-space-2) * -1);
  color: var(--tv-heading);
}

/* --- mobile drawer --- */

.tv-drawer {
  position: fixed;
  inset: 0;
  z-index: var(--tv-z-overlay);
  display: flex;
  flex-direction: column;
  padding: var(--tv-space-6) var(--tv-gutter) var(--tv-space-8);
  background-color: var(--tv-ivory);
  overflow-y: auto;
  opacity: 0;
  visibility: hidden;

  /*
   * translateY, NOT translateX. This was translateX(8%) and it broke the layout
   * of every page on every phone.
   *
   * The drawer is fixed with inset: 0, so it is exactly viewport-wide. Shifting
   * it right by 8% put its right edge 8% past the viewport, and a transformed
   * element still contributes to the document's scroll width even when hidden.
   * Worse, it feeds back: the wider document makes the viewport-sized drawer
   * wider, which pushes further still. A 390px phone settled at 497px, so every
   * page laid out at 497 and was clipped to 390 by body's overflow-x: hidden.
   * The menu button ended up off-screen, leaving no way to navigate at all.
   *
   * Vertical movement cannot create horizontal overflow, and reads the same at
   * this distance.
   */
  transform: translateY(-8px);
  transition:
    opacity    var(--tv-dur-base) var(--tv-ease),
    transform  var(--tv-dur-base) var(--tv-ease),
    visibility var(--tv-dur-base) var(--tv-ease);
}
.tv-drawer.is-open {
  opacity: 1;
  visibility: visible;
  transform: none;
}

.tv-drawer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--tv-space-4);
  margin-bottom: var(--tv-space-7);
}

.tv-drawer__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  color: var(--tv-heading);
}

.tv-drawer .tv-nav__list {
  flex-direction: column;
  align-items: stretch;
  gap: 0;
}
.tv-drawer .tv-nav__item {
  border-bottom: 1px solid var(--tv-rule-soft);
}
.tv-drawer .tv-nav__item--child { border-bottom: 0; }
.tv-drawer .tv-nav__link {
  padding: var(--tv-space-4) 0;
  font-family: var(--tv-font-display);
  font-size: var(--tv-fs-h3);
  font-weight: var(--tv-fw-display);
}
.tv-drawer .tv-nav__link::after { display: none; }

.tv-drawer .tv-nav__item--has-children > .tv-nav__link { display: inline-block; width: calc(100% - 44px); }
.tv-drawer .tv-nav__toggle {
  position: absolute;
  top: var(--tv-space-3);
  right: 0;
  width: 44px;
  height: 44px;
  justify-content: center;
}

.tv-drawer .tv-nav__submenu {
  position: static;
  min-width: 0;
  padding: 0 0 var(--tv-space-4) var(--tv-space-4);
  border: 0;
  border-left: 1px solid var(--tv-rule);
  border-radius: 0;
  box-shadow: none;
  opacity: 1;
  visibility: visible;
  transform: none;
  display: none;
}
/* No hover bridge in the drawer — the panel is static and always visible when
   open, so the pseudo-element would only float an invisible box over the link
   above it and swallow taps. */
.tv-drawer .tv-nav__submenu::before { display: none; }

.tv-drawer .tv-nav__toggle[aria-expanded="true"] + .tv-nav__submenu { display: block; }
.tv-drawer .tv-nav__submenu .tv-nav__link {
  padding: var(--tv-space-3) 0;
  font-family: var(--tv-font-body);
  font-size: var(--tv-fs-body);
  font-weight: var(--tv-fw-regular);
}

.tv-drawer__foot {
  margin-top: var(--tv-space-8);
  display: grid;
  gap: var(--tv-space-4);
}

@media (max-width: 1024px) {
  .tv-nav,
  .tv-header__actions .tv-phone { display: none; }
  .tv-nav-toggle { display: inline-flex; }
  .tv-header__actions { margin-left: auto; }
}
@media (max-width: 560px) {
  .tv-header__actions .tv-btn { display: none; }
}

/*
 * Touch target sizes.
 *
 * Measured on a real 390px viewport: the phone link in the drawer came out
 * 28px high and the hero's pause control 39px. Both are fine to click with a
 * mouse and awkward to hit with a thumb, so they get the same 44px floor the
 * nav toggle and buttons already use.
 */
@media (max-width: 1024px) {
  .tv-drawer .tv-phone {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  .tv-hero__motion { min-height: 44px; }

  /*
   * Footer links measured 17 to 23px high on a phone. WCAG 2.5.8 exempts links
   * set in a sentence, but these are a stacked column of navigation, which is
   * exactly the case the exemption is not meant to cover. Padding rather than
   * min-height, so the target grows without spacing the list out.
   */
  .tv-footer__list a,
  .tv-footer__contact a,
  .tv-footer__legal a {
    display: inline-block;
    padding-block: var(--tv-space-2);
  }
}

/* Non-overlay pages need the fixed header's height reserved. */
body:not(.tv-header-overlay) .tv-main { padding-top: var(--tv-header-h); }

/* --------------------------------------------------------------------------
   10. FOOTER
   -------------------------------------------------------------------------- */

.tv-footer {
  background-color: var(--tv-forest);
  color: var(--tv-on-dark-muted);
  border-top: 2px solid var(--tv-gold);
}
.tv-footer a { color: var(--tv-on-dark-muted); text-decoration: none; }
.tv-footer a:hover { color: var(--tv-on-dark); }

/* Same wide box as the header, so the footer logo starts at the same left
   edge as the one in the top bar. */
.tv-footer__container { max-width: var(--tv-container-wide); }

/*
 * Brand · Get in Touch · Services · Company · Hours
 *
 * Column gap is tighter than the row gap: at a uniform 48px the link columns
 * read as four unrelated islands rather than one block of navigation. Rows
 * still need the larger gap when this wraps on smaller screens.
 */
.tv-footer__top {
  display: grid;
  column-gap: var(--tv-space-5);
  row-gap: var(--tv-space-7);
  padding-block: var(--tv-space-9) var(--tv-space-8);
  grid-template-columns: 1.35fr 1.05fr 0.95fr 0.85fr 0.8fr;
}
@media (max-width: 1024px) { .tv-footer__top { grid-template-columns: 1fr 1fr; column-gap: var(--tv-space-7); } }
@media (max-width: 560px)  { .tv-footer__top { grid-template-columns: 1fr; } }

/*
 * Two links per row inside each footer column on a phone.
 *
 * Once .tv-footer__top drops to a single column the columns themselves are full
 * width, so a one-per-row list wastes half the line and runs the footer to
 * fourteen rows of short links. Pairing them halves that without shrinking
 * anything.
 *
 * Row gap stays tighter than the column gap: these are 44px tap targets already
 * (the links carry vertical padding), so the rows do not need more air between
 * them, but the columns do need enough to read as two separate lists.
 */
@media (max-width: 560px) {
  .tv-footer__list {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--tv-space-4);
  }
  /* The margin was doing the spacing when this was a plain list; the grid's own
     row gap does it now, and leaving both would double it. */
  .tv-footer__list li { margin-bottom: 0; }

  /*
   * Get in Touch deliberately stays one per row.
   *
   * Each entry is an icon, a gap and a value that cannot be broken sensibly.
   * Measured at 390px: the email needs 206px all in (20 icon + 12 gap + 174
   * text) against the 167px half a phone column offers. Forcing the pair made
   * the item overflow its cell by 39px and pushed the whole document to 409px
   * wide on a 390px screen — a horizontal scrollbar on every page.
   *
   * Two across would need roughly a 468px viewport, which is a small tablet
   * rather than a phone. The link lists above are a different case: their
   * labels are ordinary words and wrap across lines cleanly.
   */
}

.tv-footer__title {
  margin: 0 0 var(--tv-space-5);
  font-size: var(--tv-fs-caption);
  font-weight: var(--tv-fw-semibold);
  letter-spacing: var(--tv-ls-eyebrow);
  text-transform: uppercase;
  color: var(--tv-on-dark);
}

/* A second heading within one column — "Get in Touch" under the services
   list. Replaces the inline margin-top that used to sit on the Hours title. */
.tv-footer__title--stacked { margin-top: var(--tv-space-7); }

.tv-footer__list { list-style: none; margin: 0; padding: 0; }
.tv-footer__list li { margin-bottom: var(--tv-space-3); font-size: var(--tv-fs-body); }

/*
 * The footer now shows the same full-colour mark as the header rather than a
 * white knockout, so the two match.
 *
 * That mark is a dark-green wordmark, which would be invisible against the
 * forest ground — so it sits on an ivory plate. This keeps the exact brand
 * artwork, copper swoosh included, instead of flattening it to a silhouette.
 */
.tv-footer__brand .tv-logo {
  display: inline-block;
  margin-bottom: var(--tv-space-5);
  padding: var(--tv-space-4) var(--tv-space-5);
  background-color: var(--tv-ivory);
  border-radius: var(--tv-radius-md);
}

.tv-footer .tv-logo__img { height: 52px; }

/* Same arithmetic as the header, plus the ivory plate's own padding: 52px of
   logo is 319px wide, and with the plate that is 367px inside a 320px column. */
@media (max-width: 560px) {
  .tv-footer .tv-logo__img { height: 36px; }
  .tv-footer__brand .tv-logo { padding: var(--tv-space-3) var(--tv-space-4); }
}

.tv-footer__contact {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--tv-space-4);
  font-size: var(--tv-fs-body);
}
.tv-footer__contact li {
  display: flex;
  align-items: flex-start;
  gap: var(--tv-space-3);
  margin: 0;
}
.tv-footer__contact .tv-icon { color: var(--tv-gold-light); margin-top: 0.2em; }

.tv-hours { list-style: none; margin: 0; padding: 0; font-size: var(--tv-fs-body); }
.tv-hours li {
  display: flex;
  justify-content: space-between;
  gap: var(--tv-space-4);
  padding-block: var(--tv-space-2);
  border-bottom: 1px solid var(--tv-rule-on-dark);
}
.tv-hours li:last-child { border-bottom: 0; }
.tv-hours__time { color: var(--tv-on-dark); white-space: nowrap; }

.tv-social { display: flex; gap: var(--tv-space-3); margin-top: var(--tv-space-5); }
.tv-social__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--tv-rule-on-dark);
  border-radius: var(--tv-radius-pill);
  transition:
    background-color var(--tv-dur-fast) var(--tv-ease),
    border-color     var(--tv-dur-fast) var(--tv-ease),
    color            var(--tv-dur-fast) var(--tv-ease);
}
/* Gold fill on the forest footer, so the glyph goes dark rather than white —
   white on gold is 2.16:1, forest-deep on it is 5.6:1. */
.tv-social__link:hover {
  background-color: var(--tv-gold);
  border-color: var(--tv-gold);
  color: var(--tv-forest-deep);
}
.tv-social__link .tv-icon { width: 1.05em; height: 1.05em; }

.tv-footer__bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--tv-space-4);
  padding-block: var(--tv-space-5);
  border-top: 1px solid var(--tv-rule-on-dark);
  font-size: var(--tv-fs-caption);
}
.tv-footer__legal { display: flex; flex-wrap: wrap; gap: var(--tv-space-5); }

/* --------------------------------------------------------------------------
   11. BREADCRUMBS
   -------------------------------------------------------------------------- */

.tv-breadcrumbs ol {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--tv-space-2);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--tv-fs-caption);
}
.tv-breadcrumbs li { margin: 0; display: flex; align-items: center; gap: var(--tv-space-2); }
.tv-breadcrumbs li + li::before {
  content: "";
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background-color: currentColor;
  opacity: 0.45;
}
/* 21px of link is under the 24px WCAG 2.5.8 floor. Padding grows the hit area;
   the matching negative margin keeps the breadcrumb row the same height, so
   nothing below it shifts. */
.tv-breadcrumbs a {
  display: inline-block;
  padding-block: var(--tv-space-2);
  margin-block: calc(var(--tv-space-2) * -1);
  text-decoration: none;
}
.tv-breadcrumbs [aria-current="page"] { opacity: 0.75; }

/* --------------------------------------------------------------------------
   12. PAGINATION
   -------------------------------------------------------------------------- */

.tv-pagination {
  display: flex;
  justify-content: center;
  gap: var(--tv-space-2);
  margin-top: var(--tv-space-8);
}
.tv-pagination .page-numbers {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  height: 44px;
  padding-inline: var(--tv-space-3);
  border: 1px solid var(--tv-rule);
  border-radius: var(--tv-radius-sm);
  color: var(--tv-text);
  font-size: var(--tv-fs-body);
  text-decoration: none;
  transition:
    background-color var(--tv-dur-fast) var(--tv-ease),
    border-color     var(--tv-dur-fast) var(--tv-ease),
    color            var(--tv-dur-fast) var(--tv-ease);
}
.tv-pagination .page-numbers:hover,
.tv-pagination .page-numbers.current {
  background-color: var(--tv-forest);
  border-color: var(--tv-forest);
  color: var(--tv-on-dark);
}
.tv-pagination .page-numbers.dots {
  border-color: transparent;
  background: none;
  color: var(--tv-muted);
}

/* --------------------------------------------------------------------------
   13. STAR RATING
   -------------------------------------------------------------------------- */

.tv-stars { display: inline-flex; gap: 2px; color: var(--tv-gold); }
.tv-stars .tv-icon { width: 1em; height: 1em; }

/* --------------------------------------------------------------------------
   14. (removed) PLACEHOLDER BADGE
   The local-only front-end badge was dropped — it obstructed design review.
   Placeholder tracking now lives in the admin notice and site.config.json.
   See inc/config.php.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   15. ACCORDION  (data-tv-accordion)
   -------------------------------------------------------------------------- */

.tv-accordion { border-top: 1px solid var(--tv-rule); }

.tv-accordion__item { border-bottom: 1px solid var(--tv-rule); }

.tv-accordion__trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--tv-space-5);
  width: 100%;
  padding: var(--tv-space-5) 0;
  text-align: left;
  font-family: var(--tv-font-body);
  font-size: var(--tv-fs-h4);
  font-weight: var(--tv-fw-medium);
  line-height: var(--tv-lh-snug);
  color: var(--tv-heading);
  transition: color var(--tv-dur-fast) var(--tv-ease);
}
.tv-accordion__trigger:hover { color: var(--tv-gold-deep); }

.tv-accordion__mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex: none;
  border: 1px solid var(--tv-rule);
  border-radius: var(--tv-radius-pill);
  color: var(--tv-gold-deep);
  transition:
    background-color var(--tv-dur-fast) var(--tv-ease),
    border-color     var(--tv-dur-fast) var(--tv-ease),
    transform        var(--tv-dur-base) var(--tv-ease);
}
.tv-accordion__mark .tv-icon { width: 0.9em; height: 0.9em; }

.tv-accordion__trigger[aria-expanded="true"] .tv-accordion__mark {
  background-color: var(--tv-gold);
  border-color: var(--tv-gold-deep);
  color: var(--tv-forest-deep);
  transform: rotate(180deg);
}

.tv-accordion__panel {
  height: 0;
  overflow: hidden;
  transition: height var(--tv-dur-base) var(--tv-ease);
}
.tv-accordion__panel > div { padding: 0 0 var(--tv-space-5); }
.tv-accordion__panel p { color: var(--tv-muted); }

/* --------------------------------------------------------------------------
   16. SLIDER  (data-tv-slider)
   -------------------------------------------------------------------------- */

.tv-slider { position: relative; }

.tv-slider__viewport { overflow: hidden; }

.tv-slider__track {
  display: flex;
  transition: transform var(--tv-dur-slow) var(--tv-ease);
}

.tv-slide {
  flex: 0 0 100%;
  min-width: 0;
  padding-inline: var(--tv-space-1);
}

.tv-slider__controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--tv-space-5);
  margin-top: var(--tv-space-6);
}

.tv-slider__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 1px solid var(--tv-rule);
  border-radius: var(--tv-radius-pill);
  color: var(--tv-heading);
  transition:
    background-color var(--tv-dur-fast) var(--tv-ease),
    border-color     var(--tv-dur-fast) var(--tv-ease),
    color            var(--tv-dur-fast) var(--tv-ease);
}
.tv-slider__btn:hover {
  background-color: var(--tv-gold);
  border-color: var(--tv-gold-deep);
  color: var(--tv-forest-deep);
}

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

.tv-slider__dot {
  width: 9px;
  height: 9px;
  padding: 0;
  border-radius: var(--tv-radius-pill);
  background-color: var(--tv-olive-mist);
  transition:
    background-color var(--tv-dur-fast) var(--tv-ease),
    width            var(--tv-dur-fast) var(--tv-ease);
}
/* Which slide you are on is state, not decoration, so the active dot takes the
   deep step — the accent against olive mist is too close to read as a change. */
.tv-slider__dot[aria-current="true"] {
  width: 26px;
  background-color: var(--tv-gold-deep);
}

/* On forest grounds the controls invert. */
.tv-ground--forest .tv-slider__btn { border-color: var(--tv-rule-on-dark); color: var(--tv-on-dark); }
.tv-ground--forest .tv-slider__dot { background-color: var(--tv-on-dark-dot); }
.tv-ground--forest .tv-slider__dot[aria-current="true"] { background-color: var(--tv-on-dark-accent); }

/* --------------------------------------------------------------------------
   17. BEFORE / AFTER  (data-tv-compare)
   -------------------------------------------------------------------------- */

.tv-compare {
  position: relative;
  overflow: hidden;
  border-radius: var(--tv-radius-lg);
  cursor: ew-resize;
  user-select: none;
  touch-action: pan-y;
}

.tv-compare img {
  width: 100%;
  display: block;
  pointer-events: none;
}

.tv-compare__after {
  position: absolute;
  inset: 0;
}

/*
 * The extra .tv-compare in this selector is load-bearing, not tidiness.
 *
 * Elementor ships `.elementor img { height: auto }` at (0,1,1) — the same
 * specificity as a plain `.tv-compare__after img`, and its stylesheet loads
 * after this one, so it won the tie and the after image fell back to its
 * intrinsic ratio instead of filling the frame. At (0,2,1) this wins outright.
 *
 * The failure only showed on narrow screens: the frame is 8/5 on desktop, which
 * is also the images' own ratio, so the collapsed height happened to equal the
 * correct one. At the 4/3 mobile frame the after image came out 219px inside a
 * 263px box and the before image showed through the 43px strip underneath it.
 *
 * The before image was never affected because its rule is already (0,2,1).
 */
.tv-compare .tv-compare__after img {
  height: 100%;
  object-fit: cover;
}

.tv-compare__handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 44px;
  margin-left: -22px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: ew-resize;
}
.tv-compare__handle::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 2px;
  margin-left: -1px;
  background-color: var(--tv-ivory);
}
.tv-compare__grip {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  width: 44px;
  height: 44px;
  border-radius: var(--tv-radius-pill);
  background-color: var(--tv-ivory);
  color: var(--tv-forest);
  box-shadow: var(--tv-shadow-md);
}
.tv-compare__grip .tv-icon { width: 0.8em; height: 0.8em; }

.tv-compare__label {
  position: absolute;
  bottom: var(--tv-space-4);
  padding: var(--tv-space-2) var(--tv-space-4);
  border-radius: var(--tv-radius-pill);
  background-color: var(--tv-scrim-75);
  color: var(--tv-on-dark);
  font-size: var(--tv-fs-caption);
  font-weight: var(--tv-fw-medium);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  pointer-events: none;
}
.tv-compare__label--before { left: var(--tv-space-4); }
.tv-compare__label--after  { right: var(--tv-space-4); }

/* --------------------------------------------------------------------------
   18. LIGHTBOX  (built by site.js)
   -------------------------------------------------------------------------- */

.tv-lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--tv-z-overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--tv-space-4);
  padding: var(--tv-space-6);
  background-color: var(--tv-scrim-94);
  opacity: 0;
  visibility: hidden;
  transition:
    opacity    var(--tv-dur-base) var(--tv-ease),
    visibility var(--tv-dur-base) var(--tv-ease);
}
.tv-lightbox.is-open { opacity: 1; visibility: visible; }

.tv-lightbox__figure {
  margin: 0;
  max-width: min(1100px, 92vw);
  text-align: center;
}
.tv-lightbox__figure img {
  max-height: 82vh;
  width: auto;
  margin-inline: auto;
  border-radius: var(--tv-radius-md);
}
.tv-lightbox__figure figcaption {
  margin-top: var(--tv-space-4);
  color: var(--tv-on-dark-muted);
  font-size: var(--tv-fs-body);
}

.tv-lightbox__close,
.tv-lightbox__nav {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  flex: none;
  border: 1px solid var(--tv-rule-on-dark);
  border-radius: var(--tv-radius-pill);
  color: var(--tv-on-dark);
  transition:
    background-color var(--tv-dur-fast) var(--tv-ease),
    border-color     var(--tv-dur-fast) var(--tv-ease),
    color            var(--tv-dur-fast) var(--tv-ease);
}
.tv-lightbox__close:hover,
.tv-lightbox__nav:hover {
  background-color: var(--tv-gold);
  border-color: var(--tv-gold);
  color: var(--tv-forest-deep);
}

.tv-lightbox__close {
  position: absolute;
  top: var(--tv-space-5);
  right: var(--tv-space-5);
}

@media (max-width: 767px) {
  .tv-lightbox { padding: var(--tv-space-4); }
  .tv-lightbox__nav {
    position: absolute;
    bottom: var(--tv-space-5);
  }
  .tv-lightbox__nav--prev { left: var(--tv-space-5); }
  .tv-lightbox__nav--next { right: var(--tv-space-5); }
}

/* --------------------------------------------------------------------------
   19. PAGE HEADER
   The dark opening band on templates that have no Elementor hero — blog,
   archives, search, 404, plain pages. It is what the transparent header
   sits on, so its top padding must clear the fixed header.
   -------------------------------------------------------------------------- */

.tv-page-header {
  position: relative;
  /* .tv-main reserves the header height now that it no longer overlays. */
  padding-top: var(--tv-space-8);
  padding-bottom: var(--tv-space-8);
  overflow: hidden;
}

/* Faint sage wash in the corner so the band is not a flat rectangle. */
.tv-page-header::after {
  content: "";
  position: absolute;
  right: -8%;
  bottom: -60%;
  width: 46%;
  padding-bottom: 46%;
  border-radius: 50%;
  background: radial-gradient(circle, var(--tv-glow-sage), transparent 68%);
  pointer-events: none;
}

.tv-page-header > .tv-container { position: relative; z-index: 1; }

.tv-page-header .tv-breadcrumbs {
  margin-bottom: var(--tv-space-5);
  color: var(--tv-on-dark-muted);
}
.tv-page-header .tv-breadcrumbs a:hover { color: var(--tv-on-dark-accent); }

.tv-page-header .tv-display { margin-bottom: var(--tv-space-4); max-width: 20ch; }
.tv-page-header .tv-lead { color: var(--tv-on-dark-muted); max-width: 56ch; }

/* --------------------------------------------------------------------------
   20. BLOG
   -------------------------------------------------------------------------- */

.tv-post-card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--tv-space-3);
  margin: 0;
  font-size: var(--tv-fs-caption);
  color: var(--tv-muted);
  max-width: none;
}

.tv-post-card__cat {
  padding: 3px var(--tv-space-3);
  border-radius: var(--tv-radius-pill);
  background-color: var(--tv-gold-wash);
  color: var(--tv-gold-deep);
  font-weight: var(--tv-fw-medium);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: var(--tv-fs-caption);
}

.tv-post__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--tv-space-3);
  margin: 0;
  color: var(--tv-on-dark-muted);
  font-size: var(--tv-fs-body);
}

.tv-post__hero {
  margin: 0 0 var(--tv-space-8);
  aspect-ratio: 20 / 9;
}

.tv-post__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tv-space-2);
  margin-top: var(--tv-space-7);
  padding-top: var(--tv-space-5);
  border-top: 1px solid var(--tv-rule);
}
.tv-post__tags a {
  padding: var(--tv-space-2) var(--tv-space-4);
  border: 1px solid var(--tv-rule);
  border-radius: var(--tv-radius-pill);
  font-size: var(--tv-fs-caption);
  text-decoration: none;
  color: var(--tv-muted);
  transition:
    border-color var(--tv-dur-fast) var(--tv-ease),
    color        var(--tv-dur-fast) var(--tv-ease);
}
.tv-post__tags a:hover { border-color: var(--tv-gold); color: var(--tv-gold-deep); }

/* --------------------------------------------------------------------------
   21. EMPTY STATE & SEARCH FORM
   -------------------------------------------------------------------------- */

.tv-empty {
  max-width: 56ch;
  margin-inline: auto;
  padding-block: var(--tv-space-7);
  text-align: center;
}
.tv-empty p { margin-inline: auto; }
.tv-empty .tv-btn-group { justify-content: center; margin-top: var(--tv-space-6); }

.tv-search-form {
  display: flex;
  gap: var(--tv-space-3);
  max-width: 520px;
  margin-top: var(--tv-space-6);
}
.tv-search-form input[type="search"] {
  flex: 1;
  min-width: 0;
  padding: var(--tv-space-3) var(--tv-space-4);
  background-color: var(--tv-on-dark-field);
  border: 1px solid var(--tv-rule-on-dark);
  border-radius: var(--tv-radius-sm);
  color: var(--tv-on-dark);
  font-size: var(--tv-fs-body);
}
.tv-search-form input[type="search"]::placeholder { color: var(--tv-on-dark-muted); opacity: 1; }
.tv-search-form input[type="search"]:focus {
  outline: none;
  border-color: var(--tv-on-dark-accent);
}

@media (max-width: 560px) {
  .tv-search-form { flex-direction: column; }
}

/* --------------------------------------------------------------------------
   22. PAGE HERO  (Elementor pages)
   The opening band on every inner page. Promoted out of the section files
   because it appears on fourteen of them — leaving it inline would mean
   fourteen copies of the same scrim to keep in sync.
   -------------------------------------------------------------------------- */

.tv-page-hero {
  position: relative;
  /* ONE HEIGHT FOR EVERY INNER PAGE, matching the home hero's band.

     These carried no min-height at all and were sized purely by their own
     copy, so every page opened at a different depth: contact 443px, about
     533px, projects 564px, service detail 603px, services 692px. The formula
     below is the one .tv-hero uses, so an inner page and the homepage present
     the same band.

     Padding drops from space-9 to space-8 at the same time. At 80px top and
     bottom the wordier heroes cleared the floor on their own and the
     min-height never engaged; 64px lets them settle onto it.

     Content still wins if it genuinely needs more room — min-height is a
     floor, not a cap, and clipping a hero headline to hit a number would be
     the wrong trade. */
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: min(62vh, 600px);
  padding-top: var(--tv-space-8);
  padding-bottom: var(--tv-space-8);
  background-color: var(--tv-forest-deep);
  color: var(--tv-on-dark);
  overflow: hidden;
  isolation: isolate;
}

.tv-page-hero__bg { position: absolute; inset: 0; z-index: -2; }
.tv-page-hero__bg img { width: 100%; height: 100%; object-fit: cover; }

.tv-page-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(105deg, var(--tv-scrim-90), var(--tv-scrim-75) 52%, var(--tv-scrim-55));
}

.tv-page-hero__inner { position: relative; }

.tv-page-hero .tv-breadcrumbs {
  margin-bottom: var(--tv-space-5);
  color: var(--tv-on-dark-muted);
}
.tv-page-hero .tv-breadcrumbs a:hover { color: var(--tv-on-dark-accent); }

.tv-page-hero .tv-eyebrow { color: var(--tv-on-dark-accent); }

.tv-page-hero__title {
  max-width: 18ch;
  margin: 0 0 var(--tv-space-4);
  font-family: var(--tv-font-display);
  font-weight: var(--tv-fw-display-bold);
  font-size: var(--tv-fs-display);
  line-height: var(--tv-lh-display);
  letter-spacing: var(--tv-ls-display);
  color: var(--tv-on-dark);
}

.tv-page-hero__text {
  max-width: 54ch;
  margin: 0;
  font-size: var(--tv-fs-lead);
  font-weight: var(--tv-fw-regular);
  color: var(--tv-on-dark-muted);
}

@media (max-width: 1024px) {
  /* Same step .tv-hero takes at this width. */
  .tv-page-hero { min-height: min(70vh, 640px); }
}

@media (max-width: 767px) {
  .tv-page-hero {
    min-height: min(88vh, 620px);
    padding-top: calc(var(--tv-header-h) + var(--tv-space-7));
    padding-bottom: var(--tv-space-7);
  }
  .tv-page-hero__title { max-width: none; }
}

/* --------------------------------------------------------------------------
   23. SPLIT LAYOUT
   Image one side, copy the other, alternating down the page. Used by the
   about story, every service overview, and the services hub.
   -------------------------------------------------------------------------- */

/*
 * The media column takes more of the row than the copy, and the image is
 * centred within it.
 *
 * At an even 1fr 1fr with a 4:3 crop the picture read small, because the arch
 * removes the top corners and the eye measures the visible image rather than
 * the box it sits in. Widening the column, deepening the crop to 5:4 and
 * tightening the gutter gives back the weight the arch takes away.
 */
.tv-split {
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  gap: clamp(var(--tv-space-6), 5vw, var(--tv-space-9));
  align-items: center;
}

/* Reverses the visual order without touching the DOM order, so the reading
   sequence for a screen reader stays copy-then-image throughout. */
.tv-split--reverse .tv-split__media { order: 2; }

.tv-split__media {
  justify-self: center;
  width: 100%;
}

.tv-split__media .tv-media { aspect-ratio: 5 / 4; }
.tv-split__body h2 { max-width: 20ch; }

.tv-split + .tv-split { margin-top: clamp(var(--tv-space-8), 8vw, var(--tv-space-10)); }

@media (max-width: 1024px) {
  .tv-split { grid-template-columns: minmax(0, 1fr); gap: var(--tv-space-7); }
  .tv-split--reverse .tv-split__media { order: 0; }
  .tv-split__body h2 { max-width: none; }
}

/* --------------------------------------------------------------------------
   24. LOCAL BREADCRUMBS INSIDE SECTION MARKUP
   Section files are static HTML and cannot call tv_breadcrumbs(), so they
   hand-author the same structure. This keeps both renderings identical.
   -------------------------------------------------------------------------- */

.tv-page-hero .tv-breadcrumbs ol { justify-content: flex-start; }

/* --------------------------------------------------------------------------
   25. BRAND PROMISE STATEMENT  (home / 03)
   "Rooted in nature. Built with purpose." is the one statement on the page
   that has to carry, so it steps up from the H2 size to roughly the H1 step
   and the lead under it grows with it. Scoped to the section so the rest of
   the site keeps the standard section-head scale.
   -------------------------------------------------------------------------- */

/* ---- fitting the statement onto the first screen -------------------------
   The headline has to be readable while the hero is still in view, which is a
   vertical budget problem: on a 1366x768 laptop the hero ran ~496px and the
   section's own var(--tv-section-y) added ~109px more, putting the headline
   at ~605px of ~640px usable — off the bottom in practice.

   Two levers, both applied from tablet up so the phone layout is untouched.
   The hero's min-height is not one of them: its content is taller than the
   min-height at these sizes, so lowering that alone changes nothing. The
   padding is what actually binds.                                          */

@media (min-width: 768px) {
  /* The section file sets these on .tv-hero / .tv-promise from an inline
     <style> in the post content, which renders after this sheet — an
     equal-specificity rule would lose the cascade. The element selector wins
     without needing !important. */
  section.tv-hero {
    padding-top: var(--tv-space-6);        /* 64 -> 32 */

    /* 40px taller at the bottom. Both values move, because either one can be
       the binding constraint depending on the screen: on a short viewport the
       content plus padding is what sets the height, on a tall one the
       min-height is. Raising only the padding would grow the hero by 40px on
       a laptop and by about 4px on a desktop. */
    padding-bottom: calc(var(--tv-space-6) + 40px);   /* 32 -> 72 */
    min-height: min(52vh + 40px, 520px);
  }

  section.tv-promise { padding-top: var(--tv-space-7); }   /* ~109 -> 48 */
}

/* ---- hero sized so it and the statement together make one screen ---------
   Rather than a guessed height, the hero takes whatever is left of the
   viewport once the three fixed costs above and below it are paid:

     100vh
     - var(--tv-header-h)   104px  fixed header, reserved by .tv-main
     - var(--tv-space-7)     48px  the promise section's padding-top
     - 182px                       the statement itself, plus ~8px of slack:
                                   60px headline x 1.2 = 72, its 16px margin,
                                   and two 26px lead lines at 1.65 = 86

   So the lead's last line lands just above the fold and the pillars begin
   below it. Those terms are what to edit if the block changes shape — a
   third line of lead, or a different headline size, moves the 182.

   Held to >=1024px on purpose. Narrower than that the lead takes a third line
   and the arithmetic stops holding, so those widths keep the flat min-height
   above. On a viewport under ~700px tall the hero's own content is taller
   than this calc and wins, and the statement drops below the fold — there is
   no height left to give it.                                                */

@media (min-width: 1024px) {
  section.tv-hero {
    min-height: calc(100vh - var(--tv-header-h) - var(--tv-space-7) - 182px);
  }
}

/* ---- hero: video only, no still ------------------------------------------
   No placeholder frame of any kind. site.js strips the poster off the element
   before it can paint, and nothing is painted on the wrapper behind it, so the
   video is the entire background. Until it starts, the hero is the flat ivory
   of .tv-hero's own background-color — which is what the fade below starts
   from, and what the headline was already designed to sit on.

   The trade: a hero that shows nothing but ivory whenever the video does not
   play — a failed request, or scripting off, since the sources only exist in a
   data attribute. That is the point of the change, not an oversight. Restoring
   a still means putting the poster back in site.js, not adding one here.    */

.tv-hero__bg video {
  opacity: 0;
  transition: opacity 600ms var(--tv-ease);
}

.tv-hero__bg video.is-playing { opacity: 1; }

/* No mark in the hero. Hidden in CSS rather than removed in script because
   this sheet applies before first paint — stripping it from the DOM on
   DOMContentLoaded would let it flash first. 01-hero.html no longer emits it,
   so this rule only has to cover the already-built page content and goes
   inert once that is rebuilt. */
.tv-hero .tv-hero__leaf { display: none; }

/* ---- hero: strapline up, button down -------------------------------------
   "Design. Build. Nurture." carries more weight than the CTA under it, so it
   takes the H3 step and the button drops off --lg back to the standard button
   metrics. The section file styles both from an inline <style> that renders
   after this sheet, hence the extra class on each selector.                 */

.tv-hero .tv-hero__sub {
  /* A tagline under the H1, not a second heading. The deck's p.8 mockup sets
     "Design. Build. Nurture." small; at 26px against a 56px title it competed
     with the headline instead of sitting under it. */
  font-size: var(--tv-fs-lead);          /* 18 */
}

.tv-hero__actions .tv-btn--lg {
  min-height: 52px;                      /* was 62; still clears the 44px
                                            WCAG target floor */
  padding: var(--tv-space-4) var(--tv-space-7);
  font-size: var(--tv-fs-body);
}

/* The head block caps at 62ch site-wide, which would re-wrap the bigger type
   long before the container edge. Let this one run to the container. */
.tv-promise .tv-section-head--center { max-width: none; }

.tv-promise .tv-promise__title {
  /* The display step rather than a hand-rolled clamp. This was 40 → 64, which
     outran the guideline's 56px H1 once the scale was synced. */
  font-size: var(--tv-fs-display);
  max-width: 18ch;
}

/* Headline on a single line from tablet up. The size is tied to the viewport
   rather than clamped flat so the line always fits the container instead of
   overflowing it: the 37 characters run about 16.5em in Cormorant 600, so at
   5vw they take ~83vw of the ~94vw the gutters leave. Below 700px one line
   would drop the type
   under 35px, so it goes back to wrapping. white-space is what forces the
   line — the base h1–h6 rule sets text-wrap: balance, restated here as
   nowrap so the two properties agree. */
@media (min-width: 700px) {
  .tv-promise .tv-promise__title {
    max-width: none;
    white-space: nowrap;
    text-wrap: nowrap;
    /* The display step, floored by 5vw so the line still shrinks to fit rather
       than overflowing on a narrow desktop. Was a hand-rolled 60px cap, which
       sat above the guideline's 56px H1. */
    font-size: min(var(--tv-fs-display), 5vw);
  }
}

/* text-wrap: balance picks the break here by minimising the longest line. The
   max-width only has to be loose enough to keep this to two lines — balance
   does the shaping, so don't tighten it to force a break.

   Was a hand-rolled 20 → 26, an H3 size on a paragraph. Against a 16px body
   that read as a second headline rather than a lead. */
.tv-promise .tv-section-head .tv-lead {
  font-size: var(--tv-fs-lead);
  max-width: 62ch;
  text-wrap: balance;
}

/* ---- pillars above the video, video at 80% of the viewport ---------------
   The three service pillars run before the video. The source section file has
   been reordered to match, so once the page content is rebuilt the DOM and
   the visual order agree; until then `order` does it on the live page. The
   values are stated for all three children so the result is the same either
   way round.                                                                */

.tv-promise .tv-container { display: flex; flex-direction: column; }

.tv-promise .tv-section-head { order: 1; }

.tv-promise .tv-promise__pillars {
  order: 2;
  margin-bottom: var(--tv-space-9);
  /* 48 was set for three bare marks floating on the band. Once each has an
     edge of its own they need to sit closer to read as one set. */
  gap: var(--tv-space-5);
}

/* ---- pillars as bordered tiles -------------------------------------------
   The icon-over-label pairs were three small marks on an empty ivory band
   with nothing holding them. Giving each an edge is the same treatment
   .tv-card already uses — white surface, rule-soft hairline, radius-md — so
   they read as part of that system rather than as a one-off box here.

   No lift on hover, which is where this parts company with .tv-card: the icon
   inside already nudges up 3px, and animating the tile as well reads as two
   separate things moving. The border and the existing colour shift carry the
   hover state on their own.                                                */

.tv-promise .tv-pillar__link {
  padding: var(--tv-space-6) var(--tv-space-5);
  background-color: var(--tv-surface);
  border: 1px solid var(--tv-rule-soft);
  border-radius: var(--tv-radius-md);
  transition:
    color        var(--tv-dur-base) var(--tv-ease),
    border-color var(--tv-dur-base) var(--tv-ease),
    box-shadow   var(--tv-dur-base) var(--tv-ease);
}

.tv-promise .tv-pillar__link:hover {
  border-color: var(--tv-rule);
  box-shadow: var(--tv-shadow-md);
}

/* 60% of the viewport, centred on it rather than on the container. The margin
   centres 60vw inside the container's content box: half that box (50%) less
   half the video (30vw). The percentage resolves against the content box,
   which is what makes it hold at any width. At this width it now sits inside
   the container on a desktop screen rather than breaking out of it. */
.tv-promise .tv-promise__video {
  order: 3;
  width: 60vw;
  max-width: none;
  margin-inline: calc(50% - 30vw);
  margin-bottom: 0;
}

/* 60vw of a phone is a 225px-wide video, which is not worth showing. Below
   the tablet breakpoint it takes the container instead. */
@media (max-width: 767px) {
  .tv-promise .tv-promise__video {
    width: auto;
    margin-inline: 0;
  }
}

/* ---- pause/resume control on the autoplaying player ----------------------
   The stage exists only to give the button something to anchor to that is the
   size of the picture. site.js wraps the <video> in it; the <figure> above is
   the wrong box because it includes the caption.                            */

.tv-videofig__stage { position: relative; display: block; }

.tv-videofig__toggle {
  position: absolute;
  left: var(--tv-space-4);
  bottom: var(--tv-space-4);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;                           /* the WCAG target-size floor */
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: var(--tv-radius-pill);
  background-color: var(--tv-scrim-55);
  color: var(--tv-white);
  cursor: pointer;
  transition: background-color var(--tv-dur-base) var(--tv-ease);
}

/* The glyph is white against the scrim; on a gold ground it has to go dark. */
.tv-videofig__toggle:hover {
  background-color: var(--tv-gold);
  color: var(--tv-forest-deep);
}

/* The light step for the ring: this control sits on video, and the deep gold
   would vanish against a dark frame. */
.tv-videofig__toggle:focus-visible {
  outline: 2px solid var(--tv-gold-light);
  outline-offset: 3px;
}

.tv-videofig__toggle .tv-icon { width: 1.1em; height: 1.1em; }

.tv-promise .tv-videofig__frame {
  /* 16/9 at 80vw is still a tall band — 864px at 1920 wide. The cap barely
     engages below that; object-fit: cover, already set on the media, takes
     the crop. Drop it if you want the full frame. */
  max-height: 80vh;
}

/* --------------------------------------------------------------------------
   26. PROCESS  (home / 07 and the shared section)

   Restyled as cards on a solid ground: coloured band, white tiles, a large
   accent numeral over a title and a line of text in each.

   Translated into the theme rather than copied. The reference's ground is its
   own brand accent; here that is forest, the dark ground the theme already
   uses for the footer and CTA bands — copper at full-section scale is louder
   than this palette wants, and the on-dark text tokens only exist for forest.
   The numerals take the accent instead, set in Poppins SemiBold — the type
   guideline gives Poppins "functional information", which is what a step
   number is, and reserves Cormorant for display and section-opening
   statements. Titles stay Poppins semibold rather than the reference's bold
   italic, because that is what every other card title on the site is.

   The section file styles all of this from an inline <style> in the post
   content, which renders after this sheet, so every selector here carries the
   section class to outweigh it.
   -------------------------------------------------------------------------- */

section.tv-process {
  background-color: var(--tv-forest);
  color: var(--tv-on-dark);
  /* The tight step of the section rhythm rather than a one-off number, so
     this still lines up with the rest of the page. 64-120 -> 48-72. */
  padding-block: var(--tv-section-y-tight);
}

.tv-process .tv-eyebrow  { color: var(--tv-on-dark-accent); }
.tv-process .tv-headline { color: var(--tv-on-dark); }
.tv-process .tv-lead     { color: var(--tv-on-dark-muted); }

/* The hairline joining the badges belonged to a row of rings on one ground.
   Between separate tiles it reads as a stray rule, so it goes. */
.tv-process .tv-process__steps::before { display: none; }

.tv-process .tv-step {
  padding: var(--tv-space-6);            /* 48/32 -> 32 all round */
  gap: var(--tv-space-3);
  background-color: var(--tv-surface);
  border-radius: var(--tv-radius-md);
  box-shadow: var(--tv-shadow-lg);
  transition: transform var(--tv-dur-base) var(--tv-ease);
}

/* Lift only. The shadow scale tops out at --lg, and against forest a shadow
   barely registers anyway — the movement is what reads. */
.tv-process .tv-step:hover { transform: translateY(-4px); }

/* The ring is gone — the numeral itself is the mark now, so the badge's box,
   border and fill all have to be unset, not just resized. */
.tv-process .tv-step__num {
  width: auto;
  height: auto;
  margin-bottom: 0;
  border: 0;
  border-radius: 0;
  background-color: transparent;
  /* Deep gold: the numeral is the mark now, and the accent reads at 2.16:1 on
     this tile — under the floor even at display size. */
  color: var(--tv-gold-deep);
  /*
   * Poppins SemiBold, per the type guideline: Poppins carries "headings,
   * subheadings, body copy, captions, and functional information", and a step
   * number is functional information. It was Cormorant here, which the
   * guideline reserves for display and section-opening statements.
   *
   * SemiBold (600) is the ceiling, not a preference — base.css loads only
   * Poppins 400/500/600, so --tv-fw-display-bold at 700 would have the browser
   * synthesise a fake bold off the 600 face.
   *
   * Slight negative tracking because Poppins is geometric and its digits sit
   * loosely at display sizes; the section file's tabular-nums keeps 01/02/03
   * the same width as each other.
   */
  font-family: var(--tv-font-body);
  /* Kept under the H1 ceiling. At 60px this numeral outran the largest
     heading on the page once the scale was synced to the guideline. */
  font-size: clamp(2rem, 3.2vw, 2.75rem);    /* 32 -> 44 */
  font-weight: var(--tv-fw-semibold);
  line-height: 1;
  letter-spacing: -0.01em;
}

/* The badge's hover filled the ring copper and flipped the number white.
   With no ring left that would just erase the numeral against the tile. */
.tv-process .tv-step:hover .tv-step__num {
  background-color: transparent;
  color: var(--tv-forest);
}

/* Measure was set for text on an open ground; inside a tile the padding is
   what sets the line length. */
.tv-process .tv-step__text { max-width: none; }

/*
 * Trimmed down on a phone. Stacked one per row the four tiles ran 260px each
 * and the section came to 1587px — a long scroll for four short paragraphs.
 * Nothing is removed: the padding comes in one step, the numeral drops to the
 * H2 size (it is a label here, not a display figure, and 32px next to a 20px
 * title was top-heavy at this width), and the body leading eases from 1.75 to
 * 1.6, which is still comfortable for a three or four line paragraph.
 */
@media (max-width: 767px) {
  .tv-process .tv-step { padding: var(--tv-space-5); }
  .tv-process .tv-step__num { font-size: var(--tv-fs-h2); }
  .tv-process .tv-step__text { line-height: 1.6; }
}

/* Head-to-cards and cards-to-CTA, both a step down from the section file's
   48 and 64. These two gaps sit either side of the tiles, so trimming them is
   what actually shortens the band rather than just the tiles. */
.tv-process .tv-section-head { margin-bottom: var(--tv-space-6); }
.tv-process .tv-process__cta { margin-top: var(--tv-space-6); }

/*
 * CTA. --primary is a forest fill, which is invisible on a forest ground, so
 * it takes --ghost-light: the theme's own variant for exactly this ground,
 * and its hover is the solid light fill. Not --accent, which the button
 * section reserves for the header and the closing conversion band so the
 * copper keeps its weight.
 *
 * Also off --lg — at 62px tall with 64px side padding it ran ~485px, a slab
 * under four short columns. 52px still clears the 44px WCAG target floor.
 */
.tv-process__cta .tv-btn--lg {
  min-height: 52px;
  padding: var(--tv-space-4) var(--tv-space-7);
  font-size: var(--tv-fs-body);
}

.tv-process__cta .tv-btn--primary {
  background-color: transparent;
  border-color: var(--tv-on-dark-border);
  color: var(--tv-on-dark);
  box-shadow: none;
}

.tv-process__cta .tv-btn--primary:hover {
  background-color: var(--tv-on-dark);
  border-color: var(--tv-on-dark);
  color: var(--tv-forest);
  box-shadow: var(--tv-shadow-md);
}

/* --------------------------------------------------------------------------
   27. CREDENTIALS — REAL ASSOCIATION MARKS

   Replaces the CSS-drawn medallions that stood in for these. The section file
   sets .tv-badges__list to a 6-column grid built for six identical discs;
   there are seven marks here in two quite different shapes — 1:1 medallions
   and 4:1 banners — so equal columns would either crush the banners or strand
   the medallions in a lot of empty cell.

   Flex instead, with every cell on `flex: 1 1 0`. Letting each mark take the
   width it needed was what produced the uneven rows: sized by their own
   content the four discs came out ~78px and the banners three times that, so
   the line filled up mid-group and wrapped wherever it happened to run out.
   A flex-basis of 0 means no cell can ever force a wrap, which leaves the
   break element below as the only thing that decides where a row ends — the
   split is then 4 + 3 at every desktop width instead of a function of how wide
   the artwork happens to be.

   Cells share their row equally and the image is centred inside, so the marks
   land on a regular rhythm. max-height equalises the discs optically; the
   banners hit the cell width first and come out shorter, which is right — a
   wordmark 4x as wide should not also be as tall as a disc.
   -------------------------------------------------------------------------- */

.tv-badges .tv-badges__list--logos {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--tv-space-5) var(--tv-space-7);
}

/*
 * max-width caps how far the three-up row can spread, since a row of three
 * has half again the space per cell that the row of four does.
 *
 * .tv-badges scopes these to beat the section file's own .tv-seal rules, which
 * are inline in the body and therefore win any tie on specificity.
 */
.tv-badges .tv-seal--logo {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  max-width: 240px;
  justify-content: center;
  transition: transform var(--tv-dur-base) var(--tv-ease);
}

/*
 * The ground colour is painted here, on the item itself, not left to the
 * section behind it.
 *
 * This list animates in via data-tv-reveal-stagger, which sets opacity below 1
 * on each item. An opacity under 1 creates a stacking context, and the child's
 * mix-blend-mode then composites against that isolated, transparent group
 * rather than against the page — so the white rectangles stayed opaque for the
 * length of the transition and only dropped out once opacity returned to 1.
 *
 * Painting ivory here gives the blend the same backdrop in both states, so
 * there is nothing to flash.
 */
.tv-seal--logo {
  display: flex;
  align-items: center;
  background-color: var(--tv-ivory);
}

/*
 * .tv-badges is load-bearing here, not tidiness. Elementor's frontend.css ships
 * `.elementor img { height: auto; max-width: 100% }` at the same (0,1,1) this
 * would otherwise have, and it is enqueued after this file — so a bare
 * `.tv-seal--logo img` loses the tie and both properties silently belong to
 * Elementor. That is what let the banners run to their full height-capped
 * width and shove the third one onto a row of its own.
 */
.tv-badges .tv-seal--logo img {
  width: auto;
  height: auto;
  max-height: 78px;
  max-width: 100%;
  object-fit: contain;
  /*
   * Two of these ship on a white rectangle rather than transparency, which
   * reads as a white card sitting on the ivory ground. multiply drops white to
   * the background it sits on while leaving the artwork, which is the standard
   * treatment for a strip of marks from different sources — and it is a no-op
   * on the files that do have an alpha channel, so it can apply to all seven.
   *
   * It does mean these must stay on a light ground; on forest they would go
   * near-black. Use knocked-out artwork there instead.
   */
  mix-blend-mode: multiply;
}

/* --------------------------------------------------------------------------
   Hover labels

   Several of these marks do not name themselves — the three NALP medals read
   as "2024" and a ribbon, and the Weathermatic disc is a logotype — so the
   label is what tells a visitor what they are looking at.

   The text repeats the img alt, so it is aria-hidden: a screen reader already
   has the name from the image and should not hear it twice. Keeping the alt
   rather than emptying it in favour of the span also means each mark still has
   an accessible name if this CSS never arrives.

   Positioned absolutely, so revealing one cannot reflow the strip.
   -------------------------------------------------------------------------- */

.tv-seal__label {
  position: absolute;
  bottom: calc(100% + var(--tv-space-2));
  left: 50%;
  z-index: 2;
  width: max-content;
  max-width: 200px;
  padding: var(--tv-space-2) var(--tv-space-3);
  border-radius: var(--tv-radius-sm);
  background-color: var(--tv-forest);
  color: var(--tv-ivory);
  font-size: var(--tv-fs-caption);
  line-height: var(--tv-lh-snug);
  text-align: center;
  text-wrap: balance;
  box-shadow: var(--tv-shadow-md);
  opacity: 0;
  /* Starts nearer the mark and rises clear of it, so the motion reads as the
     label coming out of the logo rather than dropping onto it. */
  transform: translate(-50%, 6px);
  transition:
    opacity   var(--tv-dur-base) var(--tv-ease),
    transform var(--tv-dur-base) var(--tv-ease);
  /*
   * The label sits directly above the mark. If it took the pointer it would
   * steal hover from the item that opened it, and the two would flicker
   * against each other.
   */
  pointer-events: none;
}

/*
 * Below the desktop layout the label goes away entirely.
 *
 * It is centred on its mark and up to 200px wide, so on the rightmost mark it
 * runs past the viewport: measured at 16px over on a 320px screen, 6px on a
 * 360px, and 5px at 768px. It is also a hover affordance, and a touch screen
 * has no hover, so on exactly the widths where it overflowed it could never be
 * triggered. The mark's alt text carries the same information to a screen
 * reader either way.
 */
@media (max-width: 900px) {
  .tv-seal__label { display: none; }
}

/* Caret on the underside, so the label reads as belonging to the mark below it
   rather than floating loose in the gap. */
.tv-seal__label::before {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  border: 6px solid transparent;
  border-top-color: var(--tv-forest);
  transform: translateX(-50%);
}

/*
 * Gated on a real pointer. On a touch screen there is no hover to open this,
 * and :hover there either never fires or sticks after a tap — neither is worth
 * shipping, and the name is already in the alt.
 */
@media (hover: hover) and (pointer: fine) {
  .tv-badges .tv-seal--logo:hover .tv-seal__label {
    opacity: 1;
    transform: translate(-50%, 0);
  }

  /*
   * A label rising off the second row crosses the row gap and lands over the
   * first row's marks, each of which carries an opaque ivory ground. DOM order
   * happens to cover this — the later item paints over the earlier one — but
   * only by accident, and the lift below puts the item in its own stacking
   * context, so state it rather than rely on it.
   */
  .tv-badges .tv-seal--logo:hover {
    z-index: 3;
    /* The same 3px lift the CSS medallions used, so the two treatments read as
       one component. It is on the item and not the image on purpose: the image
       carries mix-blend-mode, and wrapping it in its own transform is the kind
       of isolation behind the white-flash bug noted above. */
    transform: translateY(-3px);
  }
}

/*
 * Forces the wrap after the fourth mark — see the note in 12b-badges.html.
 *
 * A 100% basis cannot share a line with anything, so this takes a flex line of
 * its own between the two rows of marks. It has no height, but the row-gap is
 * still traversed on both sides of it, which is why the discs and the banners
 * sit a full 2 x row-gap apart. That reads correctly here — they are two
 * different kinds of mark — so it is left as is rather than clawed back with a
 * negative margin, whose effect on a flex line's cross size is not dependable.
 */
.tv-seal-break {
  flex-basis: 100%;
  height: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (max-width: 767px) {
  .tv-badges .tv-badges__list--logos { gap: var(--tv-space-5); }
  .tv-badges .tv-seal--logo img { max-height: 62px; }

  /*
   * Two up on a phone. Four across the width of a handset would leave each
   * mark around 60px, at which point a wordmark is unreadable and the strip
   * stops doing its job. An explicit width is needed because flex-basis 0
   * would otherwise hold all seven on one line.
   */
  .tv-badges .tv-seal--logo {
    flex: 0 1 auto;
    width: calc((100% - var(--tv-space-5)) / 2);
    max-width: none;
  }

  /* The break belongs to the 4 + 3 desktop split; at two up it would put a
     lone mark on its own row. */
  .tv-seal-break { display: none; }
}

/* --------------------------------------------------------------------------
   28. TESTIMONIALS — AGGREGATE RATING

   The score reads as functional information, which the type guideline gives
   to Poppins; it was set in Cormorant. Same change as the process numerals in
   26, and the same weight ceiling — base.css loads Poppins 400/500/600 only,
   so --tv-fw-display-bold at 700 would be a synthesised fake bold.

   Tracking has to change with the family, not just carry over:
   --tv-ls-display is cut for Cormorant at display sizes and leaves Poppins
   looking loose. The section file's tabular-nums stays as it is.
   -------------------------------------------------------------------------- */

.tv-testimonials .tv-rating__score {
  font-family: var(--tv-font-body);
  font-weight: var(--tv-fw-semibold);
  letter-spacing: -0.015em;
}


/* ==========================================================================
   29. PROJECT FILTER CHIPS  (home / 08 and projects / 02)
   --------------------------------------------------------------------------
   These lived in the homepage section's inline <style>, which meant the
   projects index page — same markup, same data-tv-filter contract — rendered
   them as bare browser buttons: square, no uppercase, no target height, and
   the wrapper defaulting to display:block. Both pages use .tv-card for the
   cards themselves, so the chips belong here for the same reason.

   Keep this the only definition. A copy left in a section file would load
   after this one at equal specificity and silently win.
   -------------------------------------------------------------------------- */

.tv-projects__filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tv-space-2);
}

.tv-filter-btn {
  padding: var(--tv-space-2) var(--tv-space-5);
  /* 44, not 40: these are the only controls on the page a thumb has to hit
     accurately, and 40 was under the target size the rest of the theme holds
     to. */
  min-height: 44px;
  border: 1px solid var(--tv-rule);
  border-radius: var(--tv-radius-pill);
  background-color: transparent;
  color: var(--tv-muted);
  font-family: var(--tv-font-body);
  font-size: var(--tv-fs-caption);
  font-weight: var(--tv-fw-medium);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* Buttons inherit alignment from the container here, which is invisible while
     they are content-width in the flex row but leaves every label hard against
     the left edge once they become grid cells. */
  text-align: center;
  cursor: pointer;
  transition:
    background-color var(--tv-dur-fast) var(--tv-ease),
    border-color     var(--tv-dur-fast) var(--tv-ease),
    color            var(--tv-dur-fast) var(--tv-ease);
}

.tv-filter-btn:hover { border-color: var(--tv-gold-deep); color: var(--tv-gold-deep); }

.tv-filter-btn.is-active {
  background-color: var(--tv-forest);
  border-color: var(--tv-forest);
  color: var(--tv-on-dark);
}

/*
 * Below this width the five chips wrap ragged — 2 / 2 / 1, leaving up to 204px
 * of dead space at the ends of rows. A two column grid divides them evenly
 * instead, with "All" spanning the full width above the four services: it is
 * the reset rather than one of the set, so promoting it is what makes five
 * items fill a two column grid exactly.
 */
@media (max-width: 620px) {
  .tv-projects__filters {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /*
     * Take the whole row. This block is a flex item of a space-between header,
     * so without an explicit width it is sized to its own content — it sat at a
     * fixed 252px inside a 350px column, leaving a 98px hole on the right and
     * (on the projects page at wider phone sizes) getting pushed hard right by
     * space-between. At 100% the two columns resolve against the full row, so
     * the chips line up with the heading and lead above them.
     */
    width: 100%;
  }
  .tv-filter-btn[data-tv-filter-btn="all"] { grid-column: 1 / -1; }
  /* "Maintenance" and "Enhancement" are the long labels; the desktop side
     padding does not leave them room to hold one line in a half-width cell. */
  .tv-filter-btn { padding-inline: var(--tv-space-3); }
}


/* ==========================================================================
   30. CAROUSEL DOTS  (data-tv-carousel)
   --------------------------------------------------------------------------
   Position marker for the mobile carousels. Shared, because more than one
   section uses it now — keeping it inside a section's <style> is what left the
   project filter chips unstyled on their own index page.

   Hidden by default. A section turns them on inside the media query where its
   carousel actually exists, and that rule must come AFTER this one: same
   specificity, so source order decides.

   Colours resolve from the ground the dots sit on, so the same markup works on
   the forest testimonials band and the ivory credentials row.
   -------------------------------------------------------------------------- */

.tv-dots {
  display: none;
  justify-content: center;
  gap: var(--tv-space-1);
  margin-top: var(--tv-space-5);
}

/*
 * The 8px mark is drawn by the pseudo-element so the hit area can stay at the
 * theme's 44px target size without a row of oversized circles under the cards.
 */
.tv-dot {
  position: relative;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.tv-dot::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 8px;
  height: 8px;
  margin: -4px 0 0 -4px;
  border-radius: var(--tv-radius-pill);
  background-color: var(--tv-muted);
  opacity: 0.4;
  transition:
    opacity          var(--tv-dur-fast) var(--tv-ease),
    background-color var(--tv-dur-fast) var(--tv-ease),
    width            var(--tv-dur-fast) var(--tv-ease);
}

/* The active mark stretches into a short bar as well as changing colour, so the
   position is readable without depending on a contrast difference alone. */
.tv-dot.is-active::before {
  width: 22px;
  margin-left: -11px;
  background-color: var(--tv-gold-deep);
  opacity: 1;
}

.tv-ground--forest .tv-dot::before { background-color: var(--tv-on-dark-muted); }
.tv-ground--forest .tv-dot.is-active::before { background-color: var(--tv-gold); }


/*
 * Loop copies, inserted by site.js so the first and last slides have a
 * neighbour on both sides.
 *
 * They belong to the carousel and nothing else. Above the breakpoint each track
 * reverts to a static grid or wrapped row, where the copies render as duplicate
 * content — two repeated quotes, or two repeated credential logos. Hiding them
 * generically here covers every carousel; doing it per section is how the
 * credentials row ended up showing nine logos on a desktop grid built for
 * seven.
 *
 * Both card types are flex containers in their own right, so `flex` is the
 * correct value to restore rather than `block`.
 */
[data-tv-carousel] .is-clone { display: none; }

@media (max-width: 767px) {
  [data-tv-carousel] .is-clone { display: flex; }
}
