/* Responsive rules. The kit is authored with inline styles, so anything that has to
   change at a breakpoint carries a class hook here — media queries cannot live in a
   style attribute. Desktop values stay in the JSX; this file only overrides. */

/* Line breaks typed into the admin must survive to the page. HTML collapses a
   newline to a space, so a headline broken over two lines rendered as one long
   line. pre-line keeps the breaks and still collapses ordinary whitespace, and
   Babel already strips the newlines out of JSX source text, so indentation in
   the components cannot leak through as blank lines. */
#root{white-space:pre-line}

/* ---- page transition ----
   A full-screen panel that grows up from the floor to cover the page, and retreats
   through the ceiling to uncover the next one. Site.jsx drives it with GSAP; this
   only sets the resting state.

   Parked at scaleY(0) — open — on purpose. The intro closes it from JS on the frame
   the app mounts. Were it parked closed, a script that failed to load would leave an
   opaque panel over the whole site with nothing in the console to explain it.

   pointer-events:none so it can never swallow a click, and inset:0 rather than
   100vw/100vh, which would count the scrollbar and force a horizontal one. */
.transition-wipe{position:fixed;inset:0;z-index:var(--z-toast);pointer-events:none;
  background:var(--surface-page);transform:scaleY(0);transform-origin:bottom}
@media (prefers-reduced-motion: reduce){.transition-wipe{display:none}}

/* Scroll reveal lives entirely in Shell.jsx now — GSAP owns the hidden state so the
   page cannot be left invisible by CSS alone when a script does not run. Nothing to
   declare here, and nothing to unset under reduced motion: the hook simply never
   hides anything in that case.
   ---- */

/* ---- header ---- */
@media (max-width: 900px){
  .site-header{grid-template-columns:1fr auto!important;padding:18px 24px!important;gap:16px!important}
  .site-header>span:first-child{display:none}
}
@media (max-width: 640px){
  .site-header{grid-template-columns:1fr!important;justify-items:center;padding:16px 20px!important}
  .site-header .header-actions{justify-self:center!important}
  .site-nav{gap:20px!important;font-size:14px}
}

/* ---- hero banner ---- */
@media (max-width: 900px){
  .hero-panel{padding:56px 32px 32px!important;min-height:520px!important}
}
@media (max-width: 560px){
  .hero-panel{padding:44px 20px 28px!important;min-height:440px!important}
  .hero-meta{gap:8px 18px!important;font-size:11px!important}
}

/* ---- trusted by ----
   Client marks sit black so the row reads as one quiet band with the page, and
   give up their real colour on hover. brightness(0) crushes any ink to black while
   leaving the PNG alpha alone — no per-logo artwork needed.
   Gated on (hover:hover): touch devices get no hover, so they show full colour. */
@media (hover: hover){
  /* .58 is the knob: black over the white card lands on #6B6B6B, the neutral twin
     of --ink-600 / --text-muted, so the row sits at the same weight as muted body
     copy instead of out-shouting the headline. Lower = softer. */
  .client-mark{filter:brightness(0);opacity:.58;transition:var(--transition-surface)}
  .client-mark:hover{filter:none;opacity:1}
}

@media (max-width: 1100px){.trust-grid{grid-template-columns:repeat(4,1fr)!important}}
@media (max-width: 820px){
  .trust-section{padding:40px 32px!important}
  .trust-grid{grid-template-columns:repeat(3,1fr)!important;gap:20px 24px!important}
  .trust-grid>div{height:76px!important}
}
@media (max-width: 520px){
  .trust-section{padding:32px 20px!important}
  .trust-grid{grid-template-columns:repeat(2,1fr)!important}
}

/* ---- section inset ---- */
@media (max-width: 900px){.pad-x{padding-left:32px!important;padding-right:32px!important}}
@media (max-width: 560px){.pad-x{padding-left:20px!important;padding-right:20px!important}}

/* ---- work rails ----
   Continuous marquee. The track carries the item list TWICE (the second copy is
   .rail-half, aria-hidden, display:contents so its cards sit directly in the track
   flex). One list-width is 50% of the track plus half a gap, so translating by
   calc(-50% - 8px) lands on a pixel-identical frame and the seam is invisible.
   `reverse` makes the cards travel left-to-right; drop it to go the other way.
   Duration comes from the JSX (seconds per card) so every rail moves at one speed. */
.rail-viewport{position:relative;overflow:hidden;margin-bottom:-40px;padding-bottom:40px}
.rail-track{display:flex;gap:16px;width:max-content;animation:rail-scroll linear infinite reverse}
.rail-half{display:contents}
.rail-card{flex:0 0 clamp(200px,22vw,320px)}
.rail-viewport:hover .rail-track{animation-play-state:paused}
@keyframes rail-scroll{to{transform:translateX(calc(-50% - 8px))}}

/* Arrows sit inside the rail's own edges — the rail is full-bleed now, so the old
   negative offsets would have put them outside the shell and been clipped. Hidden
   at rest, and pointer-events off with them so they cannot swallow a card click. */
.rail-nav{position:absolute;top:calc(50% - 20px);transform:translateY(-50%);z-index:2;
  opacity:0;pointer-events:none;transition:var(--transition-control)}
.rail-nav--prev{left:20px}
.rail-nav--next{right:20px}
.rail-viewport:hover .rail-nav{opacity:1;pointer-events:auto}
/* Touch has no hover, so there they are the only control and must always show. */
@media (hover:none){.rail-nav{opacity:1;pointer-events:auto}}

/* Motion this large must respect the OS setting: park it and let the row be a
   plain overflowing strip rather than animating anyway at a slower speed. */
@media (prefers-reduced-motion: reduce){
  .rail-track{animation:none}
  .rail-viewport{overflow-x:auto}
}

@media (max-width: 640px){
  .rail-head h2{font-size:26px!important}
  .rail-head .rail-rule{display:none}
  .rail-card{flex-basis:min(72vw,300px)}
}


/* ---- services cards ---- */
@media (max-width: 1100px){.svc-grid{grid-template-columns:repeat(2,1fr)!important}}
@media (max-width: 560px){
  .svc-grid{grid-template-columns:1fr!important}
  .svc-grid>div{height:340px!important}
}

/* ---- footer ---- */
@media (max-width: 820px){
  .site-footer{padding:48px 32px!important}
  .footer-cols{grid-template-columns:repeat(2,1fr)!important;gap:32px!important}
}
@media (max-width: 520px){
  .site-footer{padding:40px 20px!important}
  .footer-cols{grid-template-columns:1fr!important}
}

/* ---- generic page sections in the other screens ---- */
@media (max-width: 900px){
  .grid-2{grid-template-columns:1fr!important}
  .grid-3{grid-template-columns:repeat(2,1fr)!important}
}
@media (max-width: 560px){
  .grid-3{grid-template-columns:1fr!important}
}

/* ---- image gallery (Ph) ----
   Arrows and dots only exist when a slot holds more than one image. They sit on the
   image and stop propagation, so the card body keeps its click-to-navigate. */
/* Cross-blur between stacked layers. Every image in the slot is mounted and
   decoded up front; only opacity and blur move, so the outgoing image blurs out
   while the incoming one blurs in and neither ever reloads mid-transition.
   A transition rather than an animation, so it runs in both directions and does
   not fire on mount — the slot loads showing its first image, still.
   Soft on purpose: --dur-reveal over --dur-slow, and --ease-in-out rather than
   --ease-out, so it drifts in and out instead of snapping. Blur stays light — at
   the midpoint both layers are half-opaque, and a heavy blur on both at once
   reads as a white haze rather than a transition. */
.ph-img{opacity:0;filter:blur(6px);
  transition:opacity var(--dur-reveal) var(--ease-in-out),filter var(--dur-reveal) var(--ease-in-out),
             transform var(--dur-reveal) var(--ease-out)}
.ph-img.is-on{opacity:1;filter:blur(0)}
/* Hover magnifies the picture you are pointing at, inside the slot's own clip.
   Only the visible layer: scaling the hidden ones would make the next cross-blur
   start from a frame that is already zoomed in. */
.ph:hover .ph-img.is-on{transform:scale(1.06)}
@media (hover:none){.ph:hover .ph-img.is-on{transform:none}}
@media (prefers-reduced-motion: reduce){.ph-img{transition:none}}

.ph-arrow{position:absolute;top:50%;transform:translateY(-50%);z-index:2;
  background:var(--white);box-shadow:var(--shadow-md);opacity:0}
/* .sph-iconbtn:active sets transform outright, which would drop the centring —
   compose the press onto the translate instead of letting it replace it. */
.ph-arrow:active{transform:translateY(-50%) var(--press-scale)}
.ph-arrow--prev{left:8px}
.ph-arrow--next{right:8px}
.ph:hover .ph-arrow{opacity:.6}
.ph-dots{position:absolute;left:0;right:0;bottom:8px;z-index:2;display:flex;gap:4px;justify-content:center}
.ph-dots i{width:5px;height:5px;border-radius:999px;background:var(--white);opacity:.5}
.ph-dots i.is-on{opacity:1;width:14px}
@media (hover:none){.ph-arrow{opacity:.6}}

/* ---- hero slideshow ---- */
.hero-dots{position:absolute;left:0;right:0;bottom:76px;z-index:3;display:flex;gap:6px;justify-content:center}
.hero-dots button{width:8px;height:8px;padding:0;border:none;border-radius:999px;cursor:pointer;
  background:rgba(255,255,255,.45);transition:var(--transition-control)}
.hero-dots button.is-on{width:28px;background:var(--white)}
/* keyed on the slide index, so React remounts it and the fade replays on change */
.hero-slide{animation:hero-fade var(--dur-slow) var(--ease-out) both}
@keyframes hero-fade{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion: reduce){.hero-slide{animation:none}}

/* The rail's way out. A text link rather than a Button because every Button in the
   kit is a pill. Hover needs a pseudo-class, so it cannot live in a style attribute. */
.see-more{display:inline-flex;align-items:center;gap:10px;background:none;border:0;
  border-bottom:1px solid var(--line-strong);padding:0 2px 6px;cursor:pointer;
  font-family:var(--font-display);font-size:var(--fs-body-md);font-weight:var(--fw-medium);
  color:var(--text-strong);transition:var(--transition-control)}
.see-more:hover{gap:16px;border-bottom-color:var(--text-strong)}

/* The work-page lightbox. Lives here rather than in a style attribute because it
   needs a media query and two pseudo-classes; the panel itself is plain flexbox. */
.lightbox{position:fixed;inset:0;z-index:var(--z-modal);display:flex;align-items:center;
  justify-content:center;padding:32px;background:rgba(23,22,20,.72);
  backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  animation:lightbox-in var(--dur-base) var(--ease-out) both}
/* The panel arrives a touch after the backdrop and from slightly below, so the
   photograph looks like it is being lifted out of the grid rather than cut to. */
@keyframes lightbox-in{from{opacity:0}to{opacity:1}}
@keyframes lightbox-panel-in{from{opacity:0;transform:translateY(16px) scale(.97)}
  to{opacity:1;transform:none}}
.lightbox-panel{position:relative;display:grid;grid-template-columns:minmax(0,1.7fr) minmax(280px,1fr);
  gap:32px;width:min(1180px,100%);max-height:88vh;padding:24px;overflow:auto;
  background:var(--surface-page);border-radius:var(--radius-panel);box-shadow:var(--shadow-lg);
  animation:lightbox-panel-in var(--dur-slow) var(--ease-out) both}
.lightbox-stage{position:relative;display:flex;align-items:center;justify-content:center;
  height:74vh;background:var(--ink-100);border-radius:var(--radius-xl);overflow:hidden}
/* Stacked, not swapped: the outgoing frame stays put and fades under the incoming
   one, so the change reads as a dissolve instead of a blink. The stage keeps a fixed
   height for the same reason — a panel that resizes to each photograph jumps. */
.lightbox-stage img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;
  opacity:0;filter:blur(8px);transform:scale(1.01);
  transition:opacity var(--dur-slow) var(--ease-out),filter var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out)}
.lightbox-stage img.is-on{opacity:1;filter:blur(0);transform:none}
.lightbox-nav{position:absolute;top:50%;transform:translateY(-50%);opacity:.6;transition:var(--transition-control)}
.lightbox-nav:hover{opacity:1}
.lightbox-nav--prev{left:12px}
.lightbox-nav--next{right:12px}
.lightbox-info{display:flex;flex-direction:column;gap:14px;padding:12px 8px 8px}
.lightbox-close{position:absolute;top:16px;right:16px}
@media (max-width:900px){
  .lightbox{padding:12px}
  .lightbox-panel{grid-template-columns:minmax(0,1fr);gap:20px;padding:16px}
  .lightbox-stage{height:52vh}
}
@media (prefers-reduced-motion:reduce){
  .lightbox,.lightbox-panel{animation:none}
  .lightbox-stage img{transition:none;filter:none;transform:none}
}

/* Grid items already stretch to the tallest card in their row, but the slack landed
   below the footer — so each 2025 sat wherever that card's own text happened to end.
   Handing the body the slack pins every footer to the bottom edge instead. */
/* 1fr rows rather than auto: a row of short cards would otherwise be shorter than
   the row above it, so "every card the same" only held within a row. */
.work-grid{align-items:stretch;grid-auto-rows:1fr}
.work-grid .sph-card__body{flex:1}

/* Footer link columns. Hover needs a pseudo-class, and the fold to one column needs
   a media query, so neither can live in a style attribute. */
.foot-link:hover{color:var(--text-strong)}
@media (max-width:900px){
  .foot-cols{grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:28px!important}
}
@media (max-width:560px){
  .foot-cols{grid-template-columns:minmax(0,1fr)!important}
}

@media (max-width:900px){
  .foot-bottom{grid-template-columns:minmax(0,1fr)!important;justify-items:start!important;gap:20px!important}
  .foot-bottom>*{justify-self:start!important}
}

/* Back-to-top. Under the lightbox (z-modal) and over everything else; hidden until
   the visitor is far enough down for it to mean anything. */
.scroll-top{position:fixed;right:28px;bottom:28px;z-index:var(--z-sticky);
  opacity:0;transform:translateY(12px);pointer-events:none;
  transition:opacity var(--dur-base) var(--ease-out),transform var(--dur-base) var(--ease-out)}
.scroll-top.is-on{opacity:1;transform:none;pointer-events:auto}
@media (max-width:640px){.scroll-top{right:16px;bottom:16px}}

/* The phone number is a call link, and says so on hover: "Call Now" unrolls from
   behind the number rather than appearing beside it, so nothing on the header row
   shifts position. max-width, not width, because the label has no fixed length — it
   is copy, and copy gets edited; the cap is far wider than "Call Now" needs so a
   longer label can never be clipped, at the cost of the last few milliseconds of
   the reveal looking slightly quick.
   aria-hidden on the label: a screen reader already announces the link as a phone
   number, and "097 074 0097 Call Now" reads as two things. */
.tel-link{display:inline-flex;align-items:center;gap:0}
.tel-link .tel-cta{max-width:0;opacity:0;overflow:hidden;white-space:nowrap;
  color:var(--text-strong);
  transition:max-width var(--dur-base) var(--ease-out),opacity var(--dur-base) var(--ease-out),
    margin-left var(--dur-base) var(--ease-out)}
/* a.tel-link, not .tel-link: base.css styles `a:hover` and that selector carries
   one more element than a bare class, so the number would take the link-accent
   colour on hover instead of this one. */
a.tel-link:hover{color:var(--text-strong)}
.tel-link:hover .tel-cta,.tel-link:focus-visible .tel-cta{max-width:12rem;opacity:1;margin-left:10px}
@media (prefers-reduced-motion:reduce){.tel-link .tel-cta{transition:none}}

/* The LINE button. The kit already darkens it on hover; this adds the movement —
   it rises a little, casts a shadow to match, and the arrow leaves in the direction
   it points, which is off the page, because that is where the link goes.
   Scoped to this one button rather than .sph-btn: every button in the kit lifting
   would be a design system change, not a hover state. */
.line-btn{transition:background var(--dur-fast) var(--ease-out),transform var(--dur-fast) var(--ease-out),box-shadow var(--dur-fast) var(--ease-out)}
.line-btn .sph-icon{transition:transform var(--dur-fast) var(--ease-out)}
.line-btn:hover{transform:translateY(-2px);box-shadow:var(--shadow-md)}
.line-btn:hover .sph-icon{transform:translate(2px,-2px)}
.line-btn:active{transform:translateY(0);box-shadow:none}
/* The kit's focus ring is blue — a browser-chrome colour that belongs to no part of
   this brand, and it lands on the button after a click, not only after a Tab. It is
   not removed, because a keyboard visitor who cannot see where they are is stranded;
   it is recoloured. Two rings rather than one: white immediately outside the button
   and ink outside that, so it stays visible whether the button sits on the white
   header or on the hero photograph. */
.line-btn:focus-visible{outline:2px solid var(--white);outline-offset:2px;
  box-shadow:0 0 0 4px var(--ink-900)}
@media (prefers-reduced-motion:reduce){
  .line-btn,.line-btn .sph-icon{transition:none}
  .line-btn:hover{transform:none}
  .line-btn:hover .sph-icon{transform:none}
}

/* The social chips are links now. The anchor must not colour the chip — base.css
   gives every a:hover the accent colour, and a chip is not a run of body text —
   and the chip itself already has the hover treatment. */
a.social-link,a.social-link:hover{color:inherit;text-decoration:none}
a.social-link:hover .sph-tag{border-color:var(--line-strong);background:var(--surface-page)}
