/*==============================================================================
  Digi Kydo — mockup overrides

  Purpose: apply the SEO / accessibility fixes the migration spec requires
  WITHOUT changing a single rendered pixel versus the live site.

  Rule for anything in this file: if it moves a pixel, it does not belong here.
  Every rule below states which live-site measurement it is pinned to.
  Load last.
==============================================================================*/

/*------------------------------------------------------------------------------
  1. HEADING SEMANTICS
  The live site ships ZERO <h1> on all 111 pages and jumps straight to <h2>.
  The mockups introduce exactly one <h1> per page and a clean H1>H2>H3 chain.
  Because the theme sizes headings through .text-style-* / component classes
  rather than through the tag, most promotions render identically with no CSS.
  These three rules cover the cases where the tag itself carried style.
------------------------------------------------------------------------------*/

/* (a) The hero slide title becomes the homepage <h1>. The element it replaced
   was a <span>, which had no margin. theme.css gives every h1-h6 a
   margin-bottom of 1.5rem — cancel it so the hero grid is untouched.
   Measured target: .slide-title box 512x109 at (64,234) @1280px. */
.dynamic-slider h1.slide-title {
  margin: 0;
}

/* (b) The 21 decorative <h3>s inside the hero slide-info panel (7 service
   names repeated across 3 slides) are demoted to <span>. .slide-info-title
   already supplies font-size/weight/colour/margin; only `display` came from
   the h3 tag. Measured target: 16px / 400 / #fff, margin 0 0 0.5rem. */
.dynamic-slider span.slide-info-title {
  display: block;
  font-family: var(--secondary-font);
  line-height: 1.2;
}

/* (b2) The hamburger was a <div> on the live site — not focusable, not
   announced, not operable by keyboard. It is now a real <button>. These
   declarations only strip native button chrome; measured box stays 68x68. */
#menu-toggle.menu-toggle {
  padding: 0;
  border: 0;
  background: none;
  -webkit-appearance: none;
  appearance: none;
}
/* menu-grid.min.css positions .menu-toggle-icon absolutely with `top:auto`,
   which resolved to 0 when its parent was a <div> full of block children.
   Inside a <button> the static position picks up a line box, pushing it 22px
   down. Pinning top/left restores the measured 1181,25 68x68 box exactly. */
#menu-toggle .menu-toggle-icon {
  top: 0;
  left: 0;
}

/* (c) Long-form article headings. The live markup used h3/h5/h6 as visual
   sizes, so the tag cannot carry semantics any more. Sizes are now set by
   class and the tag is free to be correct.
   Measured on /service/seo/ inside .shock-section .content:
     h3 -> 28px / 700 / #1a1a20 / margin 32px 0 16px
     h5 -> 20px / 700 / #1a1a20 / margin 32px 0 16px
     h6 -> 16px / 700 / #1a1a20 / margin 32px 0 16px
   All at line-height 1.2 (theme.css). */
.shock-section .content h1,
.shock-section .content h2,
.shock-section .content h3,
.shock-section .content h4,
.shock-section .content h5,
.shock-section .content h6 {
  margin-top: 2rem;
  margin-bottom: 1rem;
  font-family: var(--secondary-font);
  font-weight: 700;
  line-height: 1.2;
  color: var(--color-1);
}
/* Bootstrap 5 applies RFS to h1-h4 only. Live measurement: 28px at >=1200px,
   calc(1.3rem + 0.6vw) below that (23.05px at 375px). Reproduced exactly. */
.shock-section .content .hd-lg { font-size: calc(1.3rem + 0.6vw); } /* was h3 */
@media (min-width: 1200px) {
  .shock-section .content .hd-lg { font-size: 1.75rem; }
}
.shock-section .content .hd-md { font-size: 1.25rem; } /* was h5 */
.shock-section .content .hd-sm { font-size: 1rem; }    /* was h6 */

/* Sidebar block title: live markup used h5 (20px/700). Now h2 + .title. */
.shock-sidebar .sidebar-block .title {
  font-size: 1.25rem;
  line-height: 1.2;
}

/*------------------------------------------------------------------------------
  2. FOCUS VISIBILITY
  The live site renders no visible focus indicator anywhere. :focus-visible
  only fires for keyboard users, so pointer interaction is unchanged.
------------------------------------------------------------------------------*/
:focus-visible {
  outline: 2px solid var(--primary-color);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}
/* On the black footer and the black slide-info panel the primary blue is too
   low-contrast against #1a1a20 — use white there. */
.shock-footer :focus-visible,
.menu-grid :focus-visible,
.slide-info :focus-visible {
  outline-color: var(--white-color);
}

.skip-link {
  position: absolute;
  top: 0;
  left: -9999px;
  z-index: 9999;
  padding: 0.75rem 1.25rem;
  background-color: var(--black-color);
  color: var(--white-color);
  font-family: var(--secondary-font);
  font-size: 1rem;
  font-weight: 700;
}
.skip-link:focus {
  left: 0;
}

/* Visually hidden but crawlable / announced. Used only to complete the
   homepage <h1> string. Renders zero pixels. */
.sr-only:not(:focus):not(:active) {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/*------------------------------------------------------------------------------
  3. IMAGES — prevent layout shift.
  Every <img> in the mockups carries intrinsic width/height. Bootstrap's
  `img { max-width: 100% }` is not applied globally by this theme, so pin
  aspect-ratio behaviour explicitly where the theme sizes images by CSS.
------------------------------------------------------------------------------*/
img {
  max-width: 100%;
}
/* The logos are sized by CSS height with an intrinsic 224x120 source, so the
   width attribute must not win. Measured live: 112x60 in header and footer. */
.shock-header .navbar .navbar-brand .logo,
.shock-footer .footer-content .footer-brand .logo {
  width: auto;
}

/*------------------------------------------------------------------------------
  4. FOOTER NAP BLOCK
  The only DELIBERATE visual addition in this set: a crawlable
  name-address-phone block. The live site hides the address inside a hamburger
  menu item as lowercase "17r dover Terrace Ballygunge" with no postcode.
  Styled to match the existing footer .nav-link.has-icon rows exactly
  (16px / Lato / #fff, 0.5rem vertical padding, 1rem icon, 10px gap), so it
  reads as one more line of the Contact Us list.
------------------------------------------------------------------------------*/
.shock-footer .footer-content address.nap {
  margin: 0;
  font-style: normal;
  font-size: 1rem;
  line-height: 1.5;
}
.shock-footer .footer-content .nav-link.has-icon .icon-svg {
  flex: 0 0 auto;
  margin-top: 2px;
  margin-right: 10px;
  width: auto;
  height: 1rem;
  fill: var(--gray-color);
  stroke: var(--gray-color);
  transition: all var(--duration-base) ease;
}
.shock-footer .footer-content .nav-link.has-icon:hover .icon-svg {
  fill: var(--color);
  stroke: var(--color);
}
.shock-footer .footer-content .nav-link.has-icon.is-address {
  align-items: flex-start;
}

/*------------------------------------------------------------------------------
  5. REDUCED MOTION
  The theme runs GSAP, AOS, lax parallax, anime.js and magnetic hover, none of
  which check the user's preference. (The SVG cursor follower that used to be
  in this list was deleted on 2026-07-29 — see main.css §1 Cursor.)
------------------------------------------------------------------------------*/
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  [data-aos] {
    opacity: 1 !important;
    transform: none !important;
  }
  .floating-item,
  .floating-item-smooth,
  .circular-text .emblem,
  .zzz {
    animation: none !important;
  }
  .zzz {
    width: 100% !important;
  }
}

/*------------------------------------------------------------------------------
  6. MOCKUP-ONLY SCAFFOLDING for the three pages that had to be AUTHORED
  (blog listing, blog post, 404) because the live theme's index.php,
  single.php and 404.php are literally empty files. Every declaration here
  re-uses a value already measured elsewhere on the live site — no new
  design language is introduced.
------------------------------------------------------------------------------*/

/* Blog card: same box treatment as a portfolio thumbnail
   (.rounded 6px + .small-shadow), same hover as .hover-zoom-rotate. */
.post-card {
  display: block;
  height: 100%;
}
.post-card .image-wrapper {
  overflow: hidden;
  margin-bottom: 1.5rem;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-small);
  aspect-ratio: 16 / 9;
}
.post-card .image-wrapper .image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: all var(--duration-slow) ease-out;
}
.post-card:hover .image-wrapper .image {
  transform: scale3d(1.1, 1.1, 1) rotate(2deg);
}
.post-card .post-meta {
  margin-bottom: 0.5rem;
  font-family: var(--secondary-font);
  font-size: var(--text-small);
  font-weight: 500;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--gray-color);
}
.post-card .post-title {
  margin: 0 0 0.75rem;
  font-family: var(--secondary-font);
  font-size: var(--text-display-11); /* 19.2px / 700 — same as .text-style-11 */
  /* Reuses the .text-style-11 treatment, so it must reuse that step's tuned
     weight and tracking too — not a bare 700. With Hanken those tokens
     resolve to 700 / 0, i.e. exactly what this rule used to hard-code. */
  font-weight: var(--weight-display-11);
  letter-spacing: var(--tracking-display-11);
  line-height: 1.2;
  color: var(--black-color);
  transition: all var(--duration-base) ease;
}
.post-card:hover .post-title {
  color: var(--primary-color);
}
.post-card .post-excerpt {
  margin: 0;
  color: var(--gray-color);
}

/* Pagination: reuses the .badge outline chip, 29px tall, 4px radius. */
.pagination-list {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  justify-content: center;
  margin: 0;
  padding: 0;
  list-style: none;
}
.pagination-list li {
  margin: 0;
}
.pagination-list .page-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 40px;
  height: 40px;
  padding: 0 0.75rem;
  border: 1px solid var(--gray-50-color);
  border-radius: var(--radius-sm);
  font-family: var(--secondary-font);
  font-size: var(--text-small);
  color: var(--gray-color);
  transition: all var(--duration-base) ease;
}
.pagination-list .page-link:hover,
.pagination-list .page-link[aria-current="page"] {
  border-color: var(--secondary-color);
  background-color: var(--secondary-color);
  color: var(--white-color);
}

/* Article body on the blog post page = the exact .content treatment already
   measured on /service/seo/, so nothing new is invented. */
.post-article .post-meta {
  margin-bottom: 1.5rem;
  font-family: var(--secondary-font);
  font-size: var(--text-small);
  font-weight: 500;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--gray-color);
}
.post-article .post-hero {
  width: 100%;
  height: auto;
  margin-bottom: 2rem;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-small);
}

/* 404 — reuses the page-title lockup (.text-style-2 outline + italic solid)
   and the standard .button.outline. No new components. */
.error-page {
  text-align: center;
}
.error-page .basic-intro .title {
  justify-content: center;
}
.error-page .helpful-links {
  margin: 2rem 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  justify-content: center;
}
.error-page .helpful-links li {
  margin: 0;
}

/*------------------------------------------------------------------------------
  7. The modal close control was a bare <i> on the live site — not focusable and
  not announced. It is now a real <button> carrying the same .close-button
  class, so it keeps the measured position (absolute, top/right 1.75rem,
  1.5rem glyph). These three declarations only remove native button chrome.
------------------------------------------------------------------------------*/
.modal .modal-header button.close-button {
  border: 0;
  background: none;
  line-height: 1;
}

/* Blog card titles are headings, not body links — they must not pick up
   Bootstrap's default link blue. Colour + hover come from .post-card above,
   which reuses the measured .text-style-11 treatment (19.2px / 700 / #1a1a20,
   hover #509bbc). */
.post-card .post-title a {
  color: inherit;
}

/*------------------------------------------------------------------------------
  8. FONT SUBSTITUTION — Proxima Nova (Adobe Typekit) -> self-hosted OFL family
  Owner decision 2026-07-28: no webfont licence purchase, no Typekit.
  Owner decision 2026-07-28 (second): the display family is Montserrat.

  --secondary-font resolves to whichever VARIABLE family
  assets/fonts/display-font.css imports. Because neither substitute matches
  Proxima Nova's weight axis or its advance widths, each type-scale step gets a
  tuned wght and a tracking correction. Those values are NOT in tokens.css —
  they live with the family, in assets/fonts/montserrat.css (active) or
  assets/fonts/hanken-grotesk.css. Solved by measurement against the live
  Proxima Nova woff2 — see html/font-comparison.html.

  These rules DO move pixels, by design: they move them back toward the
  original Proxima Nova rendering. Without them the substitution is wrong.
  Layout is untouched: no width, no container, no grid changed here, and no
  font-size is changed either — apparent-size correction is done with
  size-adjust in the @font-face, so line boxes keep their authored geometry.
------------------------------------------------------------------------------*/
.text-style-1  { font-weight: var(--weight-display-1);  letter-spacing: var(--tracking-display-1); }
.text-style-2  { font-weight: var(--weight-display-2);  letter-spacing: var(--tracking-display-2); }
.text-style-3  { font-weight: var(--weight-display-3);  letter-spacing: var(--tracking-display-3); }
.text-style-4  { font-weight: var(--weight-display-4);  letter-spacing: var(--tracking-display-4); }
.text-style-5  { font-weight: var(--weight-display-5);  letter-spacing: var(--tracking-display-5); }
.text-style-6  { font-weight: var(--weight-display-6);  letter-spacing: var(--tracking-display-6); }
.text-style-7  { font-weight: var(--weight-display-7);  letter-spacing: var(--tracking-display-7); }
.text-style-8  { font-weight: var(--weight-display-8);  letter-spacing: var(--tracking-display-8); }
.text-style-9  { font-weight: var(--weight-display-9);  letter-spacing: var(--tracking-display-9); }
.text-style-10 { font-weight: var(--weight-display-10); letter-spacing: var(--tracking-display-10); }
.text-style-11 { font-weight: var(--weight-display-11); letter-spacing: var(--tracking-display-11); }
.text-style-12 { font-weight: var(--weight-display-12); letter-spacing: var(--tracking-display-12); }

/* The remaining --secondary-font type that is NOT a .text-style-* step. The
   Hanken build left these untuned because its error there was ~1%. Montserrat's
   is 13-20%, so they are tuned now; hanken-grotesk.css sets all five tracking
   values to 0 and the original Proxima weights, so swapping back reproduces
   the Hanken build exactly. */
.shock-section .content .hd-lg {
  font-weight: var(--weight-article-1);
  letter-spacing: var(--tracking-article-1);
}
.shock-section .content .hd-md {
  font-weight: var(--weight-article-2);
  letter-spacing: var(--tracking-article-2);
}
.shock-section .content .hd-sm {
  font-weight: var(--weight-article-3);
  letter-spacing: var(--tracking-article-3);
}
.shock-footer .footer-item .title {
  font-weight: var(--weight-footer-title);
  letter-spacing: var(--tracking-footer-title);
}
.menu-grid .menu-grid-minor-item,
.shock-footer .nav-list .nav-link {
  letter-spacing: var(--tracking-nav);
}

/* theme.css re-declares font-weight at <768px for .text-style-5 (800 -> 700)
   and .text-style-6 (inherits 100). The family file mirrors that shift on the
   variable axis (--weight-display-5/-6 inside its own max-width:767px block),
   so the mobile hero is not suddenly 100 units too heavy. Nothing to do here —
   the two rules above already read the responsive token. */

/*------------------------------------------------------------------------------
  9. 8px HORIZONTAL OVERFLOW AT 768px — FIXED
  Owner decision 2026-07-28: fix, do not preserve.

  Cause (measured, and it is NOT the rotated menu labels — the whole
  .menu-grid tree is clipped by overflow:hidden on .menu-grid,
  .menu-grid-item and .menu-grid-item-inner, so it contributes nothing to
  document.scrollWidth):

    core.css  @media (min-width:768px)  ->  .g-3 { --bs-gutter-x: 3rem }
              so Bootstrap's .row gets margin-left/right: -24px.
    main.css  @media (max-width:991px)  ->  .shock-footer .container
              { padding-left: 1rem; padding-right: 1rem }  i.e. 16px.

  In the 768-991px band the container is full-bleed (no auto margin slack),
  so 24px of negative row margin against 16px of container padding pushes the
  footer .row.g-3 to right: 776px on a 768px viewport. 776 - 768 = the 8px.
  Verified by hide-test: display:none on that one row drops scrollWidth
  768 <- 776 exactly.

  Fix: match the container padding to the half-gutter. Footer only — that is
  the only .row whose overflow is not already clipped by an ancestor, so it
  is the only one that reaches document.scrollWidth. Columns inside get 8px
  narrower each side in this band; nothing else moves, and the off-canvas
  menu is not touched at all.

  NOT used: overflow-x:hidden on html/body. That would mask the bug and
  would break the position:sticky sidebars driven by data-sticky /
  stickybits on the service and portfolio pages.
------------------------------------------------------------------------------*/
@media (min-width: 768px) and (max-width: 991px) {
  .shock-footer .container {
    padding-left: 1.5rem;  /* == .5 * --bs-gutter-x (3rem) for .g-3 */
    padding-right: 1.5rem;
  }
}

/*------------------------------------------------------------------------------
  10. SERVICE CARD ICONS — one stroke family.
  Pairs with resources/views/partials/service-icon.blade.php, which is the only
  place the eight glyphs are drawn. Read that file first.

  Before: eight <img> PNGs from the media library in three different
  treatments (teal line art, a solid black cart, a solid blue social glyph),
  six of the eight guessed from filenames. The owner: "the icons are not
  similar on all."

  Sizing is NOT redefined here on purpose. main.css `.card .card-image-icon`
  already sets height 50px / width auto / display block / margin-right auto,
  and editorial.css drops it to 38px inside `.ed-svc`. The SVG carries that
  same class, so the tiles' vertical rhythm is byte-identical to the PNGs it
  replaces. Only the PAINT changes below.

  main.css also sets `fill: var(--color); stroke: var(--color)` on that class,
  which would flood a stroke icon solid. Hence the .svc-icon compound
  selector (0,3,0 beats 0,2,0).

  COLOUR: --secondary-deep, #c4380f. Contrast measured, not eyeballed:
  5.36:1 on #fff and 4.83:1 on the #fdf1ea tint. WCAG 1.4.11 asks 3:1 of a
  meaningful graphic; these are decorative (aria-hidden) and still clear it
  with headroom. --secondary-color (#f95e1a) is 3.16:1 and is NOT used here —
  it is a surface tone, per the token contract in tokens.css.
------------------------------------------------------------------------------*/
.card .card-image-icon.svc-icon,
.card-image-icon.svc-icon {
  fill: none;
  stroke: currentColor;
  color: var(--secondary-deep);
  overflow: visible; /* the SEO magnifier's handle ends on the 21px gridline */
}

/* On an inverted / dark card the deep tone goes muddy; the surface tone is
   the correct one there and clears 3:1 against every dark scheme the theme
   ships. No card in the two templates this partial serves is dark today —
   this is here so that adding one cannot silently produce an invisible icon. */
.card.scheme-2 .card-image-icon.svc-icon,
.card.black .card-image-icon.svc-icon,
.bg-black .card .card-image-icon.svc-icon {
  color: var(--secondary-color);
}

/*------------------------------------------------------------------------------
  11. SCROLL-REVEAL FAIL-SAFE.
  aos.min.css sets `opacity: 0` on every [data-aos] element; something has to
  take it away, and if nothing does, the section is invisible. main.js §20
  watches for exactly that and sets this class on <html> when any [data-aos]
  element sits inside the viewport still computing opacity 0 for two seconds.

  `transition: none` is LOAD-BEARING, not tidiness. aos.min.css declares
  `transition-property: opacity, transform` on these elements, so without it
  flipping opacity here would START AN ANIMATION rather than apply a value —
  and an animation needs a rendering frame to progress. If the reason the
  section was stuck is that frames are not being produced, an animated fail-safe
  fails exactly as the thing it is rescuing. Killing the transition makes the
  reveal immediate and unconditional.

  A section that does not animate beats a section nobody can see.
------------------------------------------------------------------------------*/
html.aos-unavailable [data-aos] {
  transition: none !important;
  animation: none !important;
  opacity: 1 !important;
  transform: none !important;
}

/*------------------------------------------------------------------------------
  12. THE EIGHT SERVICE TILES ON /our-services/.
  Pairs with the `.svc-tiles` / `.svc-tile` block in
  resources/views/pages/our-services.blade.php. Read that first.

  Owner, from a screenshot: "8 is looking bad. Redesign, add or make it
  proper." Three separate defects were measured at 1273px before this block
  was written, and each declaration below answers exactly one of them:

  (a) THE SKEW. main.css:1987 `.card.double-edge .overlay::after` draws a
      second 1px frame at `transform: rotate(5deg)`. On the homepage's `.ed-svc`
      tiles that decoration is largely covered by three lines of body copy. Here
      it is not: `Page::excerpt` is empty on seven of the eight service rows, so
      seven tiles are a 40px icon, a title and a VIEW MORE row inside a 300px
      box, and the rotated ghost frame is the loudest thing in it. Measured: the
      ghost's corners sat up to 26px outside the card on every tile, which is
      what reads as "skewed / offset borders" rather than as an intentional
      double edge.

      It is NOT deleted — the double edge is the theme's own card identity and
      removing it would restyle the card out of the family. The rotation is
      replaced by a straight 8px offset, which is the same idea (two frames,
      one behind the other) with a right angle instead of 5 degrees. The
      homepage tiles are deliberately untouched: they are a different template,
      they are full of copy, and restyling an adjacent page is out of scope.

  (b) UNEQUAL HEIGHTS. Bootstrap stretches the .col, not the .card inside it,
      so a two-line title made its tile ~23px taller than its one-line
      neighbours and the row's bottom edges stepped. `height: 100%` on the card
      and a column flex body pins VIEW MORE to the bottom of every tile, so all
      four bottom edges in a row line up whatever the title does.

  (c) THE ORPHAN ROW. Handled in the Blade file (4 / 2 / 1), not here.

  COLOUR: `--border-subtle` for the resting frames and `--secondary-deep` for
  the hover state, both straight from tokens.css. No literal is written here —
  the brand orange #f95e1a is a surface tone that fails contrast as a hairline,
  and --secondary-deep (#c4380f) is the token the icons in §10 already use, so
  the tile and its icon light up in the same colour.
------------------------------------------------------------------------------*/
.svc-tiles > [class*="col-"] {
  display: flex;
}
.card.svc-tile {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  transition: transform var(--duration-fast, 0.2s) ease-out;
}
.card.svc-tile .card-body {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
}
/* Pushes VIEW MORE to the bottom of the tile whether or not there is a
   description between it and the title. `margin-top: auto` on the row rather
   than a fixed min-height on the card, so a tile that DOES get an excerpt
   later simply grows and its neighbours follow. */
.card.svc-tile .card-body .button-wrapper {
  margin-top: auto;
  padding-top: var(--space-1);
}

/* (a) The double edge, un-skewed. Same two frames, same 1px hairline, offset
   instead of rotated. main.css pins the ::after with top/left/width/height, so
   all four have to be restated: the offset frame is the card's own box moved
   8px down and right, which is why width/height go to auto and the right/bottom
   edges go negative. Setting `inset` alone does not work here — the longhands
   from main.css win on cascade order for the sides `inset` leaves at auto. */
.card.double-edge.svc-tile .overlay::before,
.card.double-edge.svc-tile .overlay::after {
  border-color: var(--border-subtle);
  transform: none;
}
.card.double-edge.svc-tile .overlay::after {
  top: 8px;
  left: 8px;
  right: -8px;
  bottom: -8px;
  width: auto;
  height: auto;
  transition: border-color var(--duration-fast, 0.2s) ease-out;
}

/* Hover / keyboard focus. `.full-link` is the real target and it covers the
   tile, so :focus-within is what carries the keyboard case. 3px of lift only —
   enough to read as interactive, small enough that it cannot reach a
   neighbouring tile's hit area across the 1.5rem gutter. */
.card.svc-tile:hover,
.card.svc-tile:focus-within {
  transform: translateY(-3px);
}
.card.double-edge.svc-tile:hover .overlay::after,
.card.double-edge.svc-tile:focus-within .overlay::after {
  border-color: var(--secondary-deep);
}

/* At the 4-up step the column is 253px at 1273 and 2.25rem of padding each
   side leaves 181px of measure, which breaks "Website Development Company in
   Kolkata" across five lines. 1.75rem gives it back 16px and keeps the tiles
   within one line of each other. Below 1200 the card is 2-up or 1-up and wide,
   so main.css's own 2.25rem is left alone. */
@media (min-width: 1200px) {
  .card.double-edge.svc-tile .card-body {
    padding: 1.75rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .card.svc-tile,
  .card.double-edge.svc-tile .overlay::after {
    transition: none;
  }
  .card.svc-tile:hover,
  .card.svc-tile:focus-within {
    transform: none;
  }
}

/*------------------------------------------------------------------------------
  13. THE OFF-CANVAS MENU CLIPS ITS OWN LINKS. Pairs with partials/nav.blade.php.

  THE BUG IS DRIVEN BY VIEWPORT HEIGHT, NOT WIDTH, which is why it survived a
  width-only sweep. Reproduced on the live site before any change, at 1024x600
  and at 1273x640 (screenshots in the handoff):

    * INDUSTRIES (item-2, 12 links) — "Healthcare" sliced through the middle at
      the top of the cell, "Personal Branding" sliced at the bottom.
    * OUR SERVICES (item-3, 8 links) — "Website Development" sliced at the
      bottom.
    * the primary links (item-1) — "About US" cut off above y=0 and "Contact"
      running past the bottom of the window.

  Three theme rules combine to produce it. menu-grid.min.css sets
  `height: 100vh` and `overflow: hidden` on .menu-grid; `overflow: hidden` again
  on .menu-grid-item and .menu-grid-item-inner; and `justify-content: center` on
  .menu-grid-main / .menu-grid-minor with `height: 100%`. Centred content inside
  an overflow:hidden box does not scroll when it is too tall — it hangs out of
  BOTH ends and is clipped at both, which is exactly what the owner photographed
  and described as items "overlapping and unreadable". The grid is 60%/40% rows,
  so INDUSTRIES gets 60vh: 384px at a 640px window, against 12 links + a label +
  70px of padding. It does not fit, so it bleeds.

  It got worse rather than appearing from nothing: the 12-hub INDUSTRIES column
  is recent, and adding Blog to the primary list this pass takes item-1 from 5
  giant links to 6. Both are legitimate content; the layout has to hold them.

  Measured at 1273x900 the menu is fine, which is why a fixed-height sweep
  missed it. The band that breaks is roughly 992px wide (where the theme stops
  hiding items 2-5) upward, at any window SHORTER than about 740px — i.e. every
  ordinary laptop with browser chrome, and a landscape iPad.

  THE FIX IS TWO INDEPENDENT LAYERS, deliberately:

    1. `justify-content: safe center` — centres exactly as before while there is
       room, and falls back to flex-start the moment content exceeds the box, so
       nothing is EVER clipped at the leading edge. Paired with `overflow-y:
       auto`, whatever still does not fit becomes scrollable instead of
       invisible. This layer is unconditional and needs no arithmetic to be
       right. A browser without `safe` drops the declaration and inherits
       today's behaviour, so it cannot make anything worse.

    2. Type that is sized against the viewport height, so layer 1 almost never
       has to fire. Both formulas below divide the space actually available by
       the number of items actually rendered — nav.blade.php writes
       --menu-main-count / --menu-minor-count from its own loops, so adding a
       seventh primary link or a thirteenth industry re-sizes the column instead
       of silently re-breaking it.

  NOTHING HERE APPLIES BELOW 992px. The theme display:nones items 2-5 there and
  the mobile menu is item-1 alone; that panel was already correct and is left
  exactly as it was.

  TAP TARGETS. editorial.css §14 raised the FOOTER and prose targets to 44px and
  that work is untouched. These desktop menu minors were never part of it — they
  measured 24px on the live site before this change — so no §14 guarantee is
  being regressed. They do get shorter at short viewports, which is stated
  plainly rather than buried: it is a pointer-driven desktop surface, and the
  alternative on offer was leaving them clipped in half.
------------------------------------------------------------------------------*/
@media (min-width: 992px) {
  /* Layer 1. The inner also needs it: in item-2 and item-3 the label is a real
     in-flow sibling of the list, so `height: 100%` on the list alone already
     overflowed the inner by 12-24px before any of this. */
  .menu-grid .menu-grid-item-inner,
  .menu-grid .menu-grid-main,
  .menu-grid .menu-grid-minor {
    justify-content: safe center;
    min-height: 0;
  }
  .menu-grid .menu-grid-main,
  .menu-grid .menu-grid-minor {
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
  }
  /* Lets the list shrink under the label instead of forcing the inner to
     overflow. max-height keeps it from ever exceeding the cell. */
  .menu-grid .menu-grid-minor {
    height: auto;
    max-height: 100%;
    flex: 0 1 auto;
  }

  /* Layer 2a — the primary links (item-1, full-height cell).
     Budget: 100dvh less the inner's 35px top and bottom padding (70px; 5rem is
     used for a little slack). Each item is one line at line-height 1 plus its
     own 1rem of padding top and bottom, hence the `- 2rem`. The theme's 5rem is
     the ceiling, so nothing changes at all on a tall window; 1.75rem is the
     floor, below which layer 1 takes over and scrolls.

     THE WIDTH TERM IS NOT OPTIONAL — the height formula alone was measured
     failing at 1024x694, where it allowed 70px and "Our Services" (the longest
     label) wrapped to two lines, blowing the six-line budget and pushing
     "Contact" past y=694. item-1 is the 50% grid column less 35px of padding
     each side; the label measures 6.2em in this face and weight (measured in
     the browser, not estimated) and carries 2rem of its own horizontal padding.
     6.4 rather than 6.2, and 7rem rather than 32px+70px, is deliberate slack for
     a real desktop scrollbar (which `vw` counts and the fixed grid does not) and
     for any font-metric drift. If it ever does wrap anyway, layer 1 scrolls it
     instead of clipping it. */
  .menu-grid .menu-grid-main-item {
    font-size: clamp(
      1.75rem,
      min(
        calc((100dvh - 5rem) / var(--menu-main-count, 6) - 2rem),
        calc((50vw - 7rem) / 6.4)
      ),
      5rem
    );
  }

  /* Layer 2b — the OUR SERVICES and INDUSTRIES lists (item-2/item-3, 60% rows).
     Budget: 60dvh less 70px of padding, less the ~24px label, less slack — 7rem
     covers all three. Divided by the real item count, less the item's own two
     margins, then divided by the line-height to get back to a font size. 1rem
     is the theme's own size and is the ceiling, so a tall window is unchanged. */
  .menu-grid .menu-grid-minor-item {
    --menu-minor-gap: clamp(1px, 0.5dvh, 5px);
    margin-block: var(--menu-minor-gap);
    line-height: 1.35;
    font-size: clamp(
      0.8rem,
      calc(
        (
          (60dvh - 7rem) / var(--menu-minor-count, 8) -
          2 * var(--menu-minor-gap)
        ) / 1.35
      ),
      1rem
    );
  }
  /* Exact parity where there is room to spare. The 1.35 leading above buys
     ~23px of fit at a 640px window, and at that size it is the difference
     between reading the list and scrolling it. On a tall window it buys nothing
     and costs 2px of link height (24 -> 22px, measured on production after the
     first pass of this deploy), so above 880px it is handed back and the rows
     are byte-identical to what shipped before this change.

     880 is not a round number chosen for looks: at 880dvh the twelve industry
     rows at 1.5 leading measure 394px against 416px of budget, and at 820dvh
     they measure 386px against 380px and would start to scroll. */
  @media (min-height: 880px) {
    .menu-grid .menu-grid-minor-item {
      line-height: 1.5;
    }
  }

  /* The social row is three icons side by side, not a stacked list; the
     vertical budget above does not describe it and would shrink it for no
     reason. main.css's own sizing is restored. */
  .menu-grid .menu-grid-icons .menu-grid-minor-item {
    font-size: 1rem;
    line-height: 1;
  }
}
