Add services page and make the portfolio carousel draggable

Port the /services page from the legacy site: hero, "Field of activity"
with the five expertise areas, the video feature image that straddles the
section boundary, the services slider, the stats row, and the capability
lists with the closing "architecture" watermark. Images come from the
legacy theme; they are also wired to Meshkee static-image slots
(services-hero/feature/cards/gallery) so they can be swapped from the panel.

The navbar Services link now points at /services instead of the homepage
anchor, and the sitemap picks the route up automatically.

PortfolioCarousel takes an optional `items` prop so the new page can reuse
it, and now supports pointer dragging. Two fixes were needed for that:

- Easing is toggled on the track imperatively with a forced style flush
  instead of via a class. Chrome decides whether to animate from the
  before-change style, so a class-based `transition: none` also suppressed
  the transition on the commit that removed it — the drag snap jumped, and
  the loop reset (which was driven by onTransitionEnd) never ran at all,
  letting the track drift off the end of the list. The loop now re-anchors
  on a timer and the reset hops instead of gliding.
- The gesture is tracked on the window rather than through pointer capture,
  so releasing outside the carousel or losing focus mid-drag still settles
  it instead of leaving the track parked at its dragged offset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
amirhosein.ashourloo
2026-09-14 12:33:19 +03:30
co-authored by Claude Opus 5
parent a8454f37f0
commit 6bcbe73d81
15 changed files with 757 additions and 35 deletions
+310 -3
View File
@@ -605,20 +605,30 @@ textarea {
--gap: 35px;
--shown: 4;
--slide: 0;
/* live pointer-drag offset, written straight to the node while dragging */
--drag: 0px;
container-type: inline-size;
width: calc(100% + 30vw);
overflow: hidden;
cursor: grab;
touch-action: pan-y;
-webkit-user-select: none;
user-select: none;
}
.portfolio-bleed.is-instant .portfolio-track {
transition: none;
.portfolio-bleed.is-dragging {
cursor: grabbing;
}
/* the transition below is switched off imperatively while the track follows the cursor and
while the loop re-anchors — see freeze()/thaw() in PortfolioCarousel */
.portfolio-track {
display: flex;
gap: var(--gap);
transition: transform 0.7s cubic-bezier(0.12, 0, 0.1, 0);
transform: translateX(calc(-1 * var(--slide) * (100cqi + var(--gap)) / var(--shown)));
transform: translateX(
calc(-1 * var(--slide) * (100cqi + var(--gap)) / var(--shown) + var(--drag))
);
}
.portfolio-card {
@@ -634,6 +644,7 @@ textarea {
height: 430px;
object-fit: cover;
display: block;
-webkit-user-drag: none;
transition: transform 1s cubic-bezier(0.12, 0, 0.39, 0);
}
@@ -1148,6 +1159,217 @@ textarea {
box-shadow: 0 25px 70px rgba(0, 0, 0, 0.45);
}
/* services page */
.service-intro {
display: grid;
grid-template-columns: 4fr 8fr;
gap: 30px;
align-items: start;
}
/* mirrors the blank line the legacy page had between the lead and the body copy */
.service-intro p + p {
margin-top: 1.875rem;
}
.service-areas {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
.service-area {
position: relative;
padding-top: 56px;
}
.service-area-number {
position: absolute;
top: -14px;
left: -4px;
font-family: var(--alt-font);
font-size: clamp(66px, 6vw, 96px);
font-weight: 800;
line-height: 1;
-webkit-text-stroke-color: rgba(255, 255, 255, 0.22);
pointer-events: none;
}
.service-area h3 {
position: relative;
margin: 0 0 6px;
color: var(--white);
font-size: 19px;
font-weight: 500;
line-height: 1.5;
}
.service-area p {
margin: 0;
}
.service-common {
grid-column: 1 / -1;
margin-top: 20px;
}
.service-common p {
margin: 6px 0 0;
}
/* the feature image straddles the boundary between the two sections */
.service-lead {
padding-bottom: 415px;
}
.service-feature-section {
padding-top: 0;
padding-bottom: 0;
}
.service-feature {
position: relative;
margin-top: -325px;
}
.service-feature-image {
height: 650px;
border-radius: 6px;
background-size: cover;
background-position: center;
box-shadow: inset 0 0 0 100vmax rgba(25, 25, 25, 0.2);
}
/* centred with margin auto rather than a transform, which `.reveal-self > *` resets */
.service-play {
position: absolute;
inset: 0;
margin: auto;
width: 170px;
height: 170px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--base-color);
color: var(--dark-gray);
font-size: 16px;
font-weight: 800;
letter-spacing: 1px;
text-transform: uppercase;
}
.service-play:hover {
background: var(--white);
}
.service-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.service-stat {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
padding: 0 20px;
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.service-stat:last-child {
border-right: 0;
}
.service-stat strong {
flex-shrink: 0;
font-family: var(--alt-font);
font-size: clamp(40px, 5vw, 58px);
font-weight: 600;
line-height: 1.1;
color: var(--white);
}
.service-stat strong sup {
font-size: 0.52em;
top: -0.5em;
}
.service-stat span {
display: block;
font-size: 18px;
line-height: 26px;
font-weight: 500;
}
.service-stars {
color: var(--base-color);
letter-spacing: 2px;
line-height: 1.4;
}
.service-outro-grid {
display: grid;
grid-template-columns: 8fr 4fr;
gap: 30px;
align-items: start;
padding-bottom: 90px;
}
.service-capabilities {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-top: 40px;
}
.service-capabilities ul {
margin: 0;
padding: 0;
list-style: none;
}
.service-capabilities li {
padding-bottom: 5px;
}
.service-capabilities li::before {
content: "+";
margin-right: 10px;
font-size: 12px;
color: var(--base-color);
}
/* nothing clips this one, so keep it clear of the footer */
.service-outro .watermark {
bottom: 0;
}
.service-visual {
position: relative;
padding-bottom: 60px;
}
.service-pic-a {
display: block;
width: 80%;
margin-left: auto;
margin-top: -20px;
box-shadow: 0 25px 70px rgba(0, 0, 0, 0.45);
}
.service-pic-b {
position: absolute;
left: 15px;
bottom: 10px;
width: 50%;
z-index: 2;
box-shadow: 0 25px 70px rgba(0, 0, 0, 0.45);
}
.service-visual.is-in .service-pic-a { transition-delay: 0.2s; }
.service-visual.is-in .service-pic-b { transition-delay: 0.45s; }
.reveal > * {
opacity: 0;
transform: translateY(30px);
@@ -1536,6 +1758,28 @@ textarea {
--shown: 3;
}
.service-intro,
.service-outro-grid {
grid-template-columns: 1fr;
}
.service-feature-image {
height: 450px;
}
.service-feature {
margin-top: -225px;
}
.service-lead {
padding-bottom: 315px;
}
.service-play {
width: 130px;
height: 130px;
}
.marquee {
letter-spacing: -5px;
}
@@ -1672,6 +1916,69 @@ textarea {
min-height: 320px;
padding-top: 130px;
}
.service-areas,
.service-stats,
.service-capabilities {
grid-template-columns: 1fr;
}
.service-areas {
gap: 24px;
}
.service-stat {
border-right: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
flex-direction: column;
gap: 4px;
text-align: center;
padding: 0 0 28px;
margin-bottom: 28px;
}
.service-stat:last-child {
border-bottom: 0;
margin-bottom: 0;
padding-bottom: 0;
}
.service-feature-image {
height: 350px;
}
.service-feature {
margin-top: -175px;
}
.service-lead {
padding-bottom: 265px;
}
.service-play {
width: 110px;
height: 110px;
font-size: 14px;
}
.service-capabilities {
gap: 0;
}
.service-visual {
display: grid;
gap: 16px;
padding-bottom: 0;
}
.service-pic-a,
.service-pic-b {
position: relative;
width: 100%;
left: auto;
bottom: auto;
margin: 0;
}
}
@media only screen and (max-width: 575px) {