-
- پیام بگذارید یا تماس بگیرید؛ تیم {name} در اولین فرصت برای هماهنگی جلسه مشاوره با شما تماس میگیرد.
-
-
-
-
تلفن
+
} label="تلفن" className="fact--phones">
{phones.map((phone) => (
-
+
{phone.display}
))}
-
+
-
-
ایمیل
+
} label="ایمیل">
{emails.map((email) => (
-
+
{email}
))}
-
+
-
+
-
- آدرس
+ } label="آدرس">
{address}
-
-
-
+
-
-
- فرم درخواست پروژه
-
+
+
+
+
+
+
>
);
}
diff --git a/src/app/globals.css b/src/app/globals.css
index 98de346..304454a 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -10,7 +10,7 @@
--gutter: clamp(20px, 5vw, 72px);
--max: 1440px;
--font-sans: var(--font-yekan), "IRANYekan", "Vazirmatn", system-ui, -apple-system, "Segoe UI", Tahoma, sans-serif;
- --font-latin: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ --font-latin: var(--font-montserrat, "Montserrat"), "Helvetica Neue", Helvetica, Arial, sans-serif;
}
*,
@@ -120,17 +120,22 @@ h3 {
.nav {
display: flex;
align-items: center;
- gap: 36px;
+ gap: 28px;
font-size: 15px;
}
-.nav a {
+.nav > a,
+.nav__trigger {
position: relative;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
padding-block: 6px;
color: var(--fg);
}
-.nav a::after {
+.nav > a::after,
+.nav__trigger::after {
content: "";
position: absolute;
inset-inline: 0;
@@ -142,11 +147,65 @@ h3 {
transition: transform 0.25s ease;
}
-.nav a:hover::after,
-.nav a[aria-current="page"]::after {
+.nav > a:hover::after,
+.nav > a[aria-current="page"]::after,
+.nav__item:hover .nav__trigger::after,
+.nav__trigger[aria-current="page"]::after {
transform: scaleX(1);
}
+.nav__caret {
+ width: 10px;
+ height: 10px;
+ opacity: 0.55;
+ transition: transform 0.2s ease;
+}
+
+.nav__item {
+ position: relative;
+}
+
+.nav__item:hover .nav__caret,
+.nav__item:focus-within .nav__caret {
+ transform: rotate(180deg);
+}
+
+.nav__menu {
+ position: absolute;
+ top: calc(100% + 10px);
+ inset-inline-start: 0;
+ min-width: 220px;
+ padding: 10px 0;
+ background: #fff;
+ border: 1px solid var(--line);
+ opacity: 0;
+ visibility: hidden;
+ transform: translateY(6px);
+ transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s ease;
+ z-index: 60;
+}
+
+.nav__item:hover .nav__menu,
+.nav__item:focus-within .nav__menu {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+}
+
+.nav__menu a {
+ display: block;
+ padding: 10px 18px;
+ font-size: 14px;
+ color: var(--fg);
+ white-space: nowrap;
+}
+
+.nav__menu a:hover,
+.nav__menu a[aria-current="page"] {
+ background: var(--surface);
+ color: var(--accent);
+}
+
.header__tools {
display: flex;
align-items: center;
@@ -154,7 +213,11 @@ h3 {
}
.header__phone {
- font-size: 14px;
+ font-family: var(--font-latin);
+ font-size: 13px;
+ font-weight: 500;
+ letter-spacing: 0.06em;
+ text-transform: none;
color: var(--muted);
direction: ltr;
}
@@ -165,31 +228,43 @@ h3 {
.header__burger {
display: none;
- width: 40px;
- height: 40px;
+ width: 44px;
+ height: 44px;
align-items: center;
justify-content: center;
+ flex: none;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ cursor: pointer;
+ color: var(--fg);
}
-.header__burger span,
-.header__burger span::before,
-.header__burger span::after {
+.header__burger span {
+ position: relative;
display: block;
width: 22px;
height: 1.5px;
- background: var(--fg);
+ background: currentColor;
+ transition: background 0.2s ease;
+}
+
+.header__burger span::before,
+.header__burger span::after {
content: "";
- position: relative;
- transition: transform 0.25s ease, opacity 0.25s ease;
+ position: absolute;
+ inset-inline: 0;
+ height: 1.5px;
+ background: currentColor;
+ transition: transform 0.25s ease;
}
.header__burger span::before {
- position: absolute;
top: -7px;
}
.header__burger span::after {
- position: absolute;
top: 7px;
}
@@ -207,12 +282,10 @@ h3 {
.mobile-nav {
display: none;
- border-top: 1px solid var(--line);
- background: var(--bg);
}
-.mobile-nav[data-open="true"] {
- display: block;
+.mobile-nav__links {
+ display: grid;
}
.mobile-nav a {
@@ -222,13 +295,66 @@ h3 {
font-size: 17px;
}
+.mobile-nav__group {
+ border-bottom: 1px solid var(--line);
+}
+
+.mobile-nav__row {
+ display: flex;
+ align-items: stretch;
+}
+
+.mobile-nav__row > a {
+ flex: 1;
+ border-bottom: 0;
+}
+
+.mobile-nav__toggle {
+ width: 52px;
+ display: grid;
+ place-items: center;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ border-inline-start: 1px solid var(--line);
+ background: transparent;
+ color: var(--fg);
+ cursor: pointer;
+}
+
+.mobile-nav__toggle svg {
+ width: 12px;
+ height: 12px;
+ transition: transform 0.2s ease;
+}
+
+.mobile-nav__group[data-open="true"] .mobile-nav__toggle svg {
+ transform: rotate(180deg);
+}
+
+.mobile-nav__sub a {
+ padding-inline-start: calc(var(--gutter) + 14px);
+ font-size: 15px;
+ color: var(--muted);
+ background: var(--surface);
+}
+
+.mobile-nav__sub a[aria-current="page"] {
+ color: var(--accent);
+}
+
.mobile-nav__meta {
- padding: 20px var(--gutter);
+ padding: 20px var(--gutter) 28px;
color: var(--muted);
font-size: 14px;
display: flex;
flex-direction: column;
- gap: 6px;
+ gap: 10px;
+}
+
+.mobile-nav__cta {
+ margin-top: 8px;
+ width: fit-content;
}
@media (max-width: 900px) {
@@ -238,11 +364,35 @@ h3 {
.nav,
.header__phone {
- display: none;
+ display: none !important;
}
.header__burger {
- display: flex;
+ display: inline-flex;
+ }
+
+ .mobile-nav {
+ display: none;
+ border-top: 1px solid var(--line);
+ background: var(--bg);
+ max-height: calc(100svh - var(--header-h));
+ overflow: auto;
+ overscroll-behavior: contain;
+ }
+
+ .mobile-nav[data-open="true"] {
+ display: block;
+ }
+
+ .header[data-nav-open="true"] {
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.06);
+ }
+}
+
+@media (min-width: 901px) {
+ .mobile-nav,
+ .mobile-nav[data-open="true"] {
+ display: none !important;
}
}
@@ -269,6 +419,148 @@ h3 {
border-top: 1px solid var(--line);
}
+/* Logo-inspired section title: latin label + geometric rail + FA heading */
+.section-title {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: clamp(20px, 3vw, 48px);
+ margin-bottom: clamp(28px, 4vw, 52px);
+}
+
+.section-title__main {
+ min-width: 0;
+ flex: 1;
+}
+
+.section-title__rail {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ margin-bottom: 16px;
+ max-width: 520px;
+}
+
+.section-title__en {
+ flex: none;
+ color: var(--muted);
+ font-size: 11px;
+ letter-spacing: 0.22em;
+ text-transform: lowercase;
+}
+
+.section-title__track {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 1;
+ min-width: 48px;
+}
+
+.section-title__line {
+ flex: 1;
+ height: 1px;
+ background: var(--fg);
+ opacity: 0.22;
+}
+
+.section-title__line--tail {
+ flex: 0 0 28px;
+}
+
+.section-title__capsule {
+ flex: none;
+ width: 18px;
+ height: 8px;
+ border: 1.5px solid var(--fg);
+ border-radius: 999px;
+ opacity: 0.55;
+}
+
+.section-title__dot {
+ flex: none;
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--accent);
+}
+
+.section-title__fa {
+ font-size: clamp(18px, 2.2vw, 28px);
+ font-weight: 400;
+ line-height: 1.55;
+ max-width: none;
+ margin: 0;
+ white-space: nowrap;
+}
+
+.section-title__fa em {
+ font-style: normal;
+ color: var(--muted);
+}
+
+.section-title__aside {
+ flex: none;
+ max-width: 34ch;
+ padding-bottom: 6px;
+}
+
+.section-title--light .section-title__en {
+ color: rgba(255, 255, 255, 0.55);
+}
+
+.section-title--light .section-title__line {
+ background: #fff;
+ opacity: 0.28;
+}
+
+.section-title--light .section-title__capsule {
+ border-color: #fff;
+ opacity: 0.55;
+}
+
+.section-title--compact {
+ margin-bottom: 20px;
+}
+
+.section-title--compact .section-title__fa {
+ font-size: clamp(18px, 2vw, 24px);
+ max-width: none;
+}
+
+.section-title--story {
+ margin-bottom: 0;
+}
+
+.section-title--story .section-title__fa {
+ font-size: clamp(15px, 1.25vw, 18px);
+ font-weight: 400;
+ line-height: 1.8;
+ max-width: none;
+ width: 100%;
+ white-space: normal;
+ text-align: justify;
+ text-align-last: right;
+}
+
+.section-title--story .section-title__rail {
+ margin-bottom: 14px;
+ max-width: 100%;
+}
+
+.section-title--light .section-title__fa {
+ color: #fff;
+}
+
+.section-title--light .section-title__fa em {
+ color: rgba(255, 255, 255, 0.55);
+}
+
+.section-title--inline {
+ margin-bottom: 0;
+}
+
+/* Legacy aliases kept for pages not yet migrated */
.eyebrow {
display: flex;
align-items: center;
@@ -297,7 +589,8 @@ h3 {
}
.lead {
- font-size: clamp(16px, 1.3vw, 19px);
+ font-size: clamp(14px, 1.1vw, 16px);
+ line-height: 1.85;
color: var(--muted);
max-width: 60ch;
}
@@ -402,34 +695,132 @@ h3 {
border-color: var(--accent);
}
-/* ---------- Hero ---------- */
+/* ---------- Hero slider (full-viewport) ---------- */
-.hero {
- padding-top: clamp(48px, 7vw, 96px);
+@keyframes hero-fade-up {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
}
-.hero__grid {
+@keyframes hero-slide-in {
+ from {
+ opacity: 0;
+ transform: scale(1.04);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+/* Reveal the white bar from the physical right edge toward the left. */
+@keyframes hero-bar-rtl {
+ from {
+ clip-path: inset(0 0 0 100%);
+ }
+ to {
+ clip-path: inset(0 0 0 0);
+ }
+}
+
+.hero-slider {
+ position: relative;
+ width: 100%;
+ background: var(--fg);
+}
+
+.hero-slider__viewport {
+ position: relative;
+ width: 100%;
+ height: calc(100svh - var(--header-h));
+ min-height: 520px;
+ overflow: hidden;
+}
+
+.hero-slider__slide {
+ position: absolute;
+ inset: 0;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 1s ease;
+}
+
+.hero-slider__slide[data-active="true"] {
+ opacity: 1;
+ pointer-events: auto;
+ z-index: 1;
+}
+
+.hero-slider__slide[data-active="true"] .hero-slider__image {
+ animation: hero-slide-in 1.15s cubic-bezier(0.22, 1, 0.36, 1) both;
+}
+
+.hero-slider__slide a {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+.hero-slider__image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.hero-slider__bar {
+ position: absolute;
+ z-index: 2;
+ inset-inline: var(--gutter);
+ bottom: clamp(20px, 3.5vw, 40px);
+ max-width: var(--max);
+ width: calc(100% - (var(--gutter) * 2));
+ margin-inline: auto;
+ background: #fff;
+ overflow: hidden;
+ clip-path: inset(0 0 0 100%);
+ /* Fast open, then a long soft settle toward the left edge. */
+ animation: hero-bar-rtl 1.05s cubic-bezier(0.05, 0.9, 0.12, 1) both;
+ animation-delay: 0.18s;
+}
+
+.hero-slider__panel {
display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- align-items: end;
- gap: 32px;
- padding-bottom: clamp(36px, 5vw, 64px);
+ grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.4fr) auto;
+ align-items: center;
+ gap: clamp(20px, 3vw, 48px);
+ padding-block: clamp(22px, 2.8vw, 36px);
+ padding-inline: clamp(20px, 3vw, 40px);
+ max-width: none;
}
-.hero__title {
- font-size: clamp(34px, 6vw, 84px);
+.hero-slider__seq {
+ --seq: 1;
+ opacity: 0;
+ animation: hero-fade-up 0.65s cubic-bezier(0.22, 1, 0.36, 1) both;
+ /* after the bar width reveal (~0.18 + 0.85), then title → lead → actions */
+ animation-delay: calc(0.7s + (var(--seq) * 0.16s));
+}
+
+.hero-slider__title {
+ font-size: clamp(22px, 2.6vw, 36px);
font-weight: 500;
- line-height: 1.25;
+ line-height: 1.35;
letter-spacing: -0.02em;
- max-width: 14ch;
+ margin: 0;
}
-.hero__title em {
+.hero-slider__title em {
font-style: normal;
color: var(--muted);
}
-.hero__dot {
+.hero-slider__dot {
display: inline-block;
width: 0.28em;
height: 0.28em;
@@ -438,133 +829,331 @@ h3 {
vertical-align: baseline;
}
-.hero__aside {
- display: flex;
- flex-direction: column;
- gap: 28px;
- align-items: flex-start;
- max-width: 340px;
- padding-bottom: 10px;
-}
-
-.hero__aside p {
+.hero-slider__lead {
color: var(--muted);
font-size: 15px;
+ line-height: 1.75;
+ margin: 0;
+ max-width: 48ch;
}
-.hero__frame {
- aspect-ratio: 16 / 9;
- max-height: 82vh;
-}
-
-.hero__caption {
+.hero-slider__actions {
display: flex;
- justify-content: space-between;
- gap: 24px;
- padding-block: 16px;
- border-bottom: 1px solid var(--line);
- color: var(--muted);
- font-size: 13px;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 14px;
+ flex: none;
+}
+
+.hero-slider__dots {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.hero-slider__dot-btn {
+ width: 22px;
+ height: 3px;
+ padding: 0;
+ background: var(--line);
+ transition: background 0.2s ease;
+}
+
+.hero-slider__dot-btn[data-active="true"],
+.hero-slider__dot-btn:hover {
+ background: var(--fg);
}
@media (max-width: 900px) {
- .hero__grid {
+ .hero-slider__panel {
grid-template-columns: 1fr;
align-items: start;
+ gap: 16px;
}
- .hero__frame {
- aspect-ratio: 4 / 3;
+ .hero-slider__actions {
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ }
+
+ .hero-slider__lead {
+ max-width: none;
+ }
+}
+
+@media (max-width: 640px) {
+ .hero-slider__viewport {
+ min-height: 560px;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .hero-slider__slide,
+ .hero-slider__slide[data-active="true"] .hero-slider__image,
+ .hero-slider__bar,
+ .hero-slider__seq {
+ animation: none !important;
+ transition: none !important;
+ opacity: 1 !important;
+ transform: none !important;
+ clip-path: none !important;
}
}
/* ---------- Intro ---------- */
+.intro {
+ display: grid;
+ /* RTL: first column lands on the right — logo is rightmost */
+ grid-template-columns: minmax(220px, 1fr) minmax(0, 0.95fr) minmax(0, 1.15fr);
+ gap: clamp(28px, 4vw, 64px);
+ align-items: stretch;
+}
+
+.intro__copy {
+ min-width: 0;
+}
+
.intro__text {
- font-size: clamp(20px, 2.2vw, 30px);
+ font-size: clamp(15px, 1.25vw, 18px);
font-weight: 400;
- line-height: 1.75;
- max-width: 30ch;
+ line-height: 1.8;
+ max-width: 28ch;
+ text-align: justify;
+ text-align-last: right;
}
.intro__meta {
display: grid;
- gap: 18px;
+ gap: 16px;
color: var(--muted);
+ font-size: clamp(14px, 1.1vw, 16px);
+ line-height: 1.85;
+ align-content: start;
+ min-width: 0;
+}
+
+.intro__meta > p {
+ margin: 0;
+ text-align: justify;
+ text-align-last: right;
}
.intro__meta strong {
display: block;
color: var(--fg);
+ font-size: clamp(15px, 1.15vw, 17px);
font-weight: 500;
}
-/* ---------- Services ---------- */
-
-.services {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- border-top: 1px solid var(--line);
- border-inline-start: 1px solid var(--line);
+.intro__meta .text-link {
+ margin-top: 8px;
+ justify-self: start;
}
-.service {
- padding: 32px 28px 40px;
- border-inline-end: 1px solid var(--line);
- border-bottom: 1px solid var(--line);
+.intro__brand {
display: flex;
- flex-direction: column;
- gap: 18px;
- min-height: 300px;
- transition: background 0.2s ease;
+ align-items: center;
+ justify-content: center;
+ align-self: stretch;
+ min-height: 100%;
}
-.service:hover {
- background: var(--surface);
-}
-
-.service__index {
- color: var(--accent);
- font-family: var(--font-latin);
- font-size: 13px;
- direction: ltr;
- text-align: right;
-}
-
-.service h3 {
- font-size: 20px;
-}
-
-.service .latin {
- color: var(--muted);
- letter-spacing: 0.14em;
-}
-
-.service p {
- color: var(--muted);
- font-size: 15px;
- margin-top: auto;
+.intro__logo {
+ width: min(100%, 380px);
+ height: auto;
+ display: block;
}
@media (max-width: 1000px) {
- .services {
- grid-template-columns: repeat(2, 1fr);
+ .intro {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .intro__brand {
+ grid-column: 1 / -1;
+ justify-content: center;
+ padding-block: 12px;
+ }
+
+ .intro__logo {
+ width: min(320px, 70vw);
}
}
-@media (max-width: 560px) {
- .services {
+@media (max-width: 640px) {
+ .intro {
grid-template-columns: 1fr;
}
- .service {
+ .intro__text {
+ max-width: none;
+ }
+
+ .intro__logo {
+ width: min(280px, 80vw);
+ }
+}
+
+/* ---------- Services (hover-expand image panels) ---------- */
+
+.service-panels {
+ display: flex;
+ height: min(68vh, 640px);
+ min-height: 420px;
+ gap: 3px;
+ background: var(--fg);
+}
+
+.service-panel {
+ position: relative;
+ flex: 1 1 0;
+ min-width: 0;
+ overflow: hidden;
+ cursor: pointer;
+ color: #fff;
+ transition: flex 0.65s cubic-bezier(0.05, 0.9, 0.12, 1);
+}
+
+.service-panel:hover,
+.service-panel:focus-within {
+ flex: 3.2 1 0;
+}
+
+.service-panel__image {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transform: scale(1.04);
+ transition: transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
+}
+
+.service-panel:hover .service-panel__image,
+.service-panel:focus-within .service-panel__image {
+ transform: scale(1);
+}
+
+.service-panel__shade {
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(
+ to top,
+ rgba(0, 0, 0, 0.78) 0%,
+ rgba(0, 0, 0, 0.28) 45%,
+ rgba(0, 0, 0, 0.08) 100%
+ );
+ transition: background 0.45s ease;
+}
+
+.service-panel:hover .service-panel__shade,
+.service-panel:focus-within .service-panel__shade {
+ background: linear-gradient(
+ to top,
+ rgba(0, 0, 0, 0.86) 0%,
+ rgba(0, 0, 0, 0.4) 55%,
+ rgba(0, 0, 0, 0.15) 100%
+ );
+}
+
+.service-panel__body {
+ position: absolute;
+ inset-inline: 0;
+ bottom: 0;
+ z-index: 1;
+ padding: clamp(18px, 2.5vw, 32px);
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+}
+
+.service-panel__index {
+ color: var(--accent);
+ font-family: var(--font-latin);
+ font-size: 12px;
+ letter-spacing: 0.14em;
+ direction: ltr;
+}
+
+.service-panel__body h3 {
+ font-size: clamp(17px, 1.6vw, 24px);
+ font-weight: 500;
+ line-height: 1.35;
+ margin: 0;
+}
+
+.service-panel__body .latin {
+ color: rgba(255, 255, 255, 0.72);
+ letter-spacing: 0.16em;
+ font-size: 10px;
+}
+
+.service-panel__desc {
+ margin: 0;
+ max-width: 36ch;
+ font-size: 14px;
+ line-height: 1.85;
+ color: rgba(255, 255, 255, 0.82);
+ opacity: 0;
+ max-height: 0;
+ overflow: hidden;
+ transform: translateY(10px);
+ transition:
+ opacity 0.4s ease 0.12s,
+ max-height 0.5s ease,
+ transform 0.45s ease 0.1s;
+}
+
+.service-panel:hover .service-panel__desc,
+.service-panel:focus-within .service-panel__desc {
+ opacity: 1;
+ max-height: 9em;
+ transform: translateY(0);
+}
+
+@media (max-width: 900px) {
+ .service-panels {
+ flex-direction: column;
+ height: auto;
min-height: 0;
+ gap: 3px;
+ }
+
+ .service-panel {
+ flex: none;
+ min-height: 200px;
+ transition: min-height 0.45s cubic-bezier(0.05, 0.9, 0.12, 1);
+ }
+
+ .service-panel:hover,
+ .service-panel:focus-within {
+ flex: none;
+ min-height: 320px;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .service-panel,
+ .service-panel__image,
+ .service-panel__desc {
+ transition: none !important;
+ }
+
+ .service-panel__desc {
+ opacity: 1;
+ max-height: none;
+ transform: none;
}
}
/* ---------- Projects ---------- */
.section-head {
+ /* Prefer SectionTitle; kept for transitional layouts */
display: flex;
align-items: flex-end;
justify-content: space-between;
@@ -572,21 +1161,62 @@ h3 {
margin-bottom: clamp(32px, 4vw, 56px);
}
+@media (max-width: 720px) {
+ .section-title {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .section-title__aside {
+ max-width: none;
+ }
+
+ .section-title__fa,
+ .page-hero .section-title__fa {
+ white-space: normal;
+ }
+}
+
.projects {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: clamp(16px, 2.5vw, 40px) clamp(16px, 2vw, 28px);
+ column-count: 3;
+ column-gap: 8px;
}
.project {
display: block;
+ break-inside: avoid;
+ -webkit-column-break-inside: avoid;
+ page-break-inside: avoid;
+ margin-bottom: 8px;
+ border: 1px solid #efeeeb;
+ border-radius: 4px;
+ background: var(--bg);
+ padding: 8px;
+ padding-bottom: 0;
+ transition: border-color 0.35s ease;
+}
+
+.project:hover {
+ border-color: var(--fg);
}
.project .frame {
- aspect-ratio: 4 / 5;
+ /* Masonry: natural image height — no forced ratio (avoids empty gap under photo) */
+ aspect-ratio: auto !important;
+ height: auto;
+ background: transparent;
+ line-height: 0;
+ overflow: hidden;
+ border-radius: 3px;
}
.project .frame img {
+ width: 100%;
+ height: auto;
+ display: block;
+ vertical-align: top;
+ object-fit: cover;
+ border-radius: 3px;
transition: transform 0.6s ease;
}
@@ -599,7 +1229,7 @@ h3 {
justify-content: space-between;
align-items: baseline;
gap: 16px;
- padding-top: 14px;
+ padding: 12px 4px 0;
}
.project__meta h3 {
@@ -614,35 +1244,953 @@ h3 {
.project__sub {
color: var(--muted);
font-size: 13px;
+ padding: 4px 4px 12px;
}
@media (max-width: 900px) {
.projects {
- grid-template-columns: repeat(2, 1fr);
+ column-count: 2;
}
}
@media (max-width: 560px) {
.projects {
+ column-count: 1;
+ }
+}
+
+/* ---------- Opening sticky frame (Mana Collection style) ---------- */
+
+.opening-track {
+ position: relative;
+ height: 260vh;
+ background: #111;
+ color: #fff;
+ view-timeline-name: --opening;
+ view-timeline-axis: block;
+}
+
+.opening-stage {
+ position: sticky;
+ top: var(--header-h);
+ z-index: 10;
+ height: calc(100svh - var(--header-h));
+ overflow: hidden;
+ background: #111;
+}
+
+.opening-media {
+ position: absolute;
+ inset: 0;
+ transform-origin: center;
+ transform: scale(1.38);
+}
+
+.opening-media img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: center 30%;
+}
+
+.opening-vignette {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background: radial-gradient(
+ circle at center,
+ transparent 28%,
+ rgba(0, 0, 0, 0.18) 68%,
+ rgba(0, 0, 0, 0.42) 100%
+ );
+}
+
+.opening-shade {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background: rgba(0, 0, 0, 0.48);
+ opacity: 0;
+}
+
+.opening-headline {
+ position: absolute;
+ inset: 0;
+ display: grid;
+ place-content: center;
+ text-align: center;
+ padding-inline: var(--gutter);
+ z-index: 1;
+}
+
+.opening-headline__eyebrow {
+ margin: 0;
+ font-family: var(--font-sans);
+ font-size: clamp(15px, 1.35vw, 18px);
+ font-weight: 500;
+ color: rgba(255, 255, 255, 0.82);
+ text-shadow: 0 2px 18px rgba(0, 0, 0, 0.45);
+}
+
+.opening-headline__title {
+ margin: 12px 0 0;
+ font-size: clamp(2.2rem, 5.8vw, 5.2rem);
+ font-weight: 500;
+ line-height: 1.15;
+ letter-spacing: -0.02em;
+ color: #f5f5f7;
+ white-space: nowrap;
+ text-shadow: 0 8px 36px rgba(0, 0, 0, 0.4);
+}
+
+.opening-headline__title em {
+ font-style: normal;
+ color: rgba(245, 245, 247, 0.58);
+}
+
+.opening-headline__mark {
+ display: inline-block;
+ width: 0.28em;
+ height: 0.28em;
+ margin-inline-start: 0.1em;
+ background: var(--accent);
+ vertical-align: baseline;
+}
+
+.opening-headline__en {
+ margin: 18px 0 0;
+ font-size: clamp(11px, 1.05vw, 13px);
+ font-weight: 400;
+ letter-spacing: 0.42em;
+ text-indent: 0.42em;
+ color: rgba(255, 255, 255, 0.62);
+ text-transform: uppercase;
+ text-shadow: 0 2px 16px rgba(0, 0, 0, 0.35);
+}
+
+.opening-copy {
+ position: absolute;
+ inset: 0;
+ z-index: 2;
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ padding: clamp(16px, 3vw, 40px) var(--gutter);
+ opacity: 0;
+ transform: translate3d(0, 28px, 0);
+ pointer-events: none;
+}
+
+/* Hom: transparent overlay + 4 icon features (not Mana big-type list) */
+.opening-panel {
+ width: min(1120px, 100%);
+ color: #fff;
+ padding: clamp(12px, 2vw, 24px) 0;
+ pointer-events: auto;
+}
+
+.opening-panel__head {
+ display: grid;
+ gap: 14px;
+ margin-bottom: clamp(22px, 3vw, 32px);
+ max-width: 52rem;
+ margin-inline: auto;
+ text-align: center;
+}
+
+.opening-panel__rail {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ max-width: 280px;
+ margin-inline: auto;
+}
+
+.opening-panel__rail .latin {
+ color: rgba(255, 255, 255, 0.65);
+ font-size: 11px;
+ letter-spacing: 0.22em;
+}
+
+.opening-panel__line {
+ flex: 1;
+ height: 1px;
+ background: rgba(255, 255, 255, 0.35);
+}
+
+.opening-panel__capsule {
+ width: 16px;
+ height: 7px;
+ border: 1.5px solid rgba(255, 255, 255, 0.55);
+ border-radius: 999px;
+}
+
+.opening-panel__dot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--accent);
+}
+
+.opening-panel__lead {
+ margin: 0;
+ font-size: clamp(14px, 1.1vw, 16px);
+ line-height: 1.85;
+ color: rgba(255, 255, 255, 0.82);
+ text-shadow: 0 2px 18px rgba(0, 0, 0, 0.35);
+}
+
+.opening-features {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 0;
+ border-top: 1px solid rgba(255, 255, 255, 0.22);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.22);
+}
+
+.opening-feature {
+ padding: clamp(18px, 2.2vw, 26px) clamp(12px, 1.5vw, 18px);
+ border-inline-end: 1px solid rgba(255, 255, 255, 0.18);
+ display: grid;
+ gap: 8px;
+ justify-items: center;
+ text-align: center;
+ align-content: start;
+ min-height: 100%;
+}
+
+.opening-feature:last-child {
+ border-inline-end: 0;
+}
+
+.opening-feature__icon {
+ width: 36px;
+ height: 36px;
+ color: #fff;
+ margin-bottom: 4px;
+}
+
+.opening-feature__icon svg {
+ width: 100%;
+ height: 100%;
+}
+
+.opening-feature h3 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 500;
+ color: #fff;
+}
+
+.opening-feature .latin {
+ color: rgba(255, 255, 255, 0.55);
+ font-size: 10px;
+ letter-spacing: 0.16em;
+}
+
+.opening-feature p {
+ margin: 0;
+ font-size: 13px;
+ line-height: 1.75;
+ color: rgba(255, 255, 255, 0.72);
+}
+
+.opening-panel__cta {
+ display: flex;
+ justify-content: center;
+ margin-top: clamp(20px, 2.5vw, 28px);
+}
+
+.opening-media,
+.opening-shade,
+.opening-headline,
+.opening-copy {
+ animation-timing-function: linear;
+ animation-fill-mode: both;
+ animation-timeline: --opening;
+ animation-duration: auto;
+}
+
+.opening-media {
+ animation-name: opening-media;
+ animation-range: cover 0% cover 68%;
+}
+
+.opening-headline {
+ animation-name: opening-headline;
+ animation-range: cover 42% cover 56%;
+}
+
+.opening-shade {
+ animation-name: opening-shade;
+ animation-range: cover 46% cover 64%;
+}
+
+.opening-copy {
+ animation-name: opening-copy, opening-copy-hit;
+ animation-timing-function: linear, step-end;
+ animation-range: cover 52% cover 70%, cover 52% cover 70%;
+}
+
+@keyframes opening-media {
+ from {
+ transform: scale(1.38);
+ }
+ to {
+ transform: scale(1.06);
+ }
+}
+
+@keyframes opening-headline {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+
+@keyframes opening-shade {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes opening-copy {
+ from {
+ opacity: 0;
+ transform: translate3d(0, 28px, 0);
+ }
+ to {
+ opacity: 1;
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes opening-copy-hit {
+ 0%,
+ 65% {
+ pointer-events: none;
+ }
+ 100% {
+ pointer-events: auto;
+ }
+}
+
+@media (max-width: 900px) {
+ .opening-headline__title {
+ white-space: normal;
+ }
+
+ .opening-features {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .opening-feature:nth-child(2) {
+ border-inline-end: 0;
+ }
+
+ .opening-feature:nth-child(1),
+ .opening-feature:nth-child(2) {
+ border-bottom: 1px solid rgba(255, 255, 255, 0.18);
+ }
+}
+
+@media (max-width: 560px) {
+ .opening-features {
+ grid-template-columns: 1fr;
+ }
+
+ .opening-feature {
+ border-inline-end: 0;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.18);
+ }
+
+ .opening-feature:last-child {
+ border-bottom: 0;
+ }
+
+ .opening-copy {
+ align-items: stretch;
+ padding-block: 12px;
+ }
+}
+
+@supports not (animation-timeline: view()) {
+ .opening-track {
+ height: auto;
+ }
+
+ .opening-stage {
+ position: relative;
+ top: auto;
+ height: calc(100svh - var(--header-h));
+ }
+
+ .opening-media,
+ .opening-shade,
+ .opening-headline,
+ .opening-copy {
+ animation: none;
+ }
+
+ .opening-media {
+ transform: scale(1.06);
+ }
+
+ .opening-shade {
+ opacity: 0.55;
+ }
+
+ .opening-headline {
+ opacity: 0;
+ }
+
+ .opening-copy {
+ opacity: 1;
+ transform: none;
+ pointer-events: auto;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .opening-track {
+ height: auto;
+ view-timeline-name: none;
+ }
+
+ .opening-stage {
+ position: relative;
+ top: auto;
+ height: min(86svh, 920px);
+ }
+
+ .opening-media,
+ .opening-shade,
+ .opening-headline,
+ .opening-copy {
+ animation: none;
+ }
+
+ .opening-media {
+ transform: scale(1.06);
+ }
+
+ .opening-shade {
+ opacity: 0.55;
+ }
+
+ .opening-headline {
+ opacity: 0;
+ }
+
+ .opening-copy {
+ opacity: 1;
+ transform: none;
+ pointer-events: auto;
+ }
+}
+
+/* ---------- Service detail hero ---------- */
+
+.service-hero {
+ position: relative;
+ min-height: min(72svh, 760px);
+ display: flex;
+ align-items: flex-end;
+ color: #fff;
+ overflow: hidden;
+ background: #111;
+}
+
+.service-hero__media {
+ position: absolute;
+ inset: 0;
+}
+
+.service-hero__media img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: center;
+}
+
+.service-hero__shade {
+ position: absolute;
+ inset: 0;
+ background:
+ linear-gradient(180deg, rgba(0, 0, 0, 0.18) 0%, rgba(0, 0, 0, 0.72) 100%),
+ linear-gradient(90deg, transparent 40%, rgba(0, 0, 0, 0.35) 100%);
+}
+
+.service-hero__content {
+ position: relative;
+ z-index: 1;
+ width: 100%;
+ padding-block: clamp(48px, 8vw, 96px) clamp(36px, 6vw, 72px);
+ display: grid;
+ gap: 16px;
+ max-width: var(--max);
+}
+
+.service-hero__index {
+ margin: 0;
+ font-size: clamp(2.4rem, 6vw, 5rem);
+ font-weight: 500;
+ letter-spacing: 0.04em;
+ line-height: 1;
+ color: rgba(255, 255, 255, 0.28);
+}
+
+.service-hero .section-title {
+ margin-bottom: 0;
+}
+
+.service-hero .section-title__fa {
+ font-size: clamp(26px, 4vw, 46px);
+ white-space: normal;
+ color: #fff;
+}
+
+.service-hero__text {
+ margin: 0;
+ max-width: 48ch;
+ font-size: clamp(14px, 1.15vw, 16px);
+ line-height: 1.85;
+ color: rgba(255, 255, 255, 0.78);
+ text-align: justify;
+ text-align-last: right;
+}
+
+/* ---------- Service sticky frame (side card — distinct from OpeningFrame) ---------- */
+
+.service-frame-track {
+ position: relative;
+ height: 240vh;
+ background: #111;
+ color: #fff;
+ view-timeline-name: --service-frame;
+ view-timeline-axis: block;
+}
+
+.service-frame-stage {
+ position: sticky;
+ top: var(--header-h);
+ z-index: 10;
+ height: calc(100svh - var(--header-h));
+ overflow: hidden;
+ background: #111;
+}
+
+.service-frame-media {
+ position: absolute;
+ inset: 0;
+ transform-origin: center;
+ transform: scale(1.32);
+}
+
+.service-frame-media img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: center;
+}
+
+.service-frame-vignette {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background: linear-gradient(90deg, rgba(0, 0, 0, 0.55) 0%, transparent 58%);
+}
+
+.service-frame-shade {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background: rgba(0, 0, 0, 0.5);
+ opacity: 0;
+}
+
+.service-frame-headline {
+ position: absolute;
+ inset: 0;
+ z-index: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ padding-inline: var(--gutter);
+ max-width: min(520px, 48vw);
+ margin-inline-start: auto;
+}
+
+.service-frame-headline__index {
+ margin: 0;
+ font-size: clamp(3rem, 8vw, 6.5rem);
+ font-weight: 500;
+ letter-spacing: 0.06em;
+ line-height: 0.9;
+ color: rgba(255, 255, 255, 0.22);
+}
+
+.service-frame-headline__title {
+ margin: 12px 0 0;
+ font-size: clamp(1.8rem, 4vw, 3.4rem);
+ font-weight: 500;
+ line-height: 1.2;
+ color: #f5f5f7;
+ text-shadow: 0 8px 36px rgba(0, 0, 0, 0.4);
+}
+
+.service-frame-headline__en {
+ margin: 14px 0 0;
+ font-size: clamp(11px, 1vw, 13px);
+ letter-spacing: 0.32em;
+ text-indent: 0.32em;
+ text-transform: uppercase;
+ color: rgba(255, 255, 255, 0.58);
+}
+
+.service-frame-copy {
+ position: absolute;
+ inset: 0;
+ z-index: 2;
+ display: flex;
+ align-items: stretch;
+ justify-content: flex-start;
+ padding: clamp(16px, 2.5vw, 32px) var(--gutter);
+ opacity: 0;
+ transform: translate3d(0, 24px, 0);
+ pointer-events: none;
+}
+
+.service-frame-card {
+ width: min(420px, 100%);
+ align-self: center;
+ background: rgba(17, 17, 17, 0.78);
+ border: 1px solid rgba(255, 255, 255, 0.16);
+ backdrop-filter: blur(14px);
+ padding: clamp(20px, 2.5vw, 28px);
+ color: #fff;
+ pointer-events: auto;
+ display: grid;
+ gap: 18px;
+}
+
+.service-frame-card__rail {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.service-frame-card__rail .latin {
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 11px;
+ letter-spacing: 0.22em;
+}
+
+.service-frame-card__line {
+ flex: 1;
+ height: 1px;
+ background: rgba(255, 255, 255, 0.28);
+}
+
+.service-frame-card__dot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--accent);
+}
+
+.service-frame-card__lead {
+ margin: 0;
+ font-size: clamp(14px, 1.1vw, 16px);
+ line-height: 1.85;
+ color: rgba(255, 255, 255, 0.82);
+ text-align: justify;
+ text-align-last: right;
+}
+
+.service-frame-steps {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: grid;
+ gap: 0;
+ border-top: 1px solid rgba(255, 255, 255, 0.16);
+}
+
+.service-frame-step {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 14px;
+ align-items: start;
+ padding-block: 14px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
+}
+
+.service-frame-step__num {
+ font-size: 13px;
+ letter-spacing: 0.08em;
+ color: rgba(255, 255, 255, 0.4);
+ padding-top: 3px;
+}
+
+.service-frame-step h3 {
+ margin: 0 0 4px;
+ font-size: 15px;
+ font-weight: 500;
+ color: #fff;
+}
+
+.service-frame-step p {
+ margin: 0;
+ font-size: 13px;
+ line-height: 1.7;
+ color: rgba(255, 255, 255, 0.62);
+}
+
+.service-frame-card__cta {
+ display: flex;
+ justify-content: flex-start;
+}
+
+.service-frame-media,
+.service-frame-shade,
+.service-frame-headline,
+.service-frame-copy {
+ animation-timing-function: linear;
+ animation-fill-mode: both;
+ animation-timeline: --service-frame;
+ animation-duration: auto;
+}
+
+.service-frame-media {
+ animation-name: service-frame-media;
+ animation-range: cover 0% cover 68%;
+}
+
+.service-frame-headline {
+ animation-name: service-frame-headline;
+ animation-range: cover 40% cover 56%;
+}
+
+.service-frame-shade {
+ animation-name: service-frame-shade;
+ animation-range: cover 44% cover 64%;
+}
+
+.service-frame-copy {
+ animation-name: service-frame-copy, service-frame-copy-hit;
+ animation-timing-function: linear, step-end;
+ animation-range: cover 50% cover 70%, cover 50% cover 70%;
+}
+
+@keyframes service-frame-media {
+ from {
+ transform: scale(1.32);
+ }
+ to {
+ transform: scale(1.05);
+ }
+}
+
+@keyframes service-frame-headline {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+
+@keyframes service-frame-shade {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes service-frame-copy {
+ from {
+ opacity: 0;
+ transform: translate3d(0, 24px, 0);
+ }
+ to {
+ opacity: 1;
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes service-frame-copy-hit {
+ 0%,
+ 65% {
+ pointer-events: none;
+ }
+ 100% {
+ pointer-events: auto;
+ }
+}
+
+.service-related {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: clamp(16px, 2vw, 28px);
+ margin-top: clamp(28px, 4vw, 48px);
+}
+
+.service-related__card {
+ display: grid;
+ gap: 12px;
+ min-width: 0;
+}
+
+.service-related__media {
+ aspect-ratio: 4 / 5;
+ overflow: hidden;
+ background: var(--surface);
+}
+
+.service-related__media img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform 0.6s ease;
+}
+
+.service-related__card:hover .service-related__media img {
+ transform: scale(1.04);
+}
+
+.service-related__meta {
+ display: grid;
+ gap: 4px;
+}
+
+.service-related__meta .latin {
+ font-size: 12px;
+ color: var(--muted);
+ letter-spacing: 0.14em;
+}
+
+.service-related__meta h3 {
+ margin: 0;
+ font-size: 17px;
+ font-weight: 500;
+}
+
+.service-related__meta p {
+ margin: 0;
+ font-size: 12px;
+ color: var(--muted);
+}
+
+@media (max-width: 900px) {
+ .service-frame-headline {
+ max-width: none;
+ margin-inline: 0;
+ align-items: center;
+ text-align: center;
+ padding-bottom: 18vh;
+ }
+
+ .service-frame-copy {
+ align-items: flex-end;
+ justify-content: center;
+ }
+
+ .service-frame-card {
+ width: min(520px, 100%);
+ }
+
+ .service-related {
grid-template-columns: 1fr;
}
}
-/* ---------- Studio ---------- */
+@supports not (animation-timeline: view()) {
+ .service-frame-track {
+ height: auto;
+ }
-.studio__frame {
- aspect-ratio: 4 / 3;
+ .service-frame-stage {
+ position: relative;
+ top: auto;
+ height: auto;
+ min-height: calc(100svh - var(--header-h));
+ }
+
+ .service-frame-media,
+ .service-frame-shade,
+ .service-frame-headline,
+ .service-frame-copy {
+ animation: none;
+ }
+
+ .service-frame-media {
+ transform: scale(1.05);
+ }
+
+ .service-frame-shade {
+ opacity: 0.55;
+ }
+
+ .service-frame-headline {
+ opacity: 0;
+ }
+
+ .service-frame-copy {
+ position: relative;
+ opacity: 1;
+ transform: none;
+ pointer-events: auto;
+ padding-block: clamp(28px, 5vw, 48px);
+ }
}
-.studio__body {
- display: flex;
- flex-direction: column;
- gap: 28px;
- align-items: flex-start;
-}
+@media (prefers-reduced-motion: reduce) {
+ .service-frame-track {
+ height: auto;
+ view-timeline-name: none;
+ }
-.studio__body p {
- color: var(--muted);
+ .service-frame-stage {
+ position: relative;
+ top: auto;
+ height: auto;
+ min-height: min(86svh, 920px);
+ }
+
+ .service-frame-media,
+ .service-frame-shade,
+ .service-frame-headline,
+ .service-frame-copy {
+ animation: none;
+ }
+
+ .service-frame-media {
+ transform: scale(1.05);
+ }
+
+ .service-frame-shade {
+ opacity: 0.55;
+ }
+
+ .service-frame-headline {
+ opacity: 0;
+ }
+
+ .service-frame-copy {
+ position: relative;
+ opacity: 1;
+ transform: none;
+ pointer-events: auto;
+ }
}
/* ---------- Process ---------- */
@@ -655,25 +2203,48 @@ h3 {
}
.process__item {
+ position: relative;
border-top: 1px solid rgba(255, 255, 255, 0.22);
padding-top: 22px;
display: grid;
gap: 12px;
+ overflow: hidden;
+ isolation: isolate;
}
-.process__step {
- color: var(--accent);
- font-size: 14px;
+.process__num {
+ position: absolute;
+ inset-inline-end: -0.05em;
+ top: 0.15em;
+ z-index: 0;
+ /* Force Montserrat — IranYekan remaps ASCII 1–9 to Persian glyphs (0 often stays Latin) */
+ font-family: var(--font-montserrat, "Montserrat"), "Montserrat", sans-serif;
+ font-size: clamp(4.5rem, 8vw, 7.5rem);
+ font-weight: 500;
+ line-height: 0.85;
+ letter-spacing: -0.04em;
+ text-transform: none;
+ color: #fff;
+ opacity: 0.07;
+ pointer-events: none;
+ user-select: none;
+ unicode-bidi: isolate;
+}
+
+.process__item h3,
+.process__item p {
+ position: relative;
+ z-index: 1;
}
.process__item h3 {
- font-size: 19px;
+ font-size: clamp(15px, 1.25vw, 18px);
color: #fff;
}
.process__item p {
color: rgba(255, 255, 255, 0.66);
- font-size: 15px;
+ font-size: clamp(14px, 1.1vw, 16px);
}
@media (max-width: 900px) {
@@ -690,23 +2261,47 @@ h3 {
/* ---------- Stats ---------- */
+.stats-band {
+ width: 100%;
+ border-block: 1px solid var(--line);
+ background-color: var(--surface);
+ background-image:
+ linear-gradient(rgba(17, 17, 17, 0.035) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(17, 17, 17, 0.035) 1px, transparent 1px);
+ background-size: 28px 28px;
+ background-position: center;
+}
+
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
- border-top: 1px solid var(--line);
+ width: 100%;
+ max-width: var(--max);
+ margin-inline: auto;
+ padding-inline: var(--gutter);
}
.stat {
- padding: 32px 0;
- border-bottom: 1px solid var(--line);
+ padding: clamp(36px, 5vw, 56px) 16px;
display: grid;
- gap: 4px;
+ gap: 8px;
+ justify-items: center;
+ text-align: center;
+ border-inline-start: 1px solid rgba(17, 17, 17, 0.08);
+}
+
+.stat:first-child {
+ border-inline-start: 0;
}
.stat strong {
+ font-family: var(--font-montserrat, "Montserrat"), "Montserrat", sans-serif;
font-size: clamp(30px, 3.5vw, 46px);
font-weight: 500;
line-height: 1.2;
+ letter-spacing: 0.04em;
+ text-transform: none;
+ unicode-bidi: isolate;
}
.stat span {
@@ -717,23 +2312,85 @@ h3 {
@media (max-width: 720px) {
.stats {
grid-template-columns: repeat(2, 1fr);
- gap: 0 24px;
+ }
+
+ .stat:nth-child(2n + 1) {
+ border-inline-start: 0;
+ }
+
+ .stat:nth-child(n + 3) {
+ border-top: 1px solid rgba(17, 17, 17, 0.08);
}
}
/* ---------- CTA ---------- */
-.cta {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 32px;
- flex-wrap: wrap;
+.cta-band {
+ position: relative;
+ overflow: hidden;
+ background:
+ linear-gradient(135deg, rgba(227, 0, 43, 0.14) 0%, transparent 42%),
+ linear-gradient(0deg, #141414, #111);
+ color: #fff;
+ padding-block: clamp(48px, 7vw, 88px);
+ border-block: 1px solid rgba(255, 255, 255, 0.08);
}
-.cta h2 {
- font-size: clamp(24px, 3vw, 40px);
+.cta-band__glow {
+ position: absolute;
+ inset-inline-start: -12%;
+ bottom: -45%;
+ width: min(560px, 70vw);
+ height: min(560px, 70vw);
+ border-radius: 50%;
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.07) 0%, transparent 68%);
+ pointer-events: none;
+}
+
+.cta-band::after {
+ content: "HOM";
+ position: absolute;
+ inset-inline-end: var(--gutter);
+ top: 50%;
+ transform: translateY(-50%);
+ font-family: var(--font-latin);
+ font-size: clamp(4.5rem, 14vw, 11rem);
+ font-weight: 600;
+ letter-spacing: 0.08em;
+ line-height: 1;
+ color: rgba(255, 255, 255, 0.04);
+ pointer-events: none;
+ user-select: none;
+}
+
+.cta {
+ position: relative;
+ z-index: 1;
+ display: block;
+}
+
+.cta .section-title {
+ align-items: center;
+ margin-bottom: 0;
+ gap: clamp(24px, 4vw, 56px);
+}
+
+.cta .section-title__fa {
+ font-size: clamp(20px, 2.6vw, 32px);
max-width: 22ch;
+ white-space: normal;
+ line-height: 1.45;
+}
+
+.cta .section-title__aside {
+ max-width: none;
+}
+
+@media (max-width: 720px) {
+ .cta-band::after {
+ opacity: 0.7;
+ inset-inline-end: 4%;
+ }
}
/* ---------- Page hero (about / contact) ---------- */
@@ -743,11 +2400,17 @@ h3 {
padding-bottom: clamp(32px, 5vw, 64px);
}
-.page-hero h1 {
- font-size: clamp(32px, 5vw, 64px);
- font-weight: 500;
- line-height: 1.3;
- max-width: 16ch;
+.page-hero h1,
+.page-hero .section-title__fa {
+ font-size: clamp(22px, 3vw, 36px);
+ font-weight: 400;
+ line-height: 1.4;
+ max-width: none;
+ white-space: nowrap;
+}
+
+.page-hero .section-title__rail {
+ max-width: 420px;
}
.page-hero__frame {
@@ -755,29 +2418,69 @@ h3 {
max-height: 70vh;
}
+@media (max-width: 900px) {
+ .page-hero .section-title__fa,
+ .contact-hero .section-title__fa {
+ white-space: normal;
+ }
+
+ .header__tools {
+ gap: 12px;
+ }
+}
+
/* ---------- About ---------- */
.about-text {
display: grid;
- gap: 20px;
- font-size: clamp(16px, 1.3vw, 19px);
+ gap: 16px;
+ font-size: clamp(14px, 1.1vw, 16px);
+ line-height: 1.85;
color: var(--muted);
}
+.about-text p {
+ text-align: justify;
+ text-align-last: right;
+}
+
.about-text p:first-child {
color: var(--fg);
- font-size: clamp(20px, 2vw, 28px);
- line-height: 1.7;
+ font-size: clamp(15px, 1.25vw, 18px);
+ line-height: 1.8;
+}
+
+.about-text ul {
+ margin: 0;
+ padding-inline-start: 18px;
+ display: grid;
+ gap: 8px;
+ font-size: inherit;
+}
+
+.about-text li {
+ text-align: justify;
+ text-align-last: right;
+}
+
+.about-text li strong {
+ color: var(--fg);
+ font-weight: 500;
}
.about-gallery {
display: grid;
- grid-template-columns: repeat(3, 1fr);
+ grid-template-columns: repeat(2, 1fr);
gap: clamp(12px, 2vw, 28px);
}
+.about-gallery--pair {
+ grid-template-columns: 1fr;
+ max-width: 920px;
+}
+
.about-gallery .frame {
- aspect-ratio: 1 / 1;
+ width: 100%;
}
@media (max-width: 720px) {
@@ -786,6 +2489,34 @@ h3 {
}
}
+.brands__grid {
+ list-style: none;
+ margin: clamp(28px, 4vw, 48px) 0 0;
+ padding: 0;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+ gap: clamp(16px, 2.5vw, 32px);
+ align-items: center;
+}
+
+.brands__logo {
+ background: transparent;
+}
+
+.brands__logo img {
+ object-fit: contain;
+ padding: 12px;
+}
+
+.brands__caption {
+ display: grid;
+ gap: 4px;
+ margin: 10px 0 0;
+ text-align: center;
+ font-size: 13px;
+ color: var(--muted);
+}
+
.values {
display: grid;
grid-template-columns: repeat(3, 1fr);
@@ -794,22 +2525,47 @@ h3 {
}
.value {
- border-top: 1px solid var(--line-strong);
+ border-top: 1px solid var(--line);
padding-top: 20px;
+ display: flex;
+ align-items: stretch;
+ gap: 18px;
+}
+
+.value__icon {
+ flex: none;
+ align-self: stretch;
+ color: var(--fg);
+}
+
+.value__icon svg {
+ display: block;
+ height: 100%;
+ width: auto;
+ aspect-ratio: 1;
+}
+
+.value__body {
display: grid;
gap: 10px;
+ min-width: 0;
+ flex: 1;
}
.value h3 {
+ margin: 0;
font-size: 19px;
}
.value p {
+ margin: 0;
color: var(--muted);
font-size: 15px;
+ text-align: justify;
+ text-align-last: right;
}
-@media (max-width: 720px) {
+@media (max-width: 900px) {
.values {
grid-template-columns: 1fr;
}
@@ -817,60 +2573,183 @@ h3 {
/* ---------- Contact ---------- */
+.contact-hero {
+ position: relative;
+ overflow: hidden;
+ background:
+ linear-gradient(160deg, rgba(227, 0, 43, 0.06) 0%, transparent 42%),
+ linear-gradient(0deg, var(--surface), var(--surface));
+ border-bottom: 1px solid var(--line);
+ padding-top: clamp(48px, 7vw, 88px);
+ padding-bottom: clamp(36px, 5vw, 64px);
+}
+
+.contact-hero__glow {
+ position: absolute;
+ inset-inline-end: -8%;
+ top: -30%;
+ width: min(520px, 55vw);
+ height: min(520px, 55vw);
+ border-radius: 50%;
+ background: radial-gradient(circle, rgba(17, 17, 17, 0.06) 0%, transparent 68%);
+ pointer-events: none;
+}
+
+.contact-hero__inner {
+ position: relative;
+ z-index: 1;
+}
+
+.contact-hero .section-title__fa {
+ font-size: clamp(24px, 3.4vw, 40px);
+ font-weight: 400;
+ line-height: 1.35;
+ max-width: none;
+ white-space: nowrap;
+}
+
+.contact-hero .section-title__rail {
+ max-width: 420px;
+}
+
+.contact-hero .section-title__aside {
+ max-width: 42ch;
+}
+
+.contact-hero__lead {
+ margin: 0;
+ font-size: clamp(14px, 1.2vw, 16px);
+ line-height: 1.85;
+ color: var(--muted);
+ text-align: justify;
+ text-align-last: right;
+}
+
+.contact-map {
+ position: relative;
+ width: 100%;
+ background: #111;
+ border-block: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.contact-map--dark .contact-map__frame {
+ filter: invert(1) hue-rotate(180deg) brightness(0.92) contrast(0.92) saturate(0.55);
+}
+
+.contact-map__frame {
+ display: block;
+ width: 100%;
+ height: clamp(280px, 42vw, 460px);
+ border: 0;
+}
+
+.contact-map__frame--google {
+ background: #1d1d1d;
+}
+
+.contact-map__open {
+ position: absolute;
+ inset-inline-start: var(--gutter);
+ bottom: 16px;
+ z-index: 1;
+ padding: 8px 14px;
+ background: #111;
+ border: 1px solid rgba(255, 255, 255, 0.28);
+ font-size: 13px;
+ color: #fff;
+ transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease;
+}
+
+.contact-map__open:hover {
+ border-color: #fff;
+ background: #fff;
+ color: #111;
+}
+
.contact-grid {
display: grid;
- grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
+ grid-template-columns: minmax(0, 4.5fr) minmax(0, 7.5fr);
gap: clamp(32px, 6vw, 96px);
align-items: start;
}
.contact-facts {
display: grid;
- gap: 28px;
+ gap: 0;
}
.fact {
+ display: flex;
+ align-items: center;
+ gap: 16px;
border-top: 1px solid var(--line);
- padding-top: 16px;
- display: grid;
- gap: 4px;
+ padding-block: 18px;
}
-.fact span {
- color: var(--muted);
- font-size: 13px;
+.fact:last-child {
+ border-bottom: 1px solid var(--line);
+}
+
+.fact__icon {
+ flex: none;
+ width: 22px;
+ height: 22px;
+ color: var(--fg);
+}
+
+.fact__icon svg {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+.fact__body {
+ display: grid;
+ gap: 4px;
+ min-width: 0;
}
.fact a,
.fact strong {
- font-size: 17px;
+ font-size: 16px;
font-weight: 500;
}
+.fact a.latin {
+ font-family: var(--font-latin);
+ font-size: 15px;
+ font-weight: 500;
+ letter-spacing: 0.06em;
+ text-transform: none;
+}
+
.fact a:hover {
color: var(--accent);
}
+.fact a[dir="ltr"] {
+ text-align: right;
+}
+
.fact--phones a {
display: block;
direction: ltr;
text-align: right;
}
-.contact-frame {
- aspect-ratio: 4 / 5;
- max-height: 520px;
+.contact-form-panel {
+ min-width: 0;
}
.form {
display: grid;
- gap: 20px;
+ gap: 14px;
}
.form__row {
display: grid;
grid-template-columns: 1fr 1fr;
- gap: 20px;
+ gap: 14px;
}
@media (max-width: 640px) {
@@ -880,38 +2759,70 @@ h3 {
}
.field {
- display: grid;
- gap: 8px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ min-width: 0;
+ background: var(--surface);
+ border: 1px solid var(--line);
+ padding: 0 14px;
+ transition: border-color 0.2s ease, background-color 0.2s ease;
}
-.field label {
- font-size: 14px;
+.field--wide {
+ align-items: flex-start;
+ padding-block: 12px;
+}
+
+.field:focus-within {
+ border-color: var(--fg);
+ background: var(--bg);
+}
+
+.field__icon {
+ flex: none;
+ width: 18px;
+ height: 18px;
+ margin-top: 2px;
color: var(--muted);
}
+.field__icon svg {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+.field:focus-within .field__icon {
+ color: var(--fg);
+}
+
.field input,
.field textarea {
width: 100%;
+ min-width: 0;
font: inherit;
- font-size: 16px;
+ font-size: 15px;
color: var(--fg);
background: transparent;
border: 0;
- border-bottom: 1px solid var(--line-strong);
- padding: 10px 0;
+ padding: 14px 0;
outline: none;
border-radius: 0;
- transition: border-color 0.2s ease;
-}
-
-.field input:focus,
-.field textarea:focus {
- border-color: var(--accent);
+ box-shadow: none;
}
.field textarea {
min-height: 140px;
resize: vertical;
+ line-height: 1.7;
+ padding-block: 2px 4px;
+}
+
+.field input::placeholder,
+.field textarea::placeholder {
+ color: var(--muted);
+ opacity: 0.85;
}
.form__footer {
@@ -919,6 +2830,7 @@ h3 {
align-items: center;
gap: 24px;
flex-wrap: wrap;
+ margin-top: 8px;
}
.form__status {
@@ -934,6 +2846,16 @@ h3 {
color: var(--accent);
}
+@media (max-width: 900px) {
+ .contact-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .contact-hero .section-title__fa {
+ white-space: normal;
+ }
+}
+
/* ---------- Footer ---------- */
.footer {
@@ -1036,3 +2958,634 @@ h3 {
.notfound h1 {
font-size: clamp(28px, 4vw, 48px);
}
+
+/* ---------- Instagram follow (Ayoubi-style frame) ---------- */
+
+.follow {
+ padding-block: clamp(56px, 8vw, 96px);
+ border-top: 1px solid var(--line);
+ background: var(--bg);
+}
+
+.follow__grid {
+ display: grid;
+ grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
+ direction: ltr;
+ align-items: center;
+ gap: clamp(28px, 5vw, 64px);
+}
+
+.follow__copy {
+ direction: rtl;
+ display: grid;
+ gap: 14px;
+ justify-items: start;
+}
+
+.follow__kicker {
+ margin: 0;
+ color: var(--muted);
+ font-size: 12px;
+}
+
+.follow__copy h2 {
+ margin: 0;
+ font-size: clamp(1.45rem, 2.4vw, 2rem);
+ font-weight: 500;
+ line-height: 1.45;
+}
+
+.follow__lead {
+ margin: 0;
+ max-width: 34ch;
+ color: var(--muted);
+ font-size: 15px;
+ line-height: 1.85;
+}
+
+.follow__photo {
+ position: relative;
+ justify-self: start;
+ width: min(100%, 18.5rem);
+ margin: 1.25rem 1.25rem 1.25rem 0;
+}
+
+.follow__photo::before {
+ content: "";
+ position: absolute;
+ z-index: 0;
+ top: 1.15rem;
+ right: -1.15rem;
+ bottom: -1.15rem;
+ left: 1.15rem;
+ border: 1px solid var(--accent);
+}
+
+.follow__photo .follow__frame,
+.follow__photo a {
+ position: relative;
+ z-index: 1;
+ display: block;
+}
+
+.follow__photo img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+
+@media (max-width: 900px) {
+ .follow__grid {
+ grid-template-columns: 1fr;
+ direction: rtl;
+ justify-items: center;
+ }
+
+ .follow__copy {
+ justify-items: center;
+ text-align: center;
+ }
+
+ .follow__photo {
+ justify-self: center;
+ margin: 1.25rem 0 0;
+ }
+}
+
+/* ---------- Details frame (home video split) ---------- */
+
+.details-frame {
+ padding-block: clamp(56px, 8vw, 96px);
+ border-top: 1px solid var(--line);
+ background: var(--bg);
+}
+
+.details-frame__grid {
+ display: grid;
+ grid-template-columns: minmax(0, 34rem) minmax(0, 26rem);
+ gap: clamp(20px, 3vw, 36px);
+ direction: ltr;
+ align-items: center;
+ justify-content: center;
+}
+
+.details-frame__copy {
+ direction: rtl;
+ display: grid;
+ gap: 18px;
+ justify-items: start;
+ align-content: center;
+}
+
+.details-frame__copy .section-title {
+ margin-bottom: 0;
+ width: 100%;
+}
+
+.details-frame__copy .section-title__fa {
+ white-space: normal;
+ max-width: 14ch;
+}
+
+.details-frame__body {
+ margin: 0;
+ max-width: 42ch;
+ color: var(--muted);
+ font-size: clamp(15px, 1.1vw, 17px);
+ line-height: 1.9;
+}
+
+.details-frame__photo {
+ position: relative;
+ width: 100%;
+ margin: 1.35rem 1.35rem 1.35rem 0;
+}
+
+.details-frame__photo::before {
+ content: "";
+ position: absolute;
+ z-index: 0;
+ top: 1.25rem;
+ right: -1.25rem;
+ bottom: -1.25rem;
+ left: 1.25rem;
+ border: 1px solid var(--accent);
+}
+
+.details-frame__frame {
+ position: relative;
+ z-index: 1;
+ display: block;
+ overflow: hidden;
+ background: var(--surface);
+}
+
+.details-frame__video {
+ display: block;
+ width: 100%;
+ height: auto;
+ aspect-ratio: 9 / 16;
+ object-fit: contain;
+ background: #111;
+}
+
+.details-frame__play {
+ position: absolute;
+ inset-inline-end: 1rem;
+ bottom: 1rem;
+ z-index: 2;
+ display: grid;
+ place-items: center;
+ width: 3rem;
+ height: 3rem;
+ border: 1px solid rgba(255, 255, 255, 0.55);
+ border-radius: 50%;
+ background: rgba(17, 17, 17, 0.45);
+ color: #fff;
+ cursor: pointer;
+ transition: background 0.2s ease, opacity 0.2s ease, transform 0.2s ease;
+}
+
+.details-frame__play svg {
+ width: 1.1rem;
+ height: 1.1rem;
+}
+
+.details-frame__play:hover {
+ background: rgba(17, 17, 17, 0.7);
+ transform: scale(1.04);
+}
+
+.details-frame__play--playing {
+ opacity: 0;
+}
+
+.details-frame__frame:hover .details-frame__play--playing,
+.details-frame__play--playing:focus-visible {
+ opacity: 1;
+}
+
+@media (max-width: 900px) {
+ .details-frame__grid {
+ grid-template-columns: 1fr;
+ direction: rtl;
+ justify-items: center;
+ }
+
+ .details-frame__copy {
+ order: 1;
+ width: 100%;
+ justify-items: center;
+ text-align: center;
+ }
+
+ .details-frame__copy .section-title {
+ justify-content: center;
+ }
+
+ .details-frame__copy .section-title__main {
+ display: grid;
+ justify-items: center;
+ text-align: center;
+ }
+
+ .details-frame__copy .section-title__fa {
+ max-width: none;
+ }
+
+ .details-frame__body {
+ max-width: 42ch;
+ text-align: center;
+ }
+
+ .details-frame__photo {
+ order: 2;
+ width: min(100%, 22rem);
+ justify-self: center;
+ margin: 1.25rem 0 0;
+ }
+
+ .details-frame__play--playing {
+ opacity: 0.85;
+ }
+}
+
+/* ---------- Account menu ---------- */
+
+.account-menu {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+}
+
+.account-menu--login,
+.account-menu--loading,
+.account-menu__trigger {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--fg);
+ font-size: 14px;
+ background: transparent;
+ border: 0;
+ padding: 0;
+ cursor: pointer;
+}
+
+.account-menu--loading {
+ opacity: 0.4;
+}
+
+.account-menu--login:hover,
+.account-menu__trigger:hover {
+ color: var(--accent);
+}
+
+.account-menu__name {
+ max-width: 9rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.account-menu__dropdown {
+ position: absolute;
+ top: calc(100% + 10px);
+ inset-inline-end: 0;
+ min-width: 180px;
+ background: #fff;
+ border: 1px solid var(--line);
+ z-index: 70;
+}
+
+.account-menu__item {
+ display: block;
+ width: 100%;
+ padding: 12px 16px;
+ border: 0;
+ background: transparent;
+ color: var(--fg);
+ font-size: 14px;
+ text-align: right;
+ cursor: pointer;
+}
+
+.account-menu__item:hover {
+ background: var(--surface);
+ color: var(--accent);
+}
+
+.account-menu__logout {
+ color: var(--accent);
+}
+
+/* ---------- Portfolio / blog detail ---------- */
+
+.portfolio-detail,
+.blog-detail {
+ display: grid;
+ grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
+ gap: clamp(28px, 4vw, 56px);
+ align-items: start;
+}
+
+.portfolio-detail__copy,
+.blog-detail__copy {
+ display: grid;
+ gap: 18px;
+ align-content: start;
+}
+
+.portfolio-detail__copy .lead,
+.blog-detail__copy .lead {
+ margin: 0;
+ font-size: clamp(16px, 1.3vw, 18px);
+ line-height: 1.85;
+ color: var(--muted);
+}
+
+.rich-text {
+ display: grid;
+ gap: 14px;
+ font-size: 15px;
+ line-height: 1.9;
+ color: var(--fg);
+}
+
+.rich-text p {
+ margin: 0;
+}
+
+.page-hero__aside {
+ display: grid;
+ gap: 6px;
+ margin: 0;
+ color: var(--muted);
+ font-size: 14px;
+}
+
+.projects--related {
+ margin-top: clamp(28px, 4vw, 40px);
+}
+
+.empty-state {
+ margin: 0;
+ padding: 48px 0;
+ text-align: center;
+ color: var(--muted);
+}
+
+.pager {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 16px;
+ margin-top: 40px;
+ padding-top: 20px;
+ border-top: 1px solid var(--line);
+ font-size: 14px;
+}
+
+.pager a:hover {
+ color: var(--accent);
+}
+
+.blog-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 24px;
+}
+
+.blog-card {
+ display: grid;
+ gap: 14px;
+ border: 1px solid #efeeeb;
+ padding: 8px;
+ transition: border-color 0.35s ease;
+}
+
+.blog-card:hover {
+ border-color: var(--fg);
+}
+
+.blog-card__media {
+ aspect-ratio: 4 / 3;
+}
+
+.blog-card__media img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+ transition: transform 0.6s ease;
+}
+
+.blog-card:hover .blog-card__media img {
+ transform: scale(1.03);
+}
+
+.blog-card__body {
+ display: grid;
+ gap: 8px;
+ padding: 4px 4px 10px;
+}
+
+.blog-card__date {
+ color: var(--muted);
+ font-size: 11px;
+}
+
+.blog-card h2 {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 500;
+}
+
+.blog-card p {
+ margin: 0;
+ color: var(--muted);
+ font-size: 14px;
+ line-height: 1.7;
+}
+
+.blog-detail__media {
+ aspect-ratio: 16 / 10;
+}
+
+.blog-detail__media img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+
+/* ---------- Media gallery + lightbox ---------- */
+
+.media-gallery__stage {
+ position: relative;
+ border: 1px solid var(--line);
+ line-height: 0;
+ cursor: zoom-in;
+ overflow: hidden;
+ background: var(--surface);
+}
+
+.media-gallery__stage > img {
+ display: block;
+ width: 100%;
+ height: auto;
+}
+
+.media-gallery__zoom {
+ display: none;
+ position: absolute;
+ inset: 0;
+ background-repeat: no-repeat;
+ background-size: 220%;
+ pointer-events: none;
+}
+
+@media (hover: hover) and (min-width: 901px) {
+ .media-gallery__zoom {
+ display: block;
+ }
+}
+
+.media-gallery__nav,
+.media-lightbox__nav {
+ position: absolute;
+ top: 50%;
+ inset-inline-start: 0.6rem;
+ translate: 0 -50%;
+ width: 2.2rem;
+ height: 2.2rem;
+ border: 1px solid rgba(255, 255, 255, 0.35);
+ background: rgba(0, 0, 0, 0.55);
+ color: #fff;
+ cursor: pointer;
+}
+
+.media-gallery__nav--next,
+.media-lightbox__nav--next {
+ inset-inline-start: auto;
+ inset-inline-end: 0.6rem;
+}
+
+.media-gallery__thumbs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin: 0.75rem 0 0;
+ padding: 0;
+ list-style: none;
+}
+
+.media-gallery__thumbs button {
+ width: 4rem;
+ height: 3.2rem;
+ padding: 0;
+ border: 1px solid var(--line);
+ background: var(--surface);
+ cursor: pointer;
+}
+
+.media-gallery__thumbs button.is-active {
+ border-color: var(--fg);
+}
+
+.media-gallery__thumbs img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+
+.media-lightbox {
+ position: fixed;
+ inset: 0;
+ z-index: 90;
+ display: flex;
+ flex-direction: column;
+ background: rgba(0, 0, 0, 0.94);
+}
+
+.media-lightbox__bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.media-lightbox__bar button {
+ border: 1px solid rgba(255, 255, 255, 0.35);
+ background: transparent;
+ color: #fff;
+ padding: 0.35rem 0.8rem;
+ cursor: pointer;
+}
+
+.media-lightbox__body {
+ position: relative;
+ flex: 1;
+ min-height: 0;
+}
+
+.media-lightbox__body img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+}
+
+/* ---------- Toast ---------- */
+
+.toast-stack {
+ position: fixed;
+ inset-inline: 0;
+ bottom: 1.25rem;
+ z-index: 100;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+ pointer-events: none;
+ padding-inline: 1rem;
+}
+
+.toast {
+ pointer-events: auto;
+ max-width: 22rem;
+ border: 1px solid var(--fg);
+ background: #fff;
+ color: var(--fg);
+ padding: 0.8rem 1.1rem;
+ font-size: 0.92rem;
+}
+
+.toast--success {
+ border-color: var(--fg);
+}
+
+.toast--error {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+
+@media (max-width: 900px) {
+ .portfolio-detail,
+ .blog-detail {
+ grid-template-columns: 1fr;
+ }
+
+ .blog-grid {
+ grid-template-columns: 1fr 1fr;
+ }
+
+}
+
+@media (max-width: 560px) {
+ .blog-grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index dd6d0cd..02b764c 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,9 +1,11 @@
import type { Metadata } from "next";
+import { Montserrat } from "next/font/google";
import localFont from "next/font/local";
import { CmsMeta, JsonLd } from "@/components/CmsMeta";
import { Footer } from "@/components/Footer";
import { Header } from "@/components/Header";
+import { Providers } from "@/components/Providers";
import { site } from "@/data/site";
import { siteUrl } from "@/lib/config";
import {
@@ -14,7 +16,7 @@ import {
resolveName,
resolvePhones,
} from "@/lib/content";
-import { getBusinessInfo, getFavicon } from "@/lib/meshkee";
+import { getBusinessId, getBusinessInfo, getFavicon } from "@/lib/meshkee";
import "./globals.css";
@@ -29,10 +31,24 @@ const yekan = localFont({
display: "swap",
});
+const montserrat = Montserrat({
+ subsets: ["latin"],
+ weight: ["400", "500", "600"],
+ variable: "--font-montserrat",
+ display: "swap",
+});
+
export async function generateMetadata(): Promise
{
const [favicon, info] = await Promise.all([getFavicon(), getBusinessInfo()]);
const name = resolveName(info);
- const icons = favicon.faviconUrl ? [{ url: favicon.faviconUrl, type: "image/png" }] : undefined;
+ const icons = favicon.faviconUrl
+ ? [{ url: favicon.faviconUrl, type: "image/png" }]
+ : [{ url: "/icon.png", type: "image/png" }];
+ const otherMeta = Object.fromEntries(
+ (info.metaTags ?? [])
+ .filter((tag) => tag.name && tag.content != null && !tag.html)
+ .map((tag) => [tag.name as string, tag.content as string]),
+ );
return {
metadataBase: new URL(siteUrl),
@@ -42,6 +58,7 @@ export async function generateMetadata(): Promise {
},
description: site.description,
icons,
+ ...(Object.keys(otherMeta).length ? { other: otherMeta } : {}),
openGraph: {
type: "website",
locale: "fa_IR",
@@ -54,7 +71,7 @@ export async function generateMetadata(): Promise {
}
export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
- const info = await getBusinessInfo();
+ const [info, businessId] = await Promise.all([getBusinessInfo(), getBusinessId()]);
const name = resolveName(info);
const logo = resolveLogo(info);
const phones = resolvePhones(info);
@@ -62,23 +79,25 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
const jsonLd = info.schema?.enabled ? info.schema.jsonLd : null;
return (
-
+
-
-
- {children}
-
+
+
+
+ {children}
+
+
);
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
index 7bf474e..b85ebcd 100644
--- a/src/app/not-found.tsx
+++ b/src/app/not-found.tsx
@@ -1,6 +1,8 @@
import type { Metadata } from "next";
import Link from "next/link";
+import { SectionTitle } from "@/components/SectionTitle";
+
export const metadata: Metadata = {
title: "صفحه پیدا نشد",
description: "صفحهای که دنبال آن بودید وجود ندارد.",
@@ -10,10 +12,7 @@ export const metadata: Metadata = {
export default function NotFound() {
return (
-
- ۴۰۴
-
- صفحه مورد نظر پیدا نشد
+
احتمالاً آدرس اشتباه است یا این صفحه جابهجا شده.
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 7f5583e..ae74896 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,9 +1,14 @@
import type { Metadata } from "next";
import Link from "next/link";
+import { DetailsFrame } from "@/components/DetailsFrame";
+import { HeroSlider } from "@/components/HeroSlider";
+import { InstagramFollow } from "@/components/InstagramFollow";
+import { OpeningFrame } from "@/components/OpeningFrame";
+import { SectionTitle } from "@/components/SectionTitle";
import { SlotImage } from "@/components/SlotImage";
import { gallery, processSteps, projects, services, site, stats } from "@/data/site";
-import { resolveAbout, resolveName } from "@/lib/content";
+import { resolveAbout, resolveInstagram, resolveLogo, resolveName } from "@/lib/content";
import { getBusinessInfo } from "@/lib/meshkee";
import { getPageSlots, slotImages, slotRatio } from "@/lib/static-images";
@@ -22,47 +27,68 @@ export default async function HomePage() {
const [info, slots] = await Promise.all([getBusinessInfo(), getPageSlots("home")]);
const name = resolveName(info);
const about = resolveAbout(info);
+ const logo = resolveLogo(info);
+ const instagram = resolveInstagram(info);
- const hero = slotImages("hero", slots, [{ url: gallery.hero, titleFa: "نمای آجری در غروب" }])[0];
- const studio = slotImages("studio-image", slots, [{ url: gallery.studio, titleFa: "ورودی سبز" }])[0];
- const projectSlots = slots.projects?.images ?? [];
+ const heroSlides = slotImages("slider", slots, gallery.heroSlides);
+ const categoryImages = slotImages(
+ "four-category",
+ slots,
+ services.map((service) => ({
+ url: service.image,
+ titleFa: service.titleFa,
+ titleEn: service.titleEn,
+ subtext: service.text,
+ linkUrl: `/services/${service.slug}`,
+ })),
+ );
+ const projectSlots = slotImages(
+ "project-images",
+ slots,
+ projects.map((project) => ({
+ url: project.image,
+ titleFa: project.titleFa,
+ titleEn: project.titleEn,
+ subtext: `${project.category} · ${project.year}`,
+ linkUrl: `/portfolio/${project.slug}`,
+ })),
+ );
+ const sticky = slotImages("sticky-frame-bg", slots, [
+ { url: gallery.studio, titleFa: "ورودی سبز", titleEn: "Studio", subtext: about[2] ?? site.about[2] },
+ ])[0];
+ const openingImage = sticky.url || gallery.studio;
+ const instagramImage = slotImages("instagram-row", slots, [
+ {
+ url: gallery.instagram,
+ titleFa: "ما را در اینستاگرام دنبال کنید",
+ titleEn: "Follow us",
+ subtext: "پروژههای تازه و جزئیات اجرا را در صفحه اینستاگرام ببینید.",
+ linkUrl: instagram.href,
+ },
+ ])[0];
+ const instagramRatio = slotRatio("instagram-row", slots);
+ const brandLogos = slotImages("brands", slots, gallery.brands);
+ const brandRatio = slotRatio("brands", slots);
return (
<>
- {/* Hero */}
-
-
-
-
- معماری ساده،
-
- زندگی روشن
-
-
-
-
- {name} یک گروه معماری و طراحی داخلی است؛ از طراحی نما و پلان تا جزئیات داخلی و اجرای نهایی.
-
-
- شروع یک پروژه
-
-
-
-
-
-
- {hero.titleFa || hero.titleEn || "پروژه منتخب"}
- {site.nameEn}
-
-
-
+
- {/* Intro */}
+ {/* Intro — RTL: logo (rightmost) · copy · focus (left) */}
-
-
-
استودیو
-
{about[0]}
+
+
+
+
+
+
{about[1] ?? site.about[1]}
@@ -71,110 +97,130 @@ export default async function HomePage() {
معماری مسکونی، طراحی داخلی، طراحی نما
- بیشتر درباره هوم
+ بیشتر بدانید
- {/* Services */}
-
+ {/* four-category — 4 service panels */}
+
-
-
-
خدمات
-
چه کاری انجام میدهیم
-
-
-
- {services.map((service) => (
-
- {service.index}
-
-
{service.titleFa}
- {service.titleEn}
+
+
+
+ {Array.from({ length: 4 }, (_, index) => {
+ const service = services[index];
+ const cms = categoryImages[index];
+ const image = cms?.url || service?.image || gallery.heroSlides[0].url;
+ const titleFa = cms?.titleFa || service?.titleFa || `خدمت ${index + 1}`;
+ const titleEn = cms?.titleEn || service?.titleEn || "";
+ const desc = cms?.subtext || service?.text || "";
+ const href = cms?.linkUrl || (service ? `/services/${service.slug}` : "/services");
+ const external = /^https?:\/\//.test(href);
+ return (
+
+
+
+
+
{service?.index ?? String(index + 1).padStart(2, "0")}
+
{titleFa}
+ {titleEn ?
{titleEn} : null}
+ {desc ?
{desc}
: null}
-
{service.text}
-
- ))}
-
+
+ );
+ })}
- {/* Projects */}
+ {/* project-images — masonry */}
-
-
-
پروژهها
-
پروژههای منتخب
-
-
- گزیدهای از کارهای اخیر در معماری، طراحی داخلی و نما.
-
-
+
گزیدهای از کارهای اخیر در معماری، طراحی داخلی و نما.}
+ />
- {projectSlots.length
- ? projectSlots.map((image, index) => (
-
-
-
-
{image.titleFa || image.titleEn || `پروژه ${index + 1}`}
- {image.subtext ? {image.subtext} : null}
-
- {image.titleFa && image.titleEn ?
{image.titleEn}
: null}
+ {projectSlots.map((image, index) => {
+ const title = image.titleFa || image.titleEn || `پروژه ${index + 1}`;
+ const media = { ...image, linkUrl: null };
+ const card = (
+ <>
+
+
+
{title}
+ {image.subtext ? {image.subtext} : null}
- ))
- : projects.map((project) => (
-
-
-
-
-
-
{project.titleFa}
-
- {project.category} · {project.year}
-
-
- {project.titleEn}
-
- ))}
+ {image.titleEn ?
{image.titleEn}
: null}
+ >
+ );
+ if (image.linkUrl) {
+ const external = /^https?:\/\//.test(image.linkUrl);
+ return (
+
+ {card}
+
+ );
+ }
+ return (
+
+ {card}
+
+ );
+ })}
- {/* Studio */}
-
-
-
-
-
-
نگاه ما
-
فضایی که با شما زندگی میکند
-
-
- ما طراحی را از نیاز واقعی آغاز میکنیم؛ نه از فرم. نور، مسیر حرکت، متریال و تناسب، ابزار ما برای ساختن فضایی هستند که
- سالها بعد هم آرام و خوانا بماند.
-
-
{about[2] ?? site.about[2]}
-
- درباره استودیو
-
-
-
-
-
+
+
+ {/* sticky-frame-bg */}
+
{/* Process */}
-
فرآیند کار
-
از ایده تا تحویل، در چهار گام
+
{processSteps.map((item) => (
-
گام {item.step}
+
+ {String(item.step).padStart(2, "0")}
+
{item.titleFa}
{item.text}
@@ -184,30 +230,68 @@ export default async function HomePage() {
{/* Stats */}
-
-
-
- {stats.map((stat) => (
-
- {stat.value}
- {stat.label}
-
- ))}
-
+
+
+ {stats.map((stat) => (
+
+
+ {stat.value}
+
+ {stat.label}
+
+ ))}
- {/* CTA */}
-
-
-
-
شروع همکاری
-
پروژهای در ذهن دارید؟ دربارهاش صحبت کنیم.
+ {/* brands */}
+ {brandLogos.length ? (
+
+ ) : null}
+
+ {/* instagram-row */}
+
+
+ {/* CTA */}
+
+
+
+
+ تماس با ما
+
+
+ }
+ />
>
diff --git a/src/app/portfolio/[slug]/page.tsx b/src/app/portfolio/[slug]/page.tsx
new file mode 100644
index 0000000..b89d3d6
--- /dev/null
+++ b/src/app/portfolio/[slug]/page.tsx
@@ -0,0 +1,126 @@
+import type { Metadata } from "next";
+import Link from "next/link";
+import { notFound } from "next/navigation";
+
+import { MediaGallery } from "@/components/MediaGallery";
+import { SectionTitle } from "@/components/SectionTitle";
+import {
+ asRichHtml,
+ fetchPortfolioById,
+ fetchPortfolioBySlug,
+ fetchPortfolios,
+ getLocalPortfolio,
+ getLocalPortfolios,
+ portfolioCover,
+ portfolioHref,
+ portfolioImages,
+ portfolioTitle,
+ type PortfolioItem,
+} from "@/lib/portfolio";
+
+export const revalidate = 300;
+
+type PageProps = { params: Promise<{ slug: string }> };
+
+async function resolvePortfolio(slug: string): Promise
{
+ const decoded = decodeURIComponent(slug);
+ const bySlug = await fetchPortfolioBySlug(decoded);
+ if (bySlug) return bySlug;
+ const byId = await fetchPortfolioById(decoded);
+ if (byId) return byId;
+ return getLocalPortfolio(decoded);
+}
+
+export async function generateMetadata({ params }: PageProps): Promise {
+ const { slug } = await params;
+ const portfolio = await resolvePortfolio(slug);
+ if (!portfolio) return { title: "نمونه کار پیدا نشد" };
+ const title = portfolioTitle(portfolio);
+ const description = portfolio.abstract || title;
+ const image = portfolioCover(portfolio);
+ return {
+ title,
+ description,
+ openGraph: { title, description, images: [{ url: image, alt: title }] },
+ };
+}
+
+export default async function PortfolioDetailPage({ params }: PageProps) {
+ const { slug } = await params;
+ const portfolio = await resolvePortfolio(slug);
+ if (!portfolio) notFound();
+
+ const title = portfolioTitle(portfolio);
+ const html = asRichHtml(portfolio.mainTextHtml || portfolio.abstract);
+ const images = portfolioImages(portfolio);
+
+ const related = await fetchPortfolios({ page: 1, pageSize: 8 });
+ const pool = related.items.length ? related.items : getLocalPortfolios();
+ const similar = pool.filter((item) => item.id !== portfolio.id).slice(0, 3);
+
+ return (
+ <>
+
+
+
+ {portfolio.categoryName ? {portfolio.categoryName} : null}
+ {portfolio.titleEn ? {portfolio.titleEn} : null}
+
+ ) : null
+ }
+ />
+
+
+
+
+
+
+
+ {portfolio.abstract ?
{portfolio.abstract}
: null}
+ {html ?
: null}
+ {portfolio.projectUrl ? (
+
+ مشاهده لینک پروژه
+
+ ) : null}
+
+ بازگشت به نمونه کارها
+
+
+
+
+
+ {similar.length ? (
+
+
+
+
+ {similar.map((item) => {
+ const itemTitle = portfolioTitle(item);
+ return (
+
+
+
+
+
+
{itemTitle}
+ {item.categoryName ? {item.categoryName} : null}
+
+ {item.titleEn ?
{item.titleEn}
: null}
+
+ );
+ })}
+
+
+
+ ) : null}
+ >
+ );
+}
diff --git a/src/app/portfolio/page.tsx b/src/app/portfolio/page.tsx
new file mode 100644
index 0000000..6f66a75
--- /dev/null
+++ b/src/app/portfolio/page.tsx
@@ -0,0 +1,88 @@
+import type { Metadata } from "next";
+import Link from "next/link";
+
+import { SectionTitle } from "@/components/SectionTitle";
+import { site } from "@/data/site";
+import {
+ fetchPortfolios,
+ getLocalPortfolios,
+ portfolioCover,
+ portfolioHref,
+ portfolioTitle,
+} from "@/lib/portfolio";
+
+export const revalidate = 300;
+
+export const metadata: Metadata = {
+ title: "نمونه کارها",
+ description: `نمونه کارهای ${site.nameFa} در معماری، طراحی داخلی و طراحی نما.`,
+};
+
+export default async function PortfolioPage({
+ searchParams,
+}: {
+ searchParams: Promise<{ page?: string }>;
+}) {
+ const { page: pageRaw } = await searchParams;
+ const page = Math.max(1, Number(pageRaw) || 1);
+ const pageSize = 12;
+ const result = await fetchPortfolios({ page, pageSize });
+ const usingApi = result.items.length > 0;
+ const items = usingApi ? result.items : page === 1 ? getLocalPortfolios() : [];
+ const total = usingApi ? result.total : items.length;
+ const totalPages = Math.max(1, Math.ceil(total / pageSize));
+
+ return (
+ <>
+
+
+
+
+ {items.length === 0 ? (
+
هنوز نمونه کاری منتشر نشده است.
+ ) : (
+
+ {items.map((item) => {
+ const title = portfolioTitle(item);
+ return (
+
+
+
+
+
+
{title}
+ {item.categoryName || item.abstract ? (
+ {item.categoryName || item.abstract}
+ ) : null}
+
+ {item.titleEn ?
{item.titleEn}
: null}
+
+ );
+ })}
+
+ )}
+
+ {totalPages > 1 && items.length ? (
+
+ {page > 1 ? قبلی : }
+
+ {page} / {totalPages}
+
+ {page < totalPages ? بعدی : }
+
+ ) : null}
+
+
+ >
+ );
+}
diff --git a/src/app/projects/[slug]/page.tsx b/src/app/projects/[slug]/page.tsx
new file mode 100644
index 0000000..72053a5
--- /dev/null
+++ b/src/app/projects/[slug]/page.tsx
@@ -0,0 +1,9 @@
+import { redirect } from "next/navigation";
+
+type PageProps = { params: Promise<{ slug: string }> };
+
+/** Legacy route — Meshkee portfolios live at `/portfolio/[slug]`. */
+export default async function ProjectRedirect({ params }: PageProps) {
+ const { slug } = await params;
+ redirect(`/portfolio/${encodeURIComponent(slug)}`);
+}
diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx
new file mode 100644
index 0000000..e1aa626
--- /dev/null
+++ b/src/app/projects/page.tsx
@@ -0,0 +1,6 @@
+import { redirect } from "next/navigation";
+
+/** Legacy route — Meshkee portfolios live at `/portfolio`. */
+export default function ProjectsRedirect() {
+ redirect("/portfolio");
+}
diff --git a/src/app/services/[slug]/page.tsx b/src/app/services/[slug]/page.tsx
new file mode 100644
index 0000000..a5c8bb6
--- /dev/null
+++ b/src/app/services/[slug]/page.tsx
@@ -0,0 +1,125 @@
+import type { Metadata } from "next";
+import Link from "next/link";
+import { notFound } from "next/navigation";
+
+import { SectionTitle } from "@/components/SectionTitle";
+import { ServiceFrame } from "@/components/ServiceFrame";
+import { services, site } from "@/data/site";
+
+type Props = { params: Promise<{ slug: string }> };
+
+export function generateStaticParams() {
+ return services.map((service) => ({ slug: service.slug }));
+}
+
+export async function generateMetadata({ params }: Props): Promise {
+ const { slug } = await params;
+ const service = services.find((item) => item.slug === slug);
+ if (!service) return { title: "تخصص" };
+ return {
+ title: service.titleFa,
+ description: service.text,
+ openGraph: {
+ title: `${service.titleFa} | ${site.nameFa}`,
+ description: service.text,
+ images: [{ url: service.image, alt: service.titleFa }],
+ },
+ };
+}
+
+export default async function ServiceDetailPage({ params }: Props) {
+ const { slug } = await params;
+ const service = services.find((item) => item.slug === slug);
+ if (!service) notFound();
+
+ const others = services.filter((item) => item.slug !== slug);
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {service.index}
+
+
+
{service.text}
+
+
+
+
+
+
+
+
+
+
+
+
{service.lead}
+
+ تیم {site.nameFa} این تخصص را از مرحله ایده تا جزئیات اجرایی همراهی میکند تا نتیجه نهایی دقیق و هماهنگ با
+ نیاز شما باشد.
+
+
+ شروع گفتگو
+
+
+
+
+
+
+
+
+ همه تخصصها
+
+ }
+ />
+
+ {others.map((item) => (
+
+
+
+
+
+
+ {item.index}
+
+
{item.titleFa}
+
{item.titleEn}
+
+
+ ))}
+
+
+
+ >
+ );
+}
diff --git a/src/app/services/page.tsx b/src/app/services/page.tsx
new file mode 100644
index 0000000..b65cf3a
--- /dev/null
+++ b/src/app/services/page.tsx
@@ -0,0 +1,47 @@
+import type { Metadata } from "next";
+import Link from "next/link";
+
+import { SectionTitle } from "@/components/SectionTitle";
+import { services, site } from "@/data/site";
+
+export const metadata: Metadata = {
+ title: "تخصصهای ما",
+ description: `تخصصهای ${site.nameFa}: طراحی معماری، طراحی داخلی، طراحی نما و نظارت و اجرا.`,
+};
+
+export default function ServicesPage() {
+ return (
+ <>
+
+
+
+
+
+ {services.map((service) => (
+
+
+
+
+
+
{service.titleFa}
+ {service.titleEn}
+
+
{service.text}
+
+ ))}
+
+
+
+ >
+ );
+}
diff --git a/src/components/AccountMenu.tsx b/src/components/AccountMenu.tsx
new file mode 100644
index 0000000..acdcd83
--- /dev/null
+++ b/src/components/AccountMenu.tsx
@@ -0,0 +1,152 @@
+"use client";
+
+import { useEffect, useRef, useState, type MouseEvent } from "react";
+
+import { businessDashboardUrl, customerPortalUrl } from "@/lib/config";
+import { useSession, userDisplayName } from "@/lib/session";
+
+function UserIcon() {
+ return (
+
+
+
+
+ );
+}
+
+export function AccountMenu() {
+ const { user, isAuthenticated, ready, isWebsiteAdmin, logout, getAccessToken } = useSession();
+ const [open, setOpen] = useState(false);
+ const [handoffBusy, setHandoffBusy] = useState(false);
+ const menuRef = useRef(null);
+
+ useEffect(() => {
+ if (!open) return;
+ const onClickOutside = (event: Event) => {
+ if (menuRef.current && event.target instanceof Node && !menuRef.current.contains(event.target)) {
+ setOpen(false);
+ }
+ };
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (event.key === "Escape") setOpen(false);
+ };
+ document.addEventListener("mousedown", onClickOutside);
+ document.addEventListener("keydown", onKeyDown);
+ return () => {
+ document.removeEventListener("mousedown", onClickOutside);
+ document.removeEventListener("keydown", onKeyDown);
+ };
+ }, [open]);
+
+ async function openBusinessDashboard(event: MouseEvent) {
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) {
+ return;
+ }
+ event.preventDefault();
+ if (handoffBusy) return;
+
+ const baseUrl = businessDashboardUrl;
+ const popup = window.open("about:blank", "_blank");
+ setHandoffBusy(true);
+ try {
+ const token = getAccessToken();
+ const res = await fetch("/api/auth/handoff", {
+ method: "POST",
+ headers: token ? { Authorization: `Bearer ${token}` } : undefined,
+ });
+ if (!res.ok) throw new Error("handoff failed");
+ const data = (await res.json()) as { ticket?: string };
+ const url = new URL(baseUrl);
+ if (data.ticket) url.searchParams.set("handoff", data.ticket);
+ if (popup) {
+ popup.opener = null;
+ popup.location.replace(url.toString());
+ } else {
+ window.open(url.toString(), "_blank", "noopener,noreferrer");
+ }
+ } catch {
+ if (popup) popup.location.replace(baseUrl);
+ else window.open(baseUrl, "_blank", "noopener,noreferrer");
+ } finally {
+ setHandoffBusy(false);
+ setOpen(false);
+ }
+ }
+
+ if (!ready) {
+ return (
+
+
+
+ );
+ }
+
+ if (!isAuthenticated || !user) {
+ return (
+
+
+
+ );
+ }
+
+ const displayName = userDisplayName(user);
+
+ return (
+
+
setOpen((value) => !value)}
+ aria-expanded={open}
+ aria-haspopup="menu"
+ className="account-menu__trigger"
+ aria-label={`${displayName} عزیز`}
+ >
+ {displayName} عزیز
+
+
+ {open ? (
+
+ ) : null}
+
+ );
+}
diff --git a/src/components/ContactForm.tsx b/src/components/ContactForm.tsx
index 8cdffac..e8a4cc6 100644
--- a/src/components/ContactForm.tsx
+++ b/src/components/ContactForm.tsx
@@ -1,11 +1,86 @@
"use client";
-import { useState, type FormEvent } from "react";
+import { useEffect, useState, type FormEvent, type ReactNode } from "react";
type Status = { tone: "idle" | "pending" | "success" | "error"; message: string };
+function IconUser() {
+ return (
+
+
+
+
+ );
+}
+
+function IconPhone() {
+ return (
+
+
+
+ );
+}
+
+function IconMail() {
+ return (
+
+
+
+
+ );
+}
+
+function IconSubject() {
+ return (
+
+
+
+ );
+}
+
+function IconMessage() {
+ return (
+
+
+
+ );
+}
+
+function Field({
+ id,
+ label,
+ icon,
+ children,
+ wide,
+}: {
+ id: string;
+ label: string;
+ icon: ReactNode;
+ children: ReactNode;
+ wide?: boolean;
+}) {
+ return (
+
+
+ {label}
+
+
+ {icon}
+
+ {children}
+
+ );
+}
+
export function ContactForm() {
const [status, setStatus] = useState({ tone: "idle", message: "" });
+ const [toast, setToast] = useState<{ tone: "success" | "error"; message: string } | null>(null);
+
+ useEffect(() => {
+ if (!toast) return;
+ const timer = window.setTimeout(() => setToast(null), 4200);
+ return () => window.clearTimeout(timer);
+ }, [toast]);
async function onSubmit(event: FormEvent) {
event.preventDefault();
@@ -20,7 +95,9 @@ export function ContactForm() {
};
if (!payload.name || !payload.title || !payload.text) {
- setStatus({ tone: "error", message: "لطفاً نام، موضوع و متن پیام را کامل کنید." });
+ const message = "لطفاً نام، موضوع و متن پیام را کامل کنید.";
+ setStatus({ tone: "error", message });
+ setToast({ tone: "error", message });
return;
}
@@ -34,69 +111,98 @@ export function ContactForm() {
if (!response.ok) {
const body = (await response.json().catch(() => ({}))) as { message?: string | string[] };
const message = Array.isArray(body.message) ? body.message.join("، ") : body.message;
- // Only surface server messages that are already localized; otherwise use a generic one.
- throw new Error(message && /[\u0600-\u06FF]/.test(message) ? message : "ارسال پیام انجام نشد. لطفاً با شماره تماس استودیو در ارتباط باشید.");
+ throw new Error(
+ message && /[\u0600-\u06FF]/.test(message)
+ ? message
+ : "ارسال پیام انجام نشد. لطفاً با شماره تماس استودیو در ارتباط باشید.",
+ );
}
form.reset();
- setStatus({ tone: "success", message: "پیام شما ثبت شد. بهزودی با شما تماس میگیریم." });
+ const success = "پیام شما ثبت شد. بهزودی با شما تماس میگیریم.";
+ setStatus({ tone: "success", message: success });
+ setToast({ tone: "success", message: success });
} catch (error) {
- setStatus({
- tone: "error",
- message: error instanceof Error ? error.message : "ارسال پیام انجام نشد. دوباره تلاش کنید.",
- });
+ const message = error instanceof Error ? error.message : "ارسال پیام انجام نشد. دوباره تلاش کنید.";
+ setStatus({ tone: "error", message });
+ setToast({ tone: "error", message });
}
}
return (
-