diff --git a/Services/Main/MainHub.Api/Services/ServiceHistoryService.cs b/Services/Main/MainHub.Api/Services/ServiceHistoryService.cs index 4efc7c816..5b3d317a2 100644 --- a/Services/Main/MainHub.Api/Services/ServiceHistoryService.cs +++ b/Services/Main/MainHub.Api/Services/ServiceHistoryService.cs @@ -255,4 +255,4 @@ Guid serviceHistoryId Price = r.Price }).ToList(); } -} +} \ No newline at end of file diff --git a/Services/Main/MainHub.Api/Validators/CreateVehicleDtoValidator.cs b/Services/Main/MainHub.Api/Validators/CreateVehicleDtoValidator.cs index 55217e6fc..679e161b6 100644 --- a/Services/Main/MainHub.Api/Validators/CreateVehicleDtoValidator.cs +++ b/Services/Main/MainHub.Api/Validators/CreateVehicleDtoValidator.cs @@ -1,4 +1,3 @@ -using System.Data; using System.Text.RegularExpressions; using FluentValidation; using MainHub.Api.DTOs; @@ -61,12 +60,12 @@ public CreateVehicleDtoValidator() .When(x => x.BoughtAt.HasValue); RuleFor(x => x.EngineCapacity) - .NotEmpty() - .WithMessage("Engine capacity is required.") - .GreaterThan(0) - .WithMessage("Engine capacity must be greater than 0.") - .LessThanOrEqualTo(10000) - .WithMessage("Engine capacity must be less than or equal to 10000."); + .NotEmpty().WithMessage("Engine capacity is required.") + .LessThanOrEqualTo(10000).WithMessage("Engine capacity must be less than or equal to 10000.") + .GreaterThan(0).WithMessage("Engine capacity must be greater than 0.") + .When(x => x.FuelType is not FuelType.Electric and not FuelType.Hydrogen) + .Equal(0).WithMessage("Engine capacity must be 0 for Electric/Hydrogen vehicles.") + .When(x => x.FuelType is FuelType.Electric or FuelType.Hydrogen); RuleFor(x => x.Mileage) .NotEmpty() diff --git a/apps/.DS_Store b/apps/.DS_Store index 5008ddfcf..69c45d310 100644 Binary files a/apps/.DS_Store and b/apps/.DS_Store differ diff --git a/apps/AdminWebUi/src/App.tsx b/apps/AdminWebUi/src/App.tsx index 9630cc9cf..57d38541c 100644 --- a/apps/AdminWebUi/src/App.tsx +++ b/apps/AdminWebUi/src/App.tsx @@ -6,6 +6,7 @@ import { BrowserRouter } from "react-router-dom"; import { AuthProvider } from "./auth/AuthContext"; import { StoProvider } from "./context/StoContext"; import { StoVehicleProvider } from "./context/StoVehicleContext"; +import { BookingRequestProvider } from "./context/BookingRequestContext"; import { queryClient } from "./lib/queryClient"; import { AppRouter } from "./routes/AppRouter"; import { persistor, store } from "./store"; @@ -19,7 +20,9 @@ function App() { - + + + diff --git a/apps/AdminWebUi/src/components/ActivityLog/ActivityLog.module.css b/apps/AdminWebUi/src/components/ActivityLog/ActivityLog.module.css new file mode 100644 index 000000000..4cd79dee1 --- /dev/null +++ b/apps/AdminWebUi/src/components/ActivityLog/ActivityLog.module.css @@ -0,0 +1,233 @@ +.section { + margin: 20px 24px 24px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: var(--radius-lg); + background-color: #1a1c1e; + overflow: hidden; +} + +.toggle { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + width: 100%; + padding: 16px 20px; + border: none; + background: linear-gradient( + 180deg, + rgba(74, 222, 158, 0.04) 0%, + transparent 100% + ); + color: inherit; + cursor: pointer; + text-align: left; +} + +.toggle:hover { + background-color: rgba(255, 255, 255, 0.02); +} + +.toggleMain { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.toggleTitle { + margin: 0; + font-size: 13px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-secondary); +} + +.countBadge { + padding: 2px 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + color: var(--accent-primary); + background-color: rgba(74, 222, 158, 0.12); + border: 1px solid rgba(74, 222, 158, 0.24); +} + +.toggleHint { + margin: 4px 0 0; + font-size: 12px; + color: var(--text-tertiary); + line-height: 1.4; +} + +.chevron { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + flex-shrink: 0; + border-radius: var(--radius-sm); + border: 1px solid rgba(255, 255, 255, 0.08); + color: var(--text-secondary); + transition: + transform 0.3s ease, + color 0.2s ease, + border-color 0.2s ease; +} + +.chevronExpanded { + transform: rotate(180deg); + color: var(--accent-primary); + border-color: rgba(74, 222, 158, 0.24); +} + +.bodyWrapper { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 0.32s ease; +} + +.bodyWrapperExpanded { + grid-template-rows: 1fr; +} + +.bodyInner { + overflow: hidden; + min-height: 0; + opacity: 0; + transition: opacity 0.24s ease; +} + +.bodyWrapperExpanded .bodyInner { + opacity: 1; +} + +.body { + border-top: 1px solid rgba(255, 255, 255, 0.06); +} + +.scrollArea { + max-height: 360px; + overflow-y: auto; + padding: 12px 16px 16px; +} + +.scrollArea::-webkit-scrollbar { + width: 6px; +} + +.scrollArea::-webkit-scrollbar-thumb { + background-color: rgba(255, 255, 255, 0.12); + border-radius: 3px; +} + +.list { + display: flex; + flex-direction: column; + gap: 0; + margin: 0; + padding: 0; + list-style: none; +} + +.entry { + display: grid; + grid-template-columns: 20px 1fr; + gap: 12px; + padding: 12px 4px; + position: relative; +} + +.entry:not(:last-child)::after { + content: ""; + position: absolute; + left: 9px; + top: 32px; + bottom: -4px; + width: 2px; + background-color: rgba(255, 255, 255, 0.08); + border-radius: 1px; +} + +.marker { + display: flex; + align-items: flex-start; + justify-content: center; + padding-top: 4px; +} + +.dot { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; + box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.04); +} + +.entryMain { + min-width: 0; +} + +.entryHeader { + display: flex; + flex-wrap: wrap; + align-items: baseline; + justify-content: space-between; + gap: 8px; + margin-bottom: 4px; +} + +.entryTitle { + margin: 0; + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + line-height: 1.35; +} + +.entryTime { + font-size: 11px; + color: var(--text-tertiary); + white-space: nowrap; +} + +.entryMeta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + margin-bottom: 4px; +} + +.actorName { + font-size: 12px; + color: var(--text-secondary); +} + +.actorRole { + padding: 1px 6px; + border-radius: 4px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.3px; + color: var(--text-tertiary); + background-color: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.entryDescription { + margin: 0; + font-size: 12px; + color: var(--text-secondary); + line-height: 1.45; +} + +.empty { + padding: 20px 16px; + font-size: 13px; + color: var(--text-tertiary); + text-align: center; +} diff --git a/apps/AdminWebUi/src/components/ActivityLog/ActivityLog.tsx b/apps/AdminWebUi/src/components/ActivityLog/ActivityLog.tsx new file mode 100644 index 000000000..baf778282 --- /dev/null +++ b/apps/AdminWebUi/src/components/ActivityLog/ActivityLog.tsx @@ -0,0 +1,120 @@ +import { useState } from "react"; +import type { StoVehicleActivityEntry, StoVehicleActivityType } from "../../types/stoVehicle"; +import { ACTIVITY_ACTOR_ROLE_LABELS } from "../../utils/stoVehicleActivityLog"; +import { formatEventDateTime } from "../../utils/stoVehicleLabels"; +import styles from "./ActivityLog.module.css"; + +const ACTIVITY_DOT_COLORS: Record = { + vehicle_created: "#6e7681", + appointment_scheduled: "#58a6ff", + vehicle_arrived: "#4ade9e", + status_changed: "#a371f7", + mechanic_assigned: "#d29922", + approval_requested: "#ffa657", + approval_sent_to_client: "#58a6ff", + approval_approved: "#4ade9e", + approval_rejected: "#f85149", +}; + +interface ActivityLogProps { + entries: StoVehicleActivityEntry[]; + hint?: string; +} + +function ChevronIcon({ expanded }: { expanded: boolean }) { + return ( + + ); +} + +export function ActivityLog({ + entries, + hint = "Повний життєвий цикл: статуси, погодження, призначення механіка", +}: ActivityLogProps) { + const [isExpanded, setIsExpanded] = useState(false); + + return ( +
+ + +
+
+
+ {entries.length === 0 ? ( +

Записів у журналі поки немає.

+ ) : ( +
+
    + {entries.map((entry) => ( +
  • +
    + +
    +
    +
    +

    {entry.title}

    + +
    +
    + {entry.actorName} + + {ACTIVITY_ACTOR_ROLE_LABELS[entry.actorRole]} + +
    + {entry.description ? ( +

    {entry.description}

    + ) : null} +
    +
  • + ))} +
+
+ )} +
+
+
+
+ ); +} diff --git a/apps/AdminWebUi/src/components/ActivityLog/index.ts b/apps/AdminWebUi/src/components/ActivityLog/index.ts new file mode 100644 index 000000000..bf5f12d23 --- /dev/null +++ b/apps/AdminWebUi/src/components/ActivityLog/index.ts @@ -0,0 +1 @@ +export { ActivityLog } from "./ActivityLog"; diff --git a/apps/AdminWebUi/src/components/BookingRequestCard/BookingRequestCard.module.css b/apps/AdminWebUi/src/components/BookingRequestCard/BookingRequestCard.module.css new file mode 100644 index 000000000..535fe375e --- /dev/null +++ b/apps/AdminWebUi/src/components/BookingRequestCard/BookingRequestCard.module.css @@ -0,0 +1,152 @@ +.card { + background-color: #1a1c1e; + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: var(--radius-lg); + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; + cursor: pointer; + transition: + border-color 0.15s ease, + transform 0.15s ease; +} + +.card:hover { + border-color: rgba(74, 222, 158, 0.28); + transform: translateY(-1px); +} + +.cardPending { + border-color: rgba(255, 166, 87, 0.2); +} + +.cardHeader { + display: flex; + gap: 12px; + align-items: flex-start; +} + +.icon { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + flex-shrink: 0; + background-color: rgba(74, 222, 158, 0.1); + border-radius: var(--radius-md); + color: var(--accent-primary); +} + +.icon svg { + width: 22px; + height: 22px; +} + +.headerContent { + flex: 1; + min-width: 0; +} + +.titleRow { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; + margin-bottom: 4px; +} + +.vehicleTitle { + font-size: 15px; + font-weight: 600; + color: var(--text-primary); + line-height: 1.3; +} + +.plateBadge { + flex-shrink: 0; + padding: 3px 8px; + background-color: #1a4038; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + color: var(--accent-primary); +} + +.clientName { + margin: 0; + font-size: 13px; + color: var(--text-secondary); +} + +.reason { + margin: 0; + font-size: 13px; + color: var(--text-primary); + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.metaGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.metaItem { + padding: 8px 10px; + border-radius: var(--radius-sm); + background-color: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.05); +} + +.metaLabel { + display: block; + margin-bottom: 2px; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.3px; + text-transform: uppercase; + color: var(--text-tertiary); +} + +.metaValue { + font-size: 12px; + color: var(--text-primary); + line-height: 1.35; +} + +.footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding-top: 4px; + border-top: 1px solid rgba(255, 255, 255, 0.06); +} + +.createdAt { + font-size: 11px; + color: var(--text-tertiary); +} + +.footerMeta { + display: flex; + align-items: center; + gap: 10px; +} + +.attachments { + font-size: 11px; + color: var(--text-secondary); +} + +@media (max-width: 700px) { + .metaGrid { + grid-template-columns: 1fr; + } +} diff --git a/apps/AdminWebUi/src/components/BookingRequestCard/BookingRequestCard.tsx b/apps/AdminWebUi/src/components/BookingRequestCard/BookingRequestCard.tsx new file mode 100644 index 000000000..05490f94f --- /dev/null +++ b/apps/AdminWebUi/src/components/BookingRequestCard/BookingRequestCard.tsx @@ -0,0 +1,74 @@ +import type { BookingRequest } from "../../types/bookingRequest"; +import { + formatPreferredSlot, + PARTS_OPTION_LABELS, + VISIT_REASON_LABELS, +} from "../../utils/bookingLabels"; +import { formatEventDateTime, formatStoVehicleTitle } from "../../utils/stoVehicleLabels"; +import { BookingStatusBadge } from "../BookingStatusBadge"; +import styles from "./BookingRequestCard.module.css"; + +interface BookingRequestCardProps { + request: BookingRequest; + onOpen: (request: BookingRequest) => void; +} + +export function BookingRequestCard({ request, onOpen }: BookingRequestCardProps) { + const title = formatStoVehicleTitle(request.brand, request.model, request.year); + const preferredSlot = formatPreferredSlot(request.preferredDate, request.preferredTimeSlot); + + return ( +
onOpen(request)} + > +
+ +
+
+

{title}

+ {request.licensePlate} +
+

{request.clientName}

+
+
+ +

+ {VISIT_REASON_LABELS[request.visitReason]} — {request.problemDescription} +

+ +
+
+ Зручно водію + {preferredSlot} +
+
+ Запчастини + {PARTS_OPTION_LABELS[request.partsOption]} +
+
+ +
+ +
+ {request.attachmentsCount ? ( + 📎 {request.attachmentsCount} + ) : null} + {formatEventDateTime(request.createdAt)} +
+
+
+ ); +} diff --git a/apps/AdminWebUi/src/components/BookingRequestCard/index.ts b/apps/AdminWebUi/src/components/BookingRequestCard/index.ts new file mode 100644 index 000000000..ced5ac6ff --- /dev/null +++ b/apps/AdminWebUi/src/components/BookingRequestCard/index.ts @@ -0,0 +1 @@ +export { BookingRequestCard } from "./BookingRequestCard"; diff --git a/apps/AdminWebUi/src/components/BookingRequestDetailModal/BookingRequestDetailModal.module.css b/apps/AdminWebUi/src/components/BookingRequestDetailModal/BookingRequestDetailModal.module.css new file mode 100644 index 000000000..6f54eabec --- /dev/null +++ b/apps/AdminWebUi/src/components/BookingRequestDetailModal/BookingRequestDetailModal.module.css @@ -0,0 +1,358 @@ +.modalContent { + display: flex; + flex-direction: column; + min-height: 480px; +} + +.topBar { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + padding: 24px 24px 20px; + border-bottom: 1px solid var(--border-secondary); + background: linear-gradient( + 180deg, + rgba(74, 222, 158, 0.06) 0%, + transparent 100% + ); +} + +.topBarMain { + display: flex; + gap: 14px; + align-items: flex-start; + min-width: 0; +} + +.topBarIcon { + display: flex; + align-items: center; + justify-content: center; + width: 52px; + height: 52px; + flex-shrink: 0; + border-radius: var(--radius-md); + background-color: rgba(74, 222, 158, 0.12); + color: var(--accent-primary); +} + +.topBarIcon svg { + width: 26px; + height: 26px; +} + +.topBarTitle { + font-size: 22px; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 8px; +} + +.topBarMeta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.plateBadge { + padding: 4px 10px; + background-color: #1a4038; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + color: var(--accent-primary); +} + +.sourcePill { + padding: 4px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + color: #58a6ff; + background-color: rgba(88, 166, 255, 0.14); + border: 1px solid rgba(88, 166, 255, 0.28); +} + +.closeButton { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + flex-shrink: 0; + border: 1px solid var(--border-primary); + border-radius: var(--radius-sm); + background-color: var(--bg-tertiary); + color: var(--text-secondary); + font-size: 24px; + line-height: 1; + cursor: pointer; +} + +.closeButton:hover { + color: var(--text-primary); + background-color: var(--bg-quaternary); +} + +.summaryGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + padding: 20px 24px; + border-bottom: 1px solid var(--border-secondary); +} + +.summaryCard { + padding: 16px; + border-radius: var(--radius-md); + background-color: #1a1c1e; + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.summaryLabel { + margin: 0 0 12px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + color: var(--accent-primary); +} + +.summaryList { + display: flex; + flex-direction: column; + gap: 10px; + margin: 0; +} + +.summaryRow { + display: grid; + grid-template-columns: 120px 1fr; + gap: 12px; +} + +.summaryRow dt { + font-size: 12px; + color: var(--text-tertiary); +} + +.summaryRow dd { + margin: 0; + font-size: 14px; + color: var(--text-primary); + line-height: 1.4; +} + +.problemSection { + padding: 20px 24px; + border-bottom: 1px solid var(--border-secondary); +} + +.sectionTitle { + margin: 0 0 12px; + font-size: 13px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-secondary); +} + +.problemCard { + padding: 14px 16px; + border-radius: var(--radius-md); + background-color: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.reasonBadge { + display: inline-block; + margin-bottom: 8px; + padding: 4px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + color: var(--accent-primary); + background-color: rgba(74, 222, 158, 0.12); + border: 1px solid rgba(74, 222, 158, 0.24); +} + +.problemText { + margin: 0; + font-size: 14px; + color: var(--text-primary); + line-height: 1.5; +} + +.schedulingSection { + padding: 20px 24px 24px; +} + +.schedulingGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-bottom: 16px; +} + +.preferenceCard, +.confirmCard { + padding: 16px; + border-radius: var(--radius-md); + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.preferenceCard { + background-color: rgba(88, 166, 255, 0.06); + border-color: rgba(88, 166, 255, 0.18); +} + +.confirmCard { + background-color: rgba(74, 222, 158, 0.05); + border-color: rgba(74, 222, 158, 0.18); +} + +.cardTitle { + margin: 0 0 10px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.4px; +} + +.preferenceCard .cardTitle { + color: #58a6ff; +} + +.confirmCard .cardTitle { + color: var(--accent-primary); +} + +.preferenceValue { + margin: 0; + font-size: 15px; + font-weight: 600; + color: var(--text-primary); + line-height: 1.4; +} + +.preferenceHint { + margin: 6px 0 0; + font-size: 12px; + color: var(--text-secondary); +} + +.datetimeInput { + width: 100%; + padding: 10px 12px; + border-radius: var(--radius-sm); + border: 1px solid var(--border-primary); + background-color: var(--bg-tertiary); + color: var(--text-primary); + font-size: 14px; + font-family: inherit; +} + +.datetimeInput:focus { + outline: none; + border-color: var(--border-focus); +} + +.fieldError { + margin: 6px 0 0; + font-size: 12px; + color: var(--accent-error); +} + +.noteTextarea { + width: 100%; + margin-top: 12px; + padding: 10px 12px; + border-radius: var(--radius-sm); + border: 1px solid var(--border-primary); + background-color: var(--bg-tertiary); + color: var(--text-primary); + font-size: 13px; + font-family: inherit; + resize: vertical; +} + +.noteTextarea:focus { + outline: none; + border-color: var(--border-focus); +} + +.attachmentsNote { + margin: 0 0 16px; + padding: 10px 12px; + border-radius: var(--radius-sm); + font-size: 12px; + color: var(--text-secondary); + background-color: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + padding-top: 4px; +} + +.rejectForm { + display: flex; + flex-direction: column; + gap: 10px; + flex-basis: 100%; + padding: 12px; + border-radius: var(--radius-sm); + background-color: rgba(248, 81, 73, 0.06); + border: 1px solid rgba(248, 81, 73, 0.18); +} + +.rejectHint { + margin: 0; + font-size: 12px; + color: var(--text-secondary); +} + +.resolvedBanner { + padding: 14px 16px; + border-radius: var(--radius-md); + background-color: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.resolvedTitle { + margin: 0 0 6px; + font-size: 14px; + font-weight: 600; + color: var(--text-primary); +} + +.resolvedText { + margin: 0; + font-size: 13px; + color: var(--text-secondary); + line-height: 1.45; +} + +.successMessage { + margin: 0; + flex-basis: 100%; + font-size: 13px; + color: var(--accent-primary); +} + +@media (max-width: 700px) { + .summaryGrid, + .schedulingGrid { + grid-template-columns: 1fr; + } + + .summaryRow { + grid-template-columns: 1fr; + gap: 4px; + } +} diff --git a/apps/AdminWebUi/src/components/BookingRequestDetailModal/BookingRequestDetailModal.tsx b/apps/AdminWebUi/src/components/BookingRequestDetailModal/BookingRequestDetailModal.tsx new file mode 100644 index 000000000..e8ad5ab46 --- /dev/null +++ b/apps/AdminWebUi/src/components/BookingRequestDetailModal/BookingRequestDetailModal.tsx @@ -0,0 +1,293 @@ +import { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { Button } from "../Button"; +import { Modal } from "../Modal"; +import { BookingStatusBadge } from "../BookingStatusBadge"; +import type { BookingRequest } from "../../types/bookingRequest"; +import { + formatBookingDateTime, + formatPreferredSlot, + fromDatetimeLocalValue, + PARTS_OPTION_LABELS, + suggestedAppointmentIso, + toDatetimeLocalValue, + VISIT_REASON_LABELS, +} from "../../utils/bookingLabels"; +import { formatEventDateTime, formatStoVehicleTitle } from "../../utils/stoVehicleLabels"; +import styles from "./BookingRequestDetailModal.module.css"; + +interface BookingRequestDetailModalProps { + request: BookingRequest | null; + isOpen: boolean; + onClose: () => void; + onConfirm: (requestId: string, appointmentAt: string, managerNote?: string) => void; + onReject: (requestId: string, reason: string) => void; +} + +export function BookingRequestDetailModal({ + request, + isOpen, + onClose, + onConfirm, + onReject, +}: BookingRequestDetailModalProps) { + const [appointmentLocal, setAppointmentLocal] = useState(""); + const [managerNote, setManagerNote] = useState(""); + const [appointmentError, setAppointmentError] = useState(null); + const [showRejectForm, setShowRejectForm] = useState(false); + const [rejectReason, setRejectReason] = useState(""); + const [successMessage, setSuccessMessage] = useState(null); + + useEffect(() => { + if (!request) return; + const suggested = suggestedAppointmentIso(request.preferredDate, request.preferredTimeSlot); + setAppointmentLocal(toDatetimeLocalValue(suggested)); + setManagerNote(""); + setAppointmentError(null); + setShowRejectForm(false); + setRejectReason(""); + setSuccessMessage(null); + }, [request?.id]); + + if (!request) return null; + + const vehicleTitle = formatStoVehicleTitle(request.brand, request.model, request.year); + const preferredSlot = formatPreferredSlot(request.preferredDate, request.preferredTimeSlot); + const isPending = request.status === "pending"; + + const handleConfirm = () => { + if (!appointmentLocal) { + setAppointmentError("Вкажіть дату та час запису на СТО"); + return; + } + + setAppointmentError(null); + onConfirm(request.id, fromDatetimeLocalValue(appointmentLocal), managerNote); + setSuccessMessage("Запис підтверджено. Авто додано до черги зі статусом «Записано»."); + setShowRejectForm(false); + }; + + const handleReject = () => { + if (!rejectReason.trim()) return; + onReject(request.id, rejectReason.trim()); + setShowRejectForm(false); + }; + + return ( + +
+
+
+ +
+

Онлайн-запис

+
+ {request.licensePlate} + + Додаток водія +
+
+
+ +
+ +
+
+

Автомобіль

+
+
+
Марка / модель
+
{vehicleTitle}
+
+
+
Номер
+
{request.licensePlate}
+
+ {request.vin ? ( +
+
VIN
+
{request.vin}
+
+ ) : null} +
+
Запчастини
+
{PARTS_OPTION_LABELS[request.partsOption]}
+
+
+
+ +
+

Водій

+
+
+
Ім'я
+
{request.clientName}
+
+
+
Телефон
+
{request.clientPhone ?? "—"}
+
+
+
Надіслано
+
{formatEventDateTime(request.createdAt)}
+
+ {request.attachmentsCount ? ( +
+
Медіа
+
{request.attachmentsCount} фото/відео від водія
+
+ ) : null} +
+
+
+ +
+

Опис проблеми

+
+ + {VISIT_REASON_LABELS[request.visitReason]} + +

{request.problemDescription}

+
+
+ +
+

Планування візиту

+ + {request.attachmentsCount ? ( +

+ Водій додав {request.attachmentsCount} медіафайл(ів) до запиту. Перегляд вкладень + буде доступний після підключення API. +

+ ) : null} + +
+
+

Зручний час для водія

+

{preferredSlot}

+

+ Врахуйте побажання клієнта при підтвердженні запису +

+
+ +
+

Реальний час на СТО

+ {isPending ? ( + <> + { + setAppointmentLocal(event.target.value); + setAppointmentError(null); + }} + /> + {appointmentError ? ( +

{appointmentError}

+ ) : null} + + + + +
+ + + + + + + diff --git a/apps/landing/script.js b/apps/landing/script.js new file mode 100644 index 000000000..581bcdb9d --- /dev/null +++ b/apps/landing/script.js @@ -0,0 +1,165 @@ +const menuToggle = document.querySelector("[data-menu-toggle]"); +const menu = document.querySelector("[data-menu]"); + +if (menuToggle && menu) { + menuToggle.addEventListener("click", () => { + const isOpen = menu.classList.toggle("open"); + menuToggle.setAttribute("aria-expanded", String(isOpen)); + }); + + menu.querySelectorAll("a").forEach((link) => { + link.addEventListener("click", () => { + menu.classList.remove("open"); + menuToggle.setAttribute("aria-expanded", "false"); + }); + }); +} + +const showcaseTabs = Array.from(document.querySelectorAll("[data-showcase-tab]")); +const showcasePanels = Array.from(document.querySelectorAll("[data-showcase-panel]")); + +showcaseTabs.forEach((tab) => { + tab.addEventListener("click", () => { + const target = tab.dataset.showcaseTab; + + showcaseTabs.forEach((item) => { + const isActive = item === tab; + item.classList.toggle("is-active", isActive); + item.setAttribute("aria-selected", String(isActive)); + }); + + showcasePanels.forEach((panel) => { + panel.classList.toggle("is-active", panel.dataset.showcasePanel === target); + }); + }); +}); + +document.querySelectorAll("[data-showcase-thumb]").forEach((thumb) => { + thumb.addEventListener("click", () => { + const panel = thumb.closest("[data-showcase-panel]"); + const mainButton = panel?.querySelector("[data-showcase-main]"); + const mainImage = mainButton?.querySelector(".device-frame img"); + const thumbImage = thumb.querySelector("img"); + + if (!mainImage || !thumbImage) { + return; + } + + mainImage.src = thumbImage.currentSrc || thumbImage.src; + mainImage.alt = thumbImage.alt || mainImage.alt; + + panel.querySelectorAll("[data-showcase-thumb]").forEach((item) => { + item.classList.toggle("is-active", item === thumb); + }); + }); +}); + +const lightbox = document.getElementById("lightbox"); +const lightboxImage = lightbox?.querySelector(".lightbox-image"); +const lightboxCaption = lightbox?.querySelector(".lightbox-caption"); +const lightboxCounter = lightbox?.querySelector(".lightbox-counter"); +const lightboxItems = Array.from(document.querySelectorAll("[data-lightbox-index]")).map( + (trigger) => { + const image = trigger.querySelector("img"); + const panel = trigger.closest("[data-showcase-panel]"); + const caption = + panel?.querySelector("[data-showcase-main]")?.dataset.caption || + image?.alt || + ""; + + return { + src: image?.currentSrc || image?.src || "", + alt: image?.alt || "", + caption, + }; + } +); + +let lightboxIndex = 0; +let lastFocusedElement = null; + +function renderLightboxItem(index) { + if (!lightboxItems.length || !lightboxImage || !lightboxCaption || !lightboxCounter) { + return; + } + + const normalizedIndex = (index + lightboxItems.length) % lightboxItems.length; + const item = lightboxItems[normalizedIndex]; + + lightboxIndex = normalizedIndex; + lightboxImage.src = item.src; + lightboxImage.alt = item.alt; + lightboxCaption.textContent = item.caption; + lightboxCounter.textContent = `${normalizedIndex + 1} / ${lightboxItems.length}`; +} + +function openLightbox(index) { + if (!lightbox || !lightboxItems.length) { + return; + } + + lastFocusedElement = document.activeElement; + renderLightboxItem(index); + + lightbox.hidden = false; + lightbox.setAttribute("aria-hidden", "false"); + document.body.style.overflow = "hidden"; + + lightbox.querySelector(".lightbox-close")?.focus(); +} + +function closeLightbox() { + if (!lightbox || !lightboxImage) { + return; + } + + lightbox.hidden = true; + lightbox.setAttribute("aria-hidden", "true"); + document.body.style.overflow = ""; + + lightboxImage.removeAttribute("src"); + lastFocusedElement?.focus(); +} + +function showNextLightboxItem(step) { + renderLightboxItem(lightboxIndex + step); +} + +document.querySelectorAll("[data-lightbox-open]").forEach((trigger) => { + trigger.addEventListener("click", () => { + const panel = trigger.closest("[data-showcase-panel]"); + const activeThumb = panel?.querySelector("[data-showcase-thumb].is-active"); + const index = Number(activeThumb?.dataset.lightboxIndex ?? 0); + openLightbox(index); + }); +}); + +lightbox?.querySelectorAll("[data-lightbox-close]").forEach((button) => { + button.addEventListener("click", closeLightbox); +}); + +lightbox + ?.querySelector("[data-lightbox-prev]") + ?.addEventListener("click", () => showNextLightboxItem(-1)); + +lightbox + ?.querySelector("[data-lightbox-next]") + ?.addEventListener("click", () => showNextLightboxItem(1)); + +document.addEventListener("keydown", (event) => { + if (!lightbox || lightbox.hidden) { + return; + } + + if (event.key === "Escape") { + closeLightbox(); + } + + if (event.key === "ArrowLeft") { + showNextLightboxItem(-1); + } + + if (event.key === "ArrowRight") { + showNextLightboxItem(1); + } +}); diff --git a/apps/landing/styles.css b/apps/landing/styles.css new file mode 100644 index 000000000..0a5b571b0 --- /dev/null +++ b/apps/landing/styles.css @@ -0,0 +1,1020 @@ +:root { + --bg-page: #0d1117; + --bg-card: #1a1c1e; + --bg-surface: #232527; + --text-primary: #f0f6fc; + --text-secondary: #8b949e; + --text-muted: #6e7681; + --accent-primary: #4ade9e; + --accent-hover: #3bc98e; + --accent-dark: #1a4038; + --accent-warning: #d29922; + --border-main: #30363d; + --radius-sm: 10px; + --radius-md: 16px; + --radius-lg: 24px; + --shadow-card: 0 14px 36px rgba(0, 0, 0, 0.35); + --container-width: 1180px; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; +} + +body { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: radial-gradient(circle at top right, #111822, #0d1117 45%); + color: var(--text-primary); + line-height: 1.55; +} + +a { + color: inherit; + text-decoration: none; +} + +.container { + width: min(var(--container-width), calc(100% - 40px)); + margin: 0 auto; +} + +.section { + padding: 84px 0; + position: relative; +} + +.section.alt { + background: linear-gradient( + 160deg, + rgba(26, 28, 30, 0.72), + rgba(13, 17, 23, 0.8) 55% + ); + border-top: 1px solid rgba(255, 255, 255, 0.04); + border-bottom: 1px solid rgba(255, 255, 255, 0.04); +} + +.site-header { + position: sticky; + top: 0; + z-index: 30; + backdrop-filter: blur(16px); + background: rgba(13, 17, 23, 0.82); + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.header-inner { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + min-height: 74px; + gap: 16px; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 10px; + flex-shrink: 0; + font-size: 22px; + font-weight: 800; + letter-spacing: 0.3px; + color: var(--accent-primary); +} + +.brand-logo { + width: 34px; + height: 34px; + border-radius: 10px; + object-fit: contain; +} + +.header-nav { + display: flex; + align-items: center; + gap: clamp(10px, 1.2vw, 18px); + color: var(--text-secondary); + font-size: 14px; + font-weight: 500; + white-space: nowrap; +} + +.header-nav a:hover { + color: var(--text-primary); +} + +.header-nav .cta-link { + margin-left: 4px; + padding: 9px 14px; + border-radius: 10px; + background: var(--accent-primary); + color: #10171c !important; + font-weight: 600; +} + +.header-nav .cta-link:hover { + background: var(--accent-hover); + color: #10171c !important; +} + +.menu-toggle { + display: none; + flex-shrink: 0; + width: 40px; + height: 40px; + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 10px; + color: var(--text-primary); + justify-content: center; + align-items: center; + flex-direction: column; + gap: 4px; + cursor: pointer; +} + +.menu-toggle span { + width: 18px; + height: 2px; + background-color: currentColor; +} + +.hero { + padding-top: 74px; +} + +.hero-grid { + display: grid; + grid-template-columns: 1.15fr 0.85fr; + gap: 28px; +} + +.eyebrow { + color: var(--accent-primary); + text-transform: uppercase; + letter-spacing: 0.11em; + font-size: 12px; + font-weight: 700; + margin: 0 0 14px; +} + +h1, +h2, +h3 { + margin: 0 0 12px; + line-height: 1.2; +} + +h1 { + font-size: clamp(34px, 4.6vw, 56px); + max-width: 12.5ch; + font-weight: 800; +} + +h2 { + font-size: clamp(28px, 3.4vw, 42px); + font-weight: 700; +} + +h3 { + font-size: 22px; + font-weight: 650; +} + +.lead { + color: var(--text-secondary); + max-width: 65ch; + font-size: 18px; +} + +.hero-actions { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-top: 28px; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + border-radius: 14px; + padding: 13px 18px; + font-weight: 600; + cursor: pointer; + transition: 0.2s ease; +} + +.btn-primary { + background: var(--accent-primary); + color: #10171c; +} + +.btn-primary:hover { + background: var(--accent-hover); + transform: translateY(-1px); +} + +.btn-secondary { + background: var(--bg-surface); + color: var(--text-primary); + border: 1px solid var(--border-main); +} + +.btn-secondary:hover { + border-color: var(--accent-primary); +} + +.quick-metrics { + margin: 30px 0 0; + padding: 0; + list-style: none; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; +} + +.quick-metrics li { + background: rgba(35, 37, 39, 0.7); + border: 1px solid rgba(255, 255, 255, 0.07); + border-radius: var(--radius-sm); + padding: 12px; +} + +.quick-metrics strong { + display: block; + margin-bottom: 6px; + color: var(--text-primary); + font-size: 14px; +} + +.quick-metrics span { + color: var(--text-secondary); + font-size: 13px; +} + +.hero-panel { + position: relative; + overflow: visible; + background: linear-gradient(180deg, rgba(26, 64, 56, 0.32), rgba(26, 28, 30, 0.95)); + border: 1px solid rgba(74, 222, 158, 0.24); + border-radius: var(--radius-lg); + padding: 28px; + box-shadow: var(--shadow-card); +} + +.hero-mark { + position: absolute; + right: -75px; + bottom: -80px; + width: 250px; + height: auto; + pointer-events: none; + user-select: none; + z-index: 0; + filter: drop-shadow(0 0 28px rgba(74, 222, 158, 0.28)); +} + +.panel-label { + position: relative; + z-index: 1; + display: inline-block; + background: rgba(74, 222, 158, 0.13); + color: var(--accent-primary); + padding: 5px 10px; + border-radius: 999px; + font-size: 12px; + margin-bottom: 16px; +} + +.hero-panel p { + color: var(--text-secondary); +} + +.hero-panel h2, +.hero-panel p, +.status-stack { + position: relative; + z-index: 1; +} + +.status-stack { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 20px; +} + +.status-pill { + border-radius: 999px; + padding: 7px 12px; + font-size: 12px; + font-weight: 600; +} + +.status-pill--success { + background: rgba(74, 222, 158, 0.15); + color: var(--accent-primary); +} + +.status-pill--info { + background: rgba(88, 166, 255, 0.14); + color: #58a6ff; +} + +.status-pill--warning { + background: rgba(210, 153, 34, 0.15); + color: var(--accent-warning); +} + +.section-head { + margin-bottom: 30px; +} + +.section-head h2 { + max-width: 18ch; +} + +.section-head--wide h2 { + max-width: 28ch; +} + +.section-lead { + margin: 0; + color: var(--text-secondary); + max-width: 52ch; +} + +.product-showcase { + background: linear-gradient(180deg, rgba(26, 28, 30, 0.95), rgba(13, 17, 23, 0.9)); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: var(--radius-lg); + padding: 18px; + box-shadow: var(--shadow-card); +} + +.showcase-tabs { + display: inline-flex; + gap: 8px; + padding: 6px; + margin-bottom: 18px; + background: rgba(35, 37, 39, 0.9); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 999px; +} + +.showcase-tab { + border: 0; + background: transparent; + color: var(--text-secondary); + border-radius: 999px; + padding: 10px 18px; + font: inherit; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.showcase-tab:hover { + color: var(--text-primary); +} + +.showcase-tab.is-active { + background: var(--accent-dark); + color: var(--accent-primary); + box-shadow: inset 0 0 0 1px rgba(74, 222, 158, 0.25); +} + +.showcase-panels { + position: relative; +} + +.showcase-panel { + display: none; + grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); + gap: 28px; + align-items: center; + padding: 8px 6px 12px; +} + +.showcase-panel.is-active { + display: grid; +} + +.showcase-label { + margin: 0 0 8px; + color: var(--accent-primary); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.showcase-copy h3 { + margin-bottom: 10px; + font-size: clamp(24px, 3vw, 32px); +} + +.showcase-copy > p { + margin: 0 0 16px; + color: var(--text-secondary); + max-width: 42ch; +} + +.showcase-points { + margin: 0 0 20px; + padding-left: 18px; + color: var(--text-secondary); +} + +.showcase-points li + li { + margin-top: 6px; +} + +.showcase-thumbs { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.showcase-thumb { + width: 64px; + aspect-ratio: 9 / 19.5; + padding: 0; + border-radius: 12px; + border: 2px solid rgba(255, 255, 255, 0.08); + background: var(--bg-surface); + overflow: hidden; + cursor: pointer; + transition: border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease; +} + +.showcase-thumb:hover { + transform: translateY(-2px); + border-color: rgba(74, 222, 158, 0.35); +} + +.showcase-thumb.is-active { + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px rgba(74, 222, 158, 0.14); +} + +.showcase-thumb img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.showcase-thumbs--desktop .showcase-thumb { + width: 128px; + aspect-ratio: 16 / 10; + border-radius: 10px; +} + +.showcase-thumbs--desktop .showcase-thumb img { + object-fit: contain; + background: #0d1117; +} + +.showcase-visual { + display: flex; + justify-content: center; + align-items: center; + min-height: 420px; + padding: 12px; + border-radius: 20px; +} + +.showcase-main { + position: relative; + padding: 0; + border: 0; + background: transparent; + cursor: zoom-in; +} + +.showcase-main:hover .showcase-expand { + opacity: 1; + transform: translateY(0); +} + +.device-frame { + position: relative; + overflow: hidden; + background: #0d1117; + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.45); +} + +.device-frame--phone { + width: min(270px, 72vw); + border-radius: 34px; + border: 2px solid rgba(255, 255, 255, 0.12); + padding: 12px 10px 14px; +} + +.device-notch { + width: 96px; + height: 22px; + margin: 0 auto 10px; + border-radius: 0 0 14px 14px; + background: #0d1117; +} + +.device-frame--phone img { + display: block; + width: 100%; + border-radius: 24px; +} + +.device-frame--desktop { + width: min(560px, 92vw); + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.device-browser-bar { + display: flex; + gap: 6px; + padding: 12px 14px; + background: #161b22; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +.device-browser-bar span { + width: 10px; + height: 10px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.14); +} + +.device-frame--desktop img { + display: block; + width: 100%; +} + +.showcase-expand { + position: absolute; + left: 50%; + bottom: 18px; + transform: translate(-50%, 8px); + opacity: 0; + padding: 8px 12px; + border-radius: 999px; + background: rgba(13, 17, 23, 0.82); + border: 1px solid rgba(74, 222, 158, 0.35); + color: var(--accent-primary); + font-size: 12px; + font-weight: 600; + transition: opacity 0.2s ease, transform 0.2s ease; + pointer-events: none; +} + +.lightbox { + position: fixed; + inset: 0; + z-index: 100; +} + +.lightbox[hidden] { + display: none; +} + +.lightbox-backdrop { + position: absolute; + inset: 0; + border: 0; + background: rgba(0, 0, 0, 0.9); + cursor: zoom-out; +} + +.lightbox-shell { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + height: 100%; + padding: 20px; +} + +.lightbox-toolbar { + display: grid; + grid-template-columns: 1fr auto auto; + align-items: center; + gap: 14px; + margin-bottom: 16px; + padding: 12px 14px; + border-radius: 14px; + background: rgba(26, 28, 30, 0.92); + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.lightbox-caption { + margin: 0; + color: var(--text-primary); + font-size: 14px; + font-weight: 600; +} + +.lightbox-counter { + color: var(--text-secondary); + font-size: 13px; + font-weight: 500; + white-space: nowrap; +} + +.lightbox-close, +.lightbox-nav { + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(35, 37, 39, 0.96); + color: var(--text-primary); + border-radius: 12px; + cursor: pointer; + transition: border-color 0.2s ease, background-color 0.2s ease; +} + +.lightbox-close:hover, +.lightbox-nav:hover { + border-color: rgba(74, 222, 158, 0.45); + background: rgba(26, 64, 56, 0.55); +} + +.lightbox-close { + width: 40px; + height: 40px; + font-size: 24px; + line-height: 1; +} + +.lightbox-stage { + flex: 1; + min-height: 0; + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 14px; +} + +.lightbox-image-wrap { + display: flex; + align-items: center; + justify-content: center; + min-height: 0; + height: 100%; +} + +.lightbox-image { + max-width: 100%; + max-height: calc(100vh - 140px); + width: auto; + height: auto; + object-fit: contain; + border-radius: 12px; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45); +} + +.lightbox-nav { + width: 44px; + height: 44px; + font-size: 28px; + line-height: 1; +} + +@media (max-width: 1040px) { + .showcase-panel, + .showcase-panel.is-active { + grid-template-columns: 1fr; + } + + .showcase-visual { + min-height: 360px; + } +} + +@media (max-width: 820px) { + .showcase-tabs { + display: flex; + width: 100%; + overflow-x: auto; + } + + .showcase-tab { + flex: 1 0 auto; + } + + .lightbox-shell { + padding: 14px; + } + + .lightbox-stage { + grid-template-columns: 1fr; + justify-items: center; + align-content: center; + } + + .lightbox-nav { + display: none; + } + + .lightbox-image { + max-height: calc(100vh - 120px); + } +} + +.cards-grid, +.vision-grid { + display: grid; + gap: 16px; +} + +.cards-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.value-card, +.vision-card, +.split-card, +.faq-list details { + background-color: var(--bg-card); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: var(--radius-md); + padding: 22px; +} + +.value-card p, +.vision-card p, +.split-card p, +.faq-list p { + color: var(--text-secondary); +} + +.timeline { + display: flex; + flex-direction: column; + gap: 0; +} + +.timeline-item { + display: grid; + grid-template-columns: 32px 1fr; + gap: 16px; + padding-bottom: 28px; +} + +.timeline-item:last-child { + padding-bottom: 0; +} + +.timeline-item:last-child .timeline-marker::after { + display: none; +} + +.timeline-marker { + position: relative; + display: flex; + justify-content: center; + padding-top: 7px; +} + +.timeline-marker::after { + content: ""; + position: absolute; + top: 24px; + bottom: -28px; + left: 50%; + width: 2px; + transform: translateX(-50%); + background: rgba(74, 222, 158, 0.22); +} + +.timeline .dot { + position: relative; + z-index: 1; + display: block; + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--accent-primary); + box-shadow: 0 0 0 5px #14181d; +} + +.timeline h3 { + margin-bottom: 8px; +} + +.timeline p { + color: var(--text-secondary); + margin: 0; +} + +.split { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; +} + +.split-card ul { + margin: 0; + padding-left: 18px; + color: var(--text-secondary); +} + +.ecosystem-thesis { + margin-bottom: 18px; +} + +.marketing-engine { + margin-top: 18px; + border-color: rgba(74, 222, 158, 0.3); + background: linear-gradient(180deg, rgba(26, 64, 56, 0.2), rgba(26, 28, 30, 0.95)); +} + +.growth-engine { + margin-top: 18px; + border-color: rgba(88, 166, 255, 0.28); + background: linear-gradient(180deg, rgba(26, 40, 64, 0.22), rgba(26, 28, 30, 0.95)); +} + +.vision-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.faq-list { + display: grid; + gap: 12px; +} + +.faq-list summary { + cursor: pointer; + list-style: none; + font-weight: 600; +} + +.faq-list summary::-webkit-details-marker { + display: none; +} + +.faq-list details[open] { + border-color: rgba(74, 222, 158, 0.4); +} + +.faq-list p { + margin: 10px 0 0; +} + +.site-footer { + padding-top: 62px; + border-top: 1px solid rgba(255, 255, 255, 0.05); +} + +.footer-inner { + display: grid; + grid-template-columns: 1fr minmax(280px, 420px); + gap: 24px; + align-items: start; +} + +.footer-inner p { + color: var(--text-secondary); +} + +.demo-form { + background: var(--bg-card); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: var(--radius-md); + padding: 20px; + display: grid; + gap: 14px; +} + +.demo-form label { + display: grid; + gap: 6px; + font-size: 14px; + color: var(--text-secondary); +} + +.demo-form input, +.demo-form textarea { + background: var(--bg-surface); + border: 1px solid rgba(255, 255, 255, 0.12); + color: var(--text-primary); + border-radius: 10px; + padding: 12px; + font: inherit; +} + +.demo-form input:focus, +.demo-form textarea:focus { + outline: none; + border-color: var(--accent-primary); +} + +.bg-glow { + position: fixed; + z-index: -1; + filter: blur(80px); + pointer-events: none; +} + +.bg-glow--one { + width: 300px; + height: 300px; + background: rgba(74, 222, 158, 0.15); + top: -80px; + right: -100px; +} + +.bg-glow--two { + width: 260px; + height: 260px; + background: rgba(88, 166, 255, 0.14); + bottom: 120px; + left: -120px; +} + +@media (max-width: 1040px) { + .hero-grid, + .footer-inner, + .split { + grid-template-columns: 1fr; + } + + .hero-mark { + width: 120px; + right: 8px; + bottom: 8px; + } + + .quick-metrics, + .cards-grid, + .vision-grid { + grid-template-columns: 1fr; + } + + .section { + padding: 68px 0; + } +} + +@media (max-width: 1140px) { + .menu-toggle { + display: inline-flex; + } + + .header-nav { + position: absolute; + top: calc(100% + 8px); + right: 0; + left: 0; + display: none; + flex-direction: column; + align-items: stretch; + background: rgba(13, 17, 23, 0.98); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 14px; + padding: 12px; + gap: 4px; + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45); + white-space: normal; + } + + .header-nav.open { + display: flex; + } + + .header-nav a { + padding: 10px 12px; + border-radius: 10px; + } + + .header-nav a:hover { + background: rgba(255, 255, 255, 0.04); + } + + .header-nav .cta-link { + margin-left: 0; + margin-top: 6px; + text-align: center; + } +} + +@media (max-width: 820px) { + .hero { + padding-top: 54px; + } + + .container { + width: min(var(--container-width), calc(100% - 28px)); + } +}