/*
 * The shared footer template (post 382, "include/general" -- entire
 * site) renders two more raw Elementor widgets (not PHP shortcodes, so
 * they can't be guarded with is_page() the way [jr_recent_projects] was):
 * "Local Roofers Serving Manchester, Lowell & Maine" and the red "Roof
 * problems? Emergency roof repairs available 24/7" banner. PROJECTS-01
 * (2026-09-15) requires neither on /projects/. Scoped to this page's
 * real numeric ID only -- the template itself is untouched and renders
 * unchanged on every other page.
 *
 * REAL BUG, found live 2026-09-15: the first version of this rule hid
 * `.elementor-element-75387cf` -- which turned out to be the OUTER
 * container for BOTH the "Local Roofers Serving" text block (541f18f)
 * AND the real quote form (404f66a > 745cfab > [jr_footer_quote]) as its
 * two flex children, not just the text. Hiding 75387cf silently
 * collapsed the quote form to zero height on this page too (confirmed:
 * .jr-quiz-card existed in the DOM with real content but rendered at
 * height 0, only found by actually clicking "Get a Free Quote" and
 * checking where it landed, not by checking the element merely exists).
 * Fixed by targeting the specific inner child (541f18f) that holds only
 * the unwanted text/icon/list content -- its sibling 404f66a (quote
 * form) is never touched. 779d81b (emergency banner) is self-contained,
 * no such risk there.
 */
body.page-id-10616 .elementor-element-541f18f,
body.page-id-10616 .elementor-element-779d81b {
  display: none !important;
}

/*
 * Projects page ([jr_projects], PROJECTS-01, 2026-09-15).
 * Layout only -- card visuals reuse .lib-card (card-base.css), eyebrows
 * reuse .jr-card-kicker.jr-eyebrow-badge (already site-wide), title
 * emphasis reuses .hhds-title-highlight (tokens-brand-bridge.css), the
 * hero reuses the real .jr-home-hero (hero.css), photos reuse the real
 * .lib-media-frame / .lib-media-image (media-frame.css / media-image.css),
 * and filter/CTA buttons reuse the real .jr-btn family (cta-buttons.css,
 * including .jr-btn-filter, a 4th type added to THIS project's own
 * family, owner-authorized 2026-09-15 -- never a generic library pattern
 * swapped in instead, per owner: "cada marca tiene sus familias de
 * botones... si no está en la librería se debe agregar, no reemplazar").
 * No new color, radius, or shadow values invented here -- everything
 * below is a jr- or lib- token reference.
 *
 * "Featured Project" is now per-category, not one global section --
 * originally it always showed the same Roofing photo regardless of which
 * filter was active (owner: "al presionar el botón del servicio
 * correspondiente no quedará este mismo de roof sino del servicio
 * seleccionado"). An intermediate pass tried to fix that plus a real
 * empty-space problem (the copy column had too little text next to a
 * tall photo) by deleting the description paragraph entirely and
 * reusing the plain grid card instead -- done without authorization
 * (owner: "cuál te autorizó a hacer eso"). Reverted: each category's
 * first project keeps its own real .jr-projects-featured__card (photo +
 * full paragraph description, functions.php), just one per category
 * instead of one fixed global one. The actual empty-space fix is a
 * properly-sized description per item, not removing the description.
 */

/* Interior-page hero doesn't need Home's near-full-screen height --
   shorter box, same photo/overlay/header-clearance mechanics. Compound
   selector (0,2,0) beats hero.css's plain .jr-home-hero (0,1,0) on
   specificity alone.
   68vh (was 62vh, was 56vh): the hero photo itself was re-cropped
   2026-09-16 (owner: "la foto de hero lo alejes mas se ve demaciado con
   zoom y no deja apreciar bien la casa") from a tight 2.82:1 roof-only
   band to a wider 2.60:1 crop showing the whole house (functions.php,
   $hero_image). A less extreme (proportionally taller) image ratio needs
   a proportionally taller box to still cover without cropping the new
   headroom/footroom back out -- 62vh (670px at a common 1920x1080
   screen) was tuned for the OLD 2.82:1 image and would just re-crop the
   new one down to roughly its old framing. 68vh (~734px at that same
   screen) covers the new 2.60:1 image close to edge-to-edge, verified
   live, while staying well short of Home's own near-full-screen hero
   (~930px at the same viewport). */
.jr-projects-hero.jr-home-hero {
  min-height: 68vh;
  padding-top: 160px;
}

/* This hero's own photo (swapped 2026-09-16) was pre-cropped to a wide
   roof-only band (source: lowell_0018_additional.jpg, cropped to the
   roofline with a small margin top and bottom) specifically so the roof
   stays the dominant, fully-visible subject once background-size:cover
   crops further to fill the section -- reusing hero.css's own
   "center 60%" (tuned for a different, sky-heavy photo) would push this
   photo's frame down and cut into the roof itself. Centered position
   keeps the crop even between the small top (sky/trees) and bottom
   (siding/grass) margins already built into the source image. */
.jr-projects-hero.jr-home-hero::before {
  background-position: center center;
}

/* REAL BUG, found live 2026-09-16 (owner: "quiero que el copy del hero
   se muestre que no se oculte"): hero.css's shared
   `@media (max-width: 767px) { .jr-home-hero__subtitle { ... } }` rule
   visually hides every hero's subtitle on mobile via the standard
   clip-based technique (position:absolute; width/height:1px;
   overflow:hidden; clip:rect(0,0,0,0)) -- confirmed live via
   getComputedStyle (rect was 1x1px despite opacity:1/visibility:visible).
   That's a real, owner-authorized decision, but only for the HOME hero
   (2026-09-09: its subtitle crowded the title on a small screen) -- it
   isn't scoped to Home in hero.css, so it silently gagged this page's
   own, different, shorter subtitle too, which never had a crowding
   problem to justify hiding it. Reversing every clipped property back to
   normal, scoped to `.jr-projects-hero` only, leaves Home's mobile
   behavior completely untouched. */
@media (max-width: 767px) {
  .jr-projects-hero .jr-home-hero__subtitle {
    position: static;
    width: auto;
    height: auto;
    padding: 0;
    margin: 16px 0 0;
    overflow: visible;
    clip: auto;
    white-space: normal;
    border: 0;
  }
}

/* Filters and the category groups used to be two separate <section>
   elements, each with its own full section-y top+bottom padding and its
   own background -- that stacked into a visibly taller, differently
   colored band sitting between the hero and "Roofing Projects" that read
   as its own section (owner, 2026-09-15: "pareciera que es otra seccion
   ... esta demasiado espacio"). One section, one background, one padding
   rhythm now; the filter row's own spacing below is a normal component
   gap, not another full section-y. */
.jr-projects-grid-section {
  padding-block: var(--jr-section-y);
  background: var(--jr-color-surface-tint);
}

.jr-projects-grid-section__inner {
  width: var(--jr-container-wide);
  margin-inline: auto;
}

/* media-frame.css's own composing rule (.lib-media-frame > .lib-media-image
   { height: 100% }) uses a direct-child combinator, which this project's
   real markup doesn't match -- every image here goes through a <picture>
   for AVIF/WebP sources (.lib-media-frame > picture > img.lib-media-image),
   making the img a grandchild, not a child. Descendant version of the same
   fix, plus picture itself (inline by default, so it never reports a real
   box height on its own). Confirmed via live cascade inspection (2026-09-15)
   that a bare `height: 100%` here loses to some other same-or-higher-
   specificity `img` rule already on this page (root rule never isolated --
   not worth more time chasing when this fixes it outright); !important
   matches this project's own established precedent for exactly this
   situation (cta-buttons.css's .elementor-widget-shortcode). */
.lib-media-frame picture {
  display: block;
  width: 100%;
  height: 100%;
}

.lib-media-frame picture .lib-media-image {
  height: 100% !important;
}

/* ---- Section-level heading (owner, 2026-09-16, page analysis: the grid
   section had no heading of its own, jumping straight from the hero into
   the filter pills). Centered, one size step above a category heading
   (--jr-text-3xl vs --jr-text-2xl) so it reads as the parent of the
   category headings below it, not a same-level sibling. ---- */
.jr-projects-grid-section__heading {
  font-family: var(--jr-font-heading);
  font-weight: 800;
  font-size: var(--jr-text-3xl);
  color: var(--jr-color-heading);
  text-align: center;
  margin: 0 0 1.5rem;
}

/* ---- Filters -- real .jr-btn-filter (cta-buttons.css, 4th button type,
   owner-authorized 2026-09-15) for the pill itself. flex:1 on each pill
   is what makes the row fill the section's real width evenly instead of
   clumping left with dead space to the right (owner, 2026-09-15: "no
   tenemos esa mierda ocupando toda la pantalla y con ese espacio
   vacío"). min-width keeps a pill from squashing unreadably thin on a
   very wide row with few categories. ---- */
.jr-projects-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1rem;
  margin-bottom: 2rem;
}

.jr-projects-filter {
  flex: 1 1 8rem;
  text-align: center;
}

/* ---- Mobile category selector -- trigger bar + bottom-sheet modal
   (owner, 2026-09-16, real bug found live: "esos botones no tienen nada
   profesional" -- 7 pills wrapped into an ugly, screen-filling grid on a
   phone before a single project photo was visible). Originally shipped
   with a second "Filter" button next to the trigger (owner's own
   reference screens showed both) -- removed same day, live on a phone:
   "el selector compite con el boton de filtrado... eliminemos el de
   filtro ya tenemos el selector". One real control now, full width.
   Swapped in for .jr-projects-filters below 640px (see @media block at
   the end of this file) -- .jr-projects-filters itself is hidden there
   instead of removed, so nothing here duplicates its filtering logic,
   only its visual presentation on small screens. ---- */
.jr-projects-filter-bar {
  display: none;
  margin-bottom: 2rem;
}

/* !important on padding: Elementor's site-wide kit CSS resets every
   plain <button> to its own 20px/40px pill padding at higher specificity
   (class+tag) than this single-class selector -- same fight
   cta-buttons.css's .jr-btn-filter and
   .jr-projects-card--paired-carousel__nav (projects.css, above) already
   document and solve the same way. Found live 2026-09-16: without
   !important here, that 40px horizontal padding alone consumed this
   trigger's whole intended width, and on the close button below (a much
   smaller box) it consumed 100% of the available space, leaving
   literally 0px left for its icon -- confirmed via getComputedStyle, not
   a guess. */
.jr-projects-filter-bar__trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  gap: 0.5rem;
  height: 3rem;
  padding: 0 1.1rem !important;
  border: 1px solid var(--jr-color-border);
  border-radius: var(--jr-radius-pill);
  background: var(--jr-color-surface);
  color: var(--jr-color-heading);
  font-family: var(--jr-font-action);
  font-size: var(--jr-text-sm);
  font-weight: 700;
  cursor: pointer;
}

.jr-projects-filter-bar__trigger span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.jr-projects-filter-bar__trigger svg {
  flex: none;
  width: 1.1rem;
  height: 1.1rem;
}

/* Overlay: fixed, full-viewport, dialog anchored to the bottom edge --
   matches the owner-supplied reference (a bottom sheet, not a centered
   popup). [hidden] (functions.php default state) removes it from layout
   entirely; projects-filters.js toggles the attribute, never display,
   so this one rule is the only visibility switch to keep in sync. */
.jr-projects-filter-modal[hidden] {
  display: none;
}

.jr-projects-filter-modal {
  display: block;
  position: fixed;
  inset: 0;
  z-index: 100;
}

/* Derived from --jr-color-graphite (not a hardcoded rgba()) -- same
   color-mix-for-a-translucent-variant approach already used site-wide
   (e.g. .jr-btn-secondary-light:hover above), owner, 2026-09-16: "debe
   ser de tokens establecidos no uno hardcodeado inventado a ultima
   hora". */
.jr-projects-filter-modal__backdrop {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--jr-color-graphite) 78%, transparent);
}

.jr-projects-filter-modal__sheet {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  max-height: 82vh;
  display: flex;
  flex-direction: column;
  background: var(--jr-color-surface);
  border-radius: 1.25rem 1.25rem 0 0;
  box-shadow: var(--jr-shadow-strong);
  padding: 0.75rem 1.5rem calc(1.5rem + env(safe-area-inset-bottom, 0px));
}

.jr-projects-filter-modal__handle {
  width: 2.75rem;
  height: 0.3rem;
  margin: 0 auto 0.75rem;
  border-radius: var(--jr-radius-pill);
  background: var(--jr-color-border);
}

.jr-projects-filter-modal__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.5rem;
}

.jr-projects-filter-modal__header h2 {
  font-family: var(--jr-font-heading);
  font-weight: 800;
  font-size: var(--jr-text-xl);
  color: var(--jr-color-heading);
  margin: 0;
}

/* !important: same Elementor kit-5 button padding fight as
   .jr-projects-filter-bar__trigger above -- this box is small
   enough (2.25rem) that the 40px horizontal padding alone exceeded it,
   leaving 0px of content width for the icon (confirmed live via
   getComputedStyle: width computed to 80px/padding 20px 40px instead of
   this rule's own 2.25rem/0). */
.jr-projects-filter-modal__close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem !important;
  height: 2.25rem !important;
  padding: 0 !important;
  border: 0;
  border-radius: var(--jr-radius-pill);
  background: var(--jr-color-surface-tint);
  color: var(--jr-color-heading);
  cursor: pointer;
}

.jr-projects-filter-modal__close svg {
  width: 1.1rem;
  height: 1.1rem;
}

.jr-projects-filter-modal__list {
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding-bottom: 0.25rem;
}

/* !important on padding: same Elementor kit-5 button fight, see
   .jr-projects-filter-modal__close above -- without it every row got
   the reset's 20px/40px instead of this rule's tighter 0.7rem/0.6rem,
   inflating each row's height and left/right whitespace well past what
   this design calls for. */
.jr-projects-filter-modal__row {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0.7rem 0.6rem !important;
  border: 0;
  border-radius: var(--jr-radius-md);
  background: none;
  text-align: left;
  cursor: pointer;
}

.jr-projects-filter-modal__row.is-active {
  background: color-mix(in srgb, var(--jr-color-primary) 10%, var(--jr-color-surface));
}

.jr-projects-filter-modal__icon {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--jr-radius-md);
  background: color-mix(in srgb, var(--jr-color-primary) 10%, var(--jr-color-surface));
  color: var(--jr-color-primary);
}

.jr-projects-filter-modal__row.is-active .jr-projects-filter-modal__icon {
  background: var(--jr-color-primary);
  color: #fff;
}

.jr-projects-filter-modal__icon svg {
  width: 1.25rem;
  height: 1.25rem;
}

.jr-projects-filter-modal__label {
  flex: 1 1 auto;
  font-family: var(--jr-font-body);
  font-size: var(--jr-text-base);
  font-weight: 700;
  color: var(--jr-color-heading);
}

.jr-projects-filter-modal__check {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  height: 1.25rem;
  color: var(--jr-color-primary);
  visibility: hidden;
}

.jr-projects-filter-modal__row.is-active .jr-projects-filter-modal__check {
  visibility: visible;
}

.jr-projects-filter-modal__check svg {
  width: 100%;
  height: 100%;
}

/* ---- Shared tag / location chips (grid + featured cards) ---- */
.jr-projects-tag {
  font-family: var(--jr-font-action);
  font-size: var(--jr-text-xs);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--jr-color-primary);
}

.jr-projects-location {
  font-family: var(--jr-font-body);
  font-size: var(--jr-text-sm);
  font-weight: 600;
  color: var(--jr-color-text-muted);
}

/* ---- Category groups (one per service type, "All" shows every group in
   sequence). The adjacent-sibling combinator (+) still matches a group
   whose immediately-preceding sibling is [hidden] -- that sibling is
   still in the DOM, just invisible -- so filtering to any category other
   than the first (Roofing) left the visible group with a top
   margin/border meant to separate it from a group that isn't actually
   there on screen (owner, 2026-09-15: "solo en roofing esta bien, al
   seleccionar otro se vuelve a separar feamente"). The general-sibling
   version with :not([hidden]) on both sides only matches when there is
   a REAL visible group earlier in the DOM, regardless of how many hidden
   ones sit in between. ---- */
.jr-projects-category-group:not([hidden]) ~ .jr-projects-category-group:not([hidden]) {
  margin-top: calc(var(--jr-section-y) * 0.7);
  padding-top: calc(var(--jr-section-y) * 0.7);
  border-top: 1px solid var(--jr-color-border);
}

.jr-projects-category-group[hidden] {
  display: none;
}

.jr-projects-category-group__heading {
  font-family: var(--jr-font-heading);
  font-weight: 800;
  font-size: var(--jr-text-2xl);
  color: var(--jr-color-heading);
  margin: 0 0 0.4rem;
}

.jr-projects-category-group__description {
  font-family: var(--jr-font-body);
  color: var(--jr-color-text-muted);
  margin: 0 0 1.5rem;
  max-width: 60ch;
}

/* ---- Per-category Featured Project -- restored 2026-09-15 (owner:
   "vuelva a como estaba antes") after an earlier pass dropped the
   description paragraph entirely without authorization while chasing
   the empty-space fix. Real fix for the space this time: every
   category's featured item now has its own real paragraph description
   (functions.php), long enough to fill the copy column the same way the
   original Roofing one did -- the dead space was never inherent to
   having a description, it was a short/missing one next to a tall
   photo. ---- */
.jr-projects-category-group__featured-kicker {
  margin-bottom: 0.75rem;
}

.jr-projects-featured__card {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 0.7fr);
  gap: 0;
  border: 1px solid var(--jr-color-border);
  border-radius: var(--jr-radius-lg);
  overflow: hidden;
  background: var(--jr-color-surface);
  box-shadow: var(--jr-shadow-card);
  margin-bottom: var(--jr-card-gap);
}

.jr-projects-featured__card > .jr-projects-featured__media {
  position: relative;
}

.jr-projects-featured__card > .jr-projects-featured__media > .lib-media-frame {
  max-height: 26rem;
  border-radius: 0;
}

/* Only when a paired card exists does the photo drop its cap and fill
   exactly as tall as the row ends up (row height set by
   projects-filters.js, which measures both cards and applies the taller
   one to both) -- without a pair to stretch against (Gutters,
   single-photo categories), removing the cap made the photo balloon to
   its raw 16:9-at-full-width size (659px tall at 1920px, caught live
   2026-09-15) with nothing there to justify it.
   REAL BUG #1, found live 2026-09-15: height:100% alone (aspect-ratio
   still on) made the browser compute WIDTH from height via the ratio
   (478px * 16/9 = ~850px) instead of from its real ~764px grid column --
   the photo spilled into the copy column, text rendering on top of it.
   width:100% fixes that by keeping width tied to the grid track.
   REAL BUG #2, found live 2026-09-15: dropping aspect-ratio to `auto`
   entirely broke the OTHER direction -- while the JS in step 1 measures
   each card's natural height (before either has an explicit pixel
   height), height:100% can't resolve yet (its own parent's height is
   what's being measured), falls back to `auto`, and with no ratio left
   to fall back to, the photo rendered at its raw intrinsic pixel size
   (a portrait drone shot came out ~1019px tall). Keeping the real 16:9
   ratio here means that measurement pass still has something sane to
   compute height from (using the already-definite width), and once the
   JS sets an explicit pixel height on the article afterward, that
   explicit height simply wins over the ratio (both dimensions end up
   definite, nothing left to derive) -- no conflict either way. */
.jr-projects-featured-row--paired .jr-projects-featured__card > .jr-projects-featured__media > .lib-media-frame {
  width: 100%;
  height: 100%;
  max-height: none;
}

/* ---- Before/After comparison slider (every category's Featured
   Project, owner request 2026-09-16). Sized exactly like the
   .lib-media-frame it replaces in this same column, so swapping one for
   the other never shifts the surrounding grid/copy layout. ---- */
.jr-projects-featured__card > .jr-projects-featured__media > .jr-ba-slider {
  max-height: 26rem;
}

.jr-projects-featured-row--paired .jr-projects-featured__card > .jr-projects-featured__media > .jr-ba-slider {
  height: 100%;
  max-height: none;
}

.jr-ba-slider,
.jr-ba-slider__frame {
  width: 100%;
  height: 100%;
}

.jr-ba-slider__frame {
  position: relative;
  overflow: hidden;
  aspect-ratio: var(--lib-aspect-video);
  user-select: none;
  -webkit-user-select: none;
}

/* Both layers are the SAME full-frame size at all times -- the before
   photo never resizes as the divider drags, it's only clipped
   (clip-path) to reveal less or more of itself. This avoids the photo
   itself re-scaling/distorting mid-drag, which a width-based wrapper
   would cause. --jr-ba-pos (0-100%, set inline on .jr-ba-slider and kept
   in sync by projects-filters.js on the range input's `input` event)
   drives both the clip and the handle position from one source of
   truth. */
.jr-ba-slider__layer {
  position: absolute;
  inset: 0;
}

.jr-ba-slider__layer--before {
  clip-path: inset(0 calc(100% - var(--jr-ba-pos, 50%)) 0 0);
  border-right: 2px solid #fff;
}

.jr-ba-slider__layer picture {
  display: block;
  width: 100%;
  height: 100%;
}

/* REAL BUG, found live 2026-09-16 (owner tested in their own real
   browser: "intenta desplazarse un poco pero no completo ni a la
   izquierda ni a la derecha"): an <img> is draggable by default in every
   browser, so a mousedown-then-move over the photo triggers the native
   HTML5 "drag this image" gesture (confirmed live: dragstart fired on
   .jr-ba-slider__img) -- that native drag hijacks the mouse mid-gesture
   and immediately fires lostpointercapture on our own drag handler
   (projects-filters.js), which is why the divider only crept a few
   percent before freezing regardless of how far the mouse kept moving.
   draggable="false" (functions.php, on the <img> itself) is the real
   fix; -webkit-user-drag:none is the Safari-specific equivalent (Safari
   doesn't fully honor the draggable attribute on img in every version). */
/* REAL BUG, found live 2026-09-16 while verifying the slider CTA overlay:
   Chrome's UA stylesheet gives every <img width height> an implicit
   `aspect-ratio: attr(width) / attr(height)` (confirmed live via
   getComputedStyle: "auto 900 / 506"), which competes with this rule's
   own width:100%/height:100% for a paired-row slider (whose frame is
   stretched to match its sibling card's JS-set height, not the frame's
   own aspect-ratio -- see .jr-projects-featured-row--paired rule below).
   `aspect-ratio: auto` cancels the UA default so this rule's own
   width/height:100% can win outright, instead of leaving Chrome to
   decide between the two on every layout pass. This alone was not
   sufficient by itself on first paint -- see the matching JS-side fix in
   projects-filters.js's syncFeaturedRowHeights() for the other half
   (the ancestor height only exists via a late JS mutation, and Chrome
   wasn't reliably re-resolving this img's percentage height against it
   without also being told to). */
.jr-ba-slider__img {
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
  object-fit: cover;
  -webkit-user-drag: none;
  user-drag: none;
}

.jr-ba-slider__handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--jr-ba-pos, 50%);
  width: 0;
  transform: translateX(-50%);
  pointer-events: none;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
}

.jr-ba-slider__handle-grip {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 999px;
  background: #fff;
  color: var(--jr-color-graphite);
  box-shadow: var(--jr-shadow-card);
}

.jr-ba-slider__label {
  position: absolute;
  top: 0.75rem;
  z-index: 2;
  padding: 0.25rem 0.65rem;
  border-radius: var(--jr-radius-pill);
  background: rgba(21, 20, 23, 0.62);
  color: #fff;
  font-family: var(--jr-font-action);
  font-size: var(--jr-text-xs);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  pointer-events: none;
}

.jr-ba-slider__label--before {
  left: 0.75rem;
}

.jr-ba-slider__label--after {
  right: 0.75rem;
}

/* The "View [Category] Service" CTA used to live here (a hand-rolled
   pill overlaid on the photo/copy) -- removed 2026-09-16, owner: "el
   boton de view service deberia ser el terciario el de navegacion
   interna, no se porque lo creaste nuevo... no vamos a dejar en el box
   de before after sino en el final de la seccion". It's now plain
   `.jr-btn.jr-btn-tertiary` (cta-buttons.css) placed after the whole
   featured row (functions.php) -- see
   .jr-projects-category-group__cta below for its one layout-only
   addition (spacing), not a new button look. */

/* REAL BUG, found live 2026-09-16 (owner: "no deja desplazar para ver el
   before completo o after completo"): a native <input type="range">'s
   own drag handling reserves half the thumb's width as dead space at
   each end of the track (so a rendered thumb never overflows past the
   track edge), which caps the reachable value short of true 0/100 no
   matter how far the pointer drags -- worse here since the thumb was
   forced to full track width/height to act as an invisible drag
   surface, maximizing that reserved dead zone. Fixed by driving the
   drag with the Pointer Events API directly on the frame instead
   (projects-filters.js): value = pointer position / frame width, linear,
   with no thumb-radius reservation, so the divider can reach true 0%
   and 100%. Pointer Events already unify mouse and touch in one API, so
   this needs no separate touch handling. The range input stays in the
   DOM as a keyboard-only fallback (Tab to focus, arrow keys to move) --
   pointer-events:none takes it out of hit-testing so it never competes
   with the frame's own pointer handlers or reintroduces the thumb-edge
   problem for mouse/touch users. */
.jr-ba-slider__frame {
  cursor: ew-resize;
  touch-action: none;
}

.jr-ba-slider__range {
  position: absolute;
  inset: 0;
  z-index: 3;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  pointer-events: none;
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
}

.jr-projects-featured__copy {
  padding: clamp(1.5rem, 3vw, 2.75rem);
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.jr-projects-featured__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
}

/* REAL BUG, found live 2026-09-16 (owner: "vemos demaciado espacio entre
   donde dice siding lowell y ese subtitulo"): Kadence's own theme CSS
   carries a global `h3` margin-top rule that beats this single-class
   selector on specificity/source order -- confirmed live via
   getComputedStyle: computed margin-top was 54px (1.5em at this h3's
   36px font-size) despite this rule's own explicit `margin: 0 ...`.
   h2 on this same page was unaffected (Kadence's collision is h3-only
   here) -- same category of bug as the Kadence heading-margin collision
   already known project-wide, !important is the already-established fix
   for exactly this fight (see .lib-media-frame picture .lib-media-image
   above). */
.jr-projects-featured__copy h3 {
  font-family: var(--jr-font-heading);
  font-weight: 800;
  font-size: var(--jr-text-3xl);
  color: var(--jr-color-heading);
  margin: 0 0 0.75rem !important;
}

.jr-projects-featured__copy p {
  font-family: var(--jr-font-body);
  color: var(--jr-color-text-muted);
  line-height: var(--jr-leading-body);
  margin: 0;
}

.jr-projects-featured__copy h3 .hhds-title-highlight {
  color: inherit;
}

@media (max-width: 1024px) {
  .jr-projects-featured__card {
    grid-template-columns: 1fr;
  }

  .jr-projects-featured__card > .jr-projects-featured__media > .lib-media-frame,
  .jr-projects-featured__card > .jr-projects-featured__media > .jr-ba-slider {
    max-height: 18rem;
  }

  /* REAL BUG, found live 2026-09-16 (owner tested on a real phone: "no se
     ve nmi un carajo el techo" -- the roof/siding/gutters were barely
     visible, cropped into a wall or trees instead). Root cause: the
     desktop-only `.jr-projects-featured-row--paired ... .jr-ba-slider
     { height:100%; max-height:none; }` rule (above, no media query of
     its own) has one more class than the max-height:18rem rule right
     above this comment, so it wins on specificity even here at mobile
     widths -- confirmed live via getComputedStyle: max-height computed
     to "none" instead of "18rem". On mobile nothing sets a real pixel
     height on .jr-projects-featured__media (syncFeaturedRowHeights()
     deliberately skips paired rows once stacked), so that inherited
     height:100% resolves against an auto-height parent -- an ambiguous
     case browsers are free to resolve differently. This test browser
     happened to fall back to the frame's own 16:9 aspect-ratio (looked
     fine here), but a real phone browser resolved it into a much
     taller/narrower box instead, and object-fit:cover then cropped hard
     into the image's horizontal center -- trees or a brick wall instead
     of the actual roof/siding/gutters. Same specificity as the paired
     rule, declared later, so this wins outright instead of relying on
     the ambiguous fallback at all. Desktop is untouched -- this whole
     block is already scoped to max-width:1024px. */
  .jr-projects-featured-row--paired .jr-projects-featured__card > .jr-projects-featured__media > .jr-ba-slider {
    height: auto;
    max-height: 18rem;
  }

  /* REAL BUG, found live 2026-09-16 (owner: "ese texto de roof
     rempolacement parece h1 estamuy grande deberia ser menor incluso que
     titulo de la seccion no mayor en jerarquia"): --jr-text-3xl (36px)
     had no mobile override, so this per-project title outsized
     .jr-projects-category-group__heading (--jr-text-2xl/28px) right
     above it on the same screen -- a child reading larger than its own
     section's heading. --jr-text-xl (22px) keeps it clearly below that,
     while still reading as a real heading, not a caption. */
  .jr-projects-featured__copy h3 {
    font-size: var(--jr-text-xl) !important;
  }
}

/* A lone featured card, sitting alone in a full-width row, left empty
   space below the copy column once its description text (which is always
   shorter than the photo is tall) ran out (owner, 2026-09-15). Pairing it
   with the next plain card from the same category closes that gap:
   narrowing the featured card to about two thirds of the row wraps its
   own description across more lines (taller text block, same font size),
   while the freed third goes to the plain card at its normal size --
   nothing here overrides the plain card's own ratio/height, so it never
   inflates to its raw photo size. Falls back to the old full-width
   featured card automatically when a category has no second item to pair
   (functions.php only wraps the pair when one exists).
   The exact height match (owner, 2026-09-15: "deben tener el mismo
   alto") is set by projects-filters.js, which measures both cards'
   natural height and applies the larger one to both as an explicit
   pixel height -- CSS alone can't do this: a percentage/stretch height
   can't resolve against an auto-height ancestor, so the featured photo's
   height:100% fell back to the raw photo's intrinsic size instead of
   matching its sibling. align-items:stretch here is just the pre-JS
   fallback (briefly correct-ish before the script runs on
   DOMContentLoaded). The featured copy's centering and the paired card's
   info centering (below) are what let either side absorb that explicit
   height without leaving a dangling gap at the bottom. */
.jr-projects-featured-row {
  display: flex;
  align-items: stretch;
  gap: var(--jr-card-gap);
  margin-bottom: var(--jr-card-gap);
}

/* Layout-only placement for the relocated "View [Category] Service" link
   (owner, 2026-09-16: "no vamos a dejar en el box de before after sino
   en el final de la seccion, justo despues de la foto de carrusel") --
   the link itself is plain `.jr-btn.jr-btn-tertiary` (cta-buttons.css),
   no new button look, this only adds the gap above it since
   .jr-projects-featured-row's own margin-bottom sits between the row
   and the next category's heading, not between the row and this link. */
.jr-projects-category-group__cta {
  display: block;
  margin-bottom: 1rem;
}

.jr-projects-featured-row > .jr-projects-featured__card {
  flex: 2 1 0;
  min-width: 0;
  margin-bottom: 0;
}

/* A featured item with no second item in its category to pair with (only
   Gutters, currently -- single-entry category) falls back to a lone
   .jr-projects-featured__card with no sibling to share the flex row, which
   let flex-grow:2 consume the entire row width -- much wider than every
   other category's featured card (owner, 2026-09-16: should match Roofing/
   Siding's width and height, not stretch edge-to-edge). Capping it to the
   same 2/3 share the paired case already gives that column keeps every
   category's featured card the same width/height at any breakpoint,
   whether or not this one has a sibling -- the remaining third of the row
   is left empty on purpose. */
.jr-projects-featured-row:not(.jr-projects-featured-row--paired) > .jr-projects-featured__card {
  flex-grow: 0;
  flex-basis: auto;
  max-width: calc((100% - var(--jr-card-gap)) * 2 / 3);
}

.jr-projects-card.jr-projects-card--paired {
  flex: 1 1 0;
  min-width: 0;
  margin-bottom: 0;
  /* Scoped to the paired card inside the Featured Project row only --
     .jr-projects-card's base radius (--jr-radius-md, 8px) is correct for
     the plain grid below, but next to the featured card's --jr-radius-lg
     (12px) it read as a visual mismatch in an otherwise single horizontal
     composition (owner, 2026-09-16). */
  border-radius: var(--jr-radius-lg);
}

.jr-projects-card.jr-projects-card--paired .jr-projects-card__info {
  flex: 1 1 auto;
  justify-content: center;
}

/* Roofing-only paired-slot carousel (owner, 2026-09-16) -- reuses the
   generic content-slider engine (UI-017: same [data-lib-slider]/
   [data-slider-track]/[data-slider-prev]/[data-slider-next] contract as
   [jr_recent_projects] and the reviews carousel elsewhere on the site),
   just overriding .lib-content-slider__item's default partial-card width
   (min(20rem, 82vw), content-slider.css) to 100% so exactly one slide
   shows at a time. Everything below is scoped under
   .jr-projects-card--paired-carousel so it never touches the shared
   engine's other, unrelated usages. */
.jr-projects-card--paired-carousel {
  position: relative;
}

.jr-projects-card--paired-carousel .lib-content-slider__track {
  height: 100%;
  gap: 0;
  padding: 0;
  scroll-snap-type: x mandatory;
}

.jr-projects-card--paired-carousel .lib-content-slider__item {
  flex: 0 0 100%;
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  display: flex;
  flex-direction: column;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

/* .jr-projects-card > .lib-media-frame (direct-child selector, above)
   no longer reaches once the photo sits inside the carousel's <ul><li>
   wrapper -- reapplied here as a descendant rule so every slide's photo
   keeps the same capped, non-stretching size as every other card's. */
.jr-projects-card--paired-carousel .lib-media-frame {
  flex: none;
  border-radius: 0;
  max-height: 20rem;
}

.jr-projects-card--paired-carousel__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  /* !important: Elementor's site-wide kit CSS (.elementor-kit-5 button,
     loaded on every page regardless of whether Elementor built it) resets
     plain <button> padding/width to its own 20px 40px pill shape at
     higher specificity (class+tag) than this single-class selector --
     same fight cta-buttons.css already documents and solves the same way. */
  width: 2rem !important;
  height: 2rem !important;
  padding: 0 !important;
  border: 1px solid rgba(0, 0, 0, 0.15);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.92);
  color: var(--jr-color-heading);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.jr-projects-card--paired-carousel__nav:hover {
  background: var(--jr-color-primary);
  border-color: var(--jr-color-primary);
  color: var(--jr-color-text-inverse, #FFFFFF);
}

.jr-projects-card--paired-carousel__nav:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

.jr-projects-card--paired-carousel__nav--prev {
  left: 0.5rem;
}

.jr-projects-card--paired-carousel__nav--next {
  right: 0.5rem;
}

@media (max-width: 1024px) {
  /* flex-basis:0 (the desktop 2:1 split) is a HEIGHT basis once the row
     becomes a column, and align-items:flex-start becomes a WIDTH rule in
     that same flip -- together they collapsed both cards to near-zero
     height at 390px (caught live, 2026-09-15). Reset both to plain
     stacked full-width blocks instead of trying to reuse the desktop
     ratio sideways. */
  .jr-projects-featured-row {
    flex-direction: column;
    align-items: stretch;
  }

  .jr-projects-featured-row > .jr-projects-featured__card,
  .jr-projects-card.jr-projects-card--paired {
    flex: none;
  }

  .jr-projects-featured-row:not(.jr-projects-featured-row--paired) > .jr-projects-featured__card {
    max-width: none;
  }
}

/* ---- Grid ---- */
.jr-projects-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--jr-card-gap);
}

.jr-projects-card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--jr-color-surface);
  border: 1px solid var(--jr-color-border);
  border-radius: var(--jr-radius-md);
}

/* Ratio/overflow/radius come from the real .lib-media-frame[data-ratio="tall"]
   (media-frame.css, --lib-aspect-tall = 4:5) -- most curated photos are
   portrait phone shots (3:4); 4:3 (wider than tall, the first pass) forced
   an aggressive top/bottom crop on those, which read as wildly different
   zoom levels card to card (owner, 2026-09-15: "parecen de tamaños
   distintos todos"). flex:none keeps the frame from being stretched by
   its flex-column card parent. Radius zeroed here -- the outer
   .jr-projects-card already owns border-radius + overflow:hidden, so the
   frame fills flush inside it.
   Capped at 20rem -- on a wide desktop, 3 equal grid columns get wide
   enough that a pure 4:5 ratio makes each card absurdly tall (owner,
   2026-09-15: "tienen un alto absurdamente innecesario"). The ratio
   still governs on narrower columns (tablet/mobile) where width * 5/4
   stays under this cap; past that width, height just stops growing
   instead of scaling with the column. */
.jr-projects-card > .lib-media-frame {
  flex: none;
  border-radius: 0;
  max-height: 20rem;
}

.jr-projects-card .lib-media-image {
  transition: transform 0.3s ease;
}

.jr-projects-card:hover .lib-media-image {
  transform: scale(1.03);
}

.jr-projects-card__info {
  padding: 1.1rem 1.1rem 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

/* !important: same Kadence h3 margin-top collision as
   .jr-projects-featured__copy h3 above -- confirmed live on this grid
   card's own h3 too (28.5px/9.5px computed instead of the 0 this rule
   declares). */
.jr-projects-card__info h3 {
  font-family: var(--jr-font-heading);
  font-weight: 700;
  font-size: var(--jr-text-lg);
  color: var(--jr-color-heading);
  margin: 0 !important;
}

/* Copy LAW #2 requires the highlight span on every h2/h3 here too, but the
   .jr-projects-tag chip right above each title already carries the red
   accent -- doubling it onto the whole title as well reads as too much red
   in a compact card. Same precedent as .jr-services .hhds-title-highlight
   (services-grid.css): span stays for compliance, color inherits instead
   of the brand red. */
.jr-projects-card__info h3 .hhds-title-highlight {
  color: inherit;
}

/* One-line context per card (owner, 2026-09-16, page analysis: cards
   showed only a category tag + title, no line describing what the photo
   actually shows). Matches this card's existing .jr-projects-location
   color/weight pattern (already the established muted-gray body-copy
   color on this page, same as .jr-projects-category-group__description
   and .jr-projects-featured__copy p) rather than introducing a one-off
   pure-black paragraph that would look inconsistent sitting directly next
   to those. */
.jr-projects-card__blurb {
  font-family: var(--jr-font-body);
  font-size: var(--jr-text-sm);
  color: var(--jr-color-text-muted);
  line-height: var(--jr-leading-body);
  margin: 0;
}

.jr-projects-empty {
  font-family: var(--jr-font-body);
  color: var(--jr-color-text-muted);
  text-align: center;
  padding: 2rem 0 0;
  margin: 0;
}

.jr-projects-empty:not([hidden]) {
  display: block;
}

/* ---- Final CTA -- same dark-card pattern as the rest of the site's
   closing CTAs, .jr-btn-primary for the action (no new button look). ---- */
.jr-projects-final-cta {
  padding-block: var(--jr-section-y);
  background: var(--jr-color-surface);
}

.jr-projects-final-cta__inner {
  width: var(--jr-container-wide);
  margin-inline: auto;
}

.jr-projects-final-cta__card {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  background: var(--jr-color-graphite);
  border-radius: var(--jr-radius-lg);
  padding: clamp(1.75rem, 3vw, 2.75rem);
}

.jr-projects-final-cta__copy h2 {
  font-family: var(--jr-font-heading);
  font-weight: 800;
  font-size: var(--jr-text-3xl);
  color: var(--jr-color-text-inverse);
  margin: 0 0 0.5rem;
}

.jr-projects-final-cta__copy p {
  font-family: var(--jr-font-body);
  color: color-mix(in srgb, var(--jr-color-text-inverse) 85%, transparent);
  max-width: 55ch;
  margin: 0;
}

.jr-projects-final-cta__button {
  flex: none;
}

/* ---- Responsive ---- */
@media (max-width: 1024px) {
  .jr-projects-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 640px) {
  .jr-projects-grid {
    grid-template-columns: 1fr;
  }

  .jr-projects-final-cta__card {
    flex-direction: column;
    align-items: flex-start;
  }

  /* The actual mobile/desktop switch for the category selector -- pills
     hidden (not removed: same underlying [data-jr-project-filter]
     buttons projects-filters.js already wires up, just not the ones on
     screen here), trigger bar shown. */
  .jr-projects-filters {
    display: none;
  }

  .jr-projects-filter-bar {
    display: flex;
  }
}
