From b2b4a0b07621afea0b4d5b8463070d80295ed710 Mon Sep 17 00:00:00 2001 From: Rajpreet0 Date: Fri, 25 Sep 2026 00:45:33 +0200 Subject: [PATCH] added full design --- app/apple-icon.png | Bin 0 -> 6989 bytes app/favicon.ico | Bin 25931 -> 4142 bytes app/fonts.ts | 21 + app/globals.css | 551 ++++++++++++++---- app/icon.svg | 21 + app/layout.tsx | 74 ++- app/manifest.ts | 26 + app/page.tsx | 7 +- components/ui/button.tsx | 65 ++- components/ui/card.tsx | 96 +++ components/ui/input.tsx | 20 + components/ui/label.tsx | 18 + components/ui/textarea.tsx | 18 + lib/sound.ts | 161 +++++ lib/utils.ts | 16 +- modules/landingPage/data/content.ts | 135 +++++ modules/landingPage/data/pricing.ts | 124 ++++ modules/landingPage/forms/contact-form.tsx | 193 ++++++ modules/landingPage/lib/contact-prefill.ts | 7 + modules/landingPage/lib/pricing.ts | 152 +++++ modules/landingPage/lib/tools.ts | 129 ++++ modules/landingPage/schema/contact-schema.ts | 35 ++ modules/landingPage/server/contact-action.ts | 50 ++ .../ui/components/about-section.tsx | 57 ++ .../ui/components/ai-visibility-section.tsx | 53 ++ .../ui/components/contact-section.tsx | 53 ++ .../landingPage/ui/components/count-up.tsx | 36 ++ .../ui/components/hero/hero-section.tsx | 83 +++ .../ui/components/hero/hero-shader.tsx | 330 +++++++++++ .../ui/components/hero/kinetic-headline.tsx | 127 ++++ .../ui/components/marquee-band.tsx | 94 +++ .../ui/components/pricing/animated-number.tsx | 49 ++ .../components/pricing/price-calculator.tsx | 376 ++++++++++++ .../ui/components/pricing/pricing-section.tsx | 26 + .../ui/components/process-section.tsx | 64 ++ .../ui/components/projects/project-visual.tsx | 105 ++++ .../components/projects/projects-showcase.tsx | 148 +++++ .../ui/components/scroll-statement.tsx | 68 +++ .../ui/components/services-section.tsx | 66 +++ .../ui/components/stats-section.tsx | 24 + .../ui/components/tools-section.tsx | 43 ++ .../ui/components/tools/contrast-tool.tsx | 73 +++ .../ui/components/tools/snippet-tool.tsx | 71 +++ .../ui/components/tools/tool-output.tsx | 81 +++ .../ui/components/tools/tool-panel.tsx | 94 +++ .../ui/views/landing-page-view.tsx | 42 ++ modules/shared/config/site.ts | 40 ++ modules/shared/lib/intro.ts | 25 + .../shared/ui/components/custom-cursor.tsx | 114 ++++ .../shared/ui/components/inline-script.tsx | 13 + modules/shared/ui/components/live-clock.tsx | 30 + modules/shared/ui/components/magnetic.tsx | 55 ++ modules/shared/ui/components/preloader.tsx | 105 ++++ .../shared/ui/components/scramble-link.tsx | 62 ++ modules/shared/ui/components/site-footer.tsx | 91 +++ modules/shared/ui/components/site-header.tsx | 159 +++++ .../shared/ui/components/smooth-scroll.tsx | 31 + .../shared/ui/components/sound-effects.tsx | 56 ++ modules/shared/ui/components/sound-toggle.tsx | 60 ++ package-lock.json | 96 ++- package.json | 5 +- public/icons/icon-192.png | Bin 0 -> 10862 bytes public/icons/icon-512.png | Bin 0 -> 41100 bytes public/icons/icon-maskable-512.png | Bin 0 -> 26712 bytes public/pictures/since3-logo-weiss.svg | 1 + public/pictures/since3-logo.png | Bin 0 -> 145453 bytes public/pictures/since3-logo.svg | 12 + 67 files changed, 4884 insertions(+), 153 deletions(-) create mode 100644 app/apple-icon.png create mode 100644 app/fonts.ts create mode 100644 app/icon.svg create mode 100644 app/manifest.ts create mode 100644 components/ui/card.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 lib/sound.ts create mode 100644 modules/landingPage/data/content.ts create mode 100644 modules/landingPage/data/pricing.ts create mode 100644 modules/landingPage/forms/contact-form.tsx create mode 100644 modules/landingPage/lib/contact-prefill.ts create mode 100644 modules/landingPage/lib/pricing.ts create mode 100644 modules/landingPage/lib/tools.ts create mode 100644 modules/landingPage/schema/contact-schema.ts create mode 100644 modules/landingPage/server/contact-action.ts create mode 100644 modules/landingPage/ui/components/about-section.tsx create mode 100644 modules/landingPage/ui/components/ai-visibility-section.tsx create mode 100644 modules/landingPage/ui/components/contact-section.tsx create mode 100644 modules/landingPage/ui/components/count-up.tsx create mode 100644 modules/landingPage/ui/components/hero/hero-section.tsx create mode 100644 modules/landingPage/ui/components/hero/hero-shader.tsx create mode 100644 modules/landingPage/ui/components/hero/kinetic-headline.tsx create mode 100644 modules/landingPage/ui/components/marquee-band.tsx create mode 100644 modules/landingPage/ui/components/pricing/animated-number.tsx create mode 100644 modules/landingPage/ui/components/pricing/price-calculator.tsx create mode 100644 modules/landingPage/ui/components/pricing/pricing-section.tsx create mode 100644 modules/landingPage/ui/components/process-section.tsx create mode 100644 modules/landingPage/ui/components/projects/project-visual.tsx create mode 100644 modules/landingPage/ui/components/projects/projects-showcase.tsx create mode 100644 modules/landingPage/ui/components/scroll-statement.tsx create mode 100644 modules/landingPage/ui/components/services-section.tsx create mode 100644 modules/landingPage/ui/components/stats-section.tsx create mode 100644 modules/landingPage/ui/components/tools-section.tsx create mode 100644 modules/landingPage/ui/components/tools/contrast-tool.tsx create mode 100644 modules/landingPage/ui/components/tools/snippet-tool.tsx create mode 100644 modules/landingPage/ui/components/tools/tool-output.tsx create mode 100644 modules/landingPage/ui/components/tools/tool-panel.tsx create mode 100644 modules/landingPage/ui/views/landing-page-view.tsx create mode 100644 modules/shared/config/site.ts create mode 100644 modules/shared/lib/intro.ts create mode 100644 modules/shared/ui/components/custom-cursor.tsx create mode 100644 modules/shared/ui/components/inline-script.tsx create mode 100644 modules/shared/ui/components/live-clock.tsx create mode 100644 modules/shared/ui/components/magnetic.tsx create mode 100644 modules/shared/ui/components/preloader.tsx create mode 100644 modules/shared/ui/components/scramble-link.tsx create mode 100644 modules/shared/ui/components/site-footer.tsx create mode 100644 modules/shared/ui/components/site-header.tsx create mode 100644 modules/shared/ui/components/smooth-scroll.tsx create mode 100644 modules/shared/ui/components/sound-effects.tsx create mode 100644 modules/shared/ui/components/sound-toggle.tsx create mode 100644 public/icons/icon-192.png create mode 100644 public/icons/icon-512.png create mode 100644 public/icons/icon-maskable-512.png create mode 100644 public/pictures/since3-logo-weiss.svg create mode 100644 public/pictures/since3-logo.png create mode 100644 public/pictures/since3-logo.svg diff --git a/app/apple-icon.png b/app/apple-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e96357341f784c3e68111ade4aa6394002530d56 GIT binary patch literal 6989 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 kPT?ZxpJI6bDPU}jz>4SoS4Q67(N8c`l{6Kr@~ literal 0 HcmV?d00001 diff --git a/app/favicon.ico b/app/favicon.ico index 718d6fea4835ec2d246af9800eddb7ffb276240c..22c19a30c1955d123261ad25ff61b41f5a055f81 100644 GIT binary patch literal 4142 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 Bu_yol literal 25931 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 LrJugUA?W`A8`#=m diff --git a/app/fonts.ts b/app/fonts.ts new file mode 100644 index 0000000..a7b8468 --- /dev/null +++ b/app/fonts.ts @@ -0,0 +1,21 @@ +import { JetBrains_Mono, Mona_Sans } from "next/font/google"; + +// next/font downloads the files at build time and serves them from our own +// domain, so no request ever reaches Google (no extra roundtrip, GDPR-safe). + +export const sans = Mona_Sans({ + subsets: ["latin", "latin-ext"], + // The width axis is the main typographic lever (see DESIGN.md §3) + axes: ["wdth"], + variable: "--font-mona", + display: "swap", + preload: true, +}); + +export const mono = JetBrains_Mono({ + subsets: ["latin", "latin-ext"], + variable: "--font-jetbrains", + display: "swap", + // Only needed for small labels, not for the LCP element + preload: false, +}); diff --git a/app/globals.css b/app/globals.css index c56032b..67e4668 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,129 +2,474 @@ @import "tw-animate-css"; @import "shadcn/tailwind.css"; -@custom-variant dark (&:is(.dark *)); +@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); + +/* ── Design tokens ─────────────────────────────────────────────────────── */ + +@theme { + /* Ink scale: luminance only, no hue */ + --color-ink-0: oklch(1 0 0); + --color-ink-50: oklch(0.975 0 0); + --color-ink-100: oklch(0.945 0 0); + --color-ink-200: oklch(0.885 0 0); + --color-ink-300: oklch(0.785 0 0); + --color-ink-400: oklch(0.665 0 0); + --color-ink-500: oklch(0.555 0 0); + --color-ink-600: oklch(0.45 0 0); + --color-ink-700: oklch(0.35 0 0); + --color-ink-800: oklch(0.245 0 0); + --color-ink-900: oklch(0.155 0 0); + --color-ink-950: oklch(0.085 0 0); + --color-ink-1000: oklch(0 0 0); + + --color-signal-error: oklch(0.63 0.21 25); + --color-signal-success: oklch(0.72 0.17 150); + + /* Type scale */ + --text-mega: clamp(3.25rem, 12.5vw, 14rem); + --text-mega--line-height: 0.84; + --text-display: clamp(2.75rem, 8.5vw, 8.5rem); + --text-display--line-height: 0.86; + --text-statement: clamp(2rem, 5vw, 4.75rem); + --text-statement--line-height: 1; + --text-title: clamp(1.5rem, 2.6vw, 2.5rem); + --text-title--line-height: 1.1; + --text-lead: clamp(1.125rem, 1.5vw, 1.375rem); + --text-lead--line-height: 1.5; + --text-body: 1.0625rem; + --text-body--line-height: 1.6; + --text-label: 0.8125rem; + --text-label--line-height: 1.4; + + /* Space and layout */ + --spacing-section: clamp(7rem, 16vh, 14rem); + --spacing-gutter: clamp(1.25rem, 4vw, 4rem); + --container-site: 90rem; + --radius-glass: 1.5rem; + + --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in-out-quart: cubic-bezier(0.76, 0, 0.24, 1); +} @theme inline { + --font-sans: var(--font-mona), ui-sans-serif, system-ui, sans-serif; + --font-mono: var(--font-jetbrains), ui-monospace, monospace; + --font-heading: var(--font-sans); + + --color-bg: var(--bg); + --color-fg: var(--fg); + --color-fg-muted: var(--fg-muted); + --color-fg-subtle: var(--fg-subtle); + --color-line: var(--line); + --color-surface: var(--surface); + + /* shadcn/ui bridge */ --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-sans); - --font-mono: var(--font-geist-mono); - --font-heading: var(--font-sans); - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar: var(--sidebar); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --color-ring: var(--ring); - --color-input: var(--input); - --color-border: var(--border); - --color-destructive: var(--destructive); - --color-accent-foreground: var(--accent-foreground); - --color-accent: var(--accent); - --color-muted-foreground: var(--muted-foreground); - --color-muted: var(--muted); - --color-secondary-foreground: var(--secondary-foreground); - --color-secondary: var(--secondary); - --color-primary-foreground: var(--primary-foreground); - --color-primary: var(--primary); - --color-popover-foreground: var(--popover-foreground); - --color-popover: var(--popover); - --color-card-foreground: var(--card-foreground); --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); --radius-sm: calc(var(--radius) * 0.6); --radius-md: calc(var(--radius) * 0.8); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) * 1.4); --radius-2xl: calc(var(--radius) * 1.8); - --radius-3xl: calc(var(--radius) * 2.2); - --radius-4xl: calc(var(--radius) * 2.6); +} + +/* ── Themes ────────────────────────────────────────────────────────────── */ + +:root, +[data-theme="light"] { + --bg: var(--color-ink-0); + --fg: var(--color-ink-1000); + --fg-muted: var(--color-ink-600); + --fg-subtle: var(--color-ink-400); + --line: var(--color-ink-200); + --surface: var(--color-ink-100); +} + +[data-theme="dark"] { + --bg: var(--color-ink-1000); + --fg: var(--color-ink-0); + --fg-muted: var(--color-ink-400); + --fg-subtle: var(--color-ink-600); + --line: var(--color-ink-800); + --surface: var(--color-ink-900); } :root { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.87 0 0); - --chart-2: oklch(0.556 0 0); - --chart-3: oklch(0.439 0 0); - --chart-4: oklch(0.371 0 0); - --chart-5: oklch(0.269 0 0); - --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --radius: 0.75rem; + /* Static light position for the glass edge highlight */ + --light-x: 30%; + --light-y: 0%; } -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.87 0 0); - --chart-2: oklch(0.556 0 0); - --chart-3: oklch(0.439 0 0); - --chart-4: oklch(0.371 0 0); - --chart-5: oklch(0.269 0 0); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); +/* + * Custom properties resolve where they are declared, so the shadcn mapping + * is re-declared on every themed element. + */ +:root, +[data-theme] { + --background: var(--bg); + --foreground: var(--fg); + --card: var(--bg); + --card-foreground: var(--fg); + --popover: var(--bg); + --popover-foreground: var(--fg); + --primary: var(--fg); + --primary-foreground: var(--bg); + --secondary: var(--surface); + --secondary-foreground: var(--fg); + --muted: var(--surface); + --muted-foreground: var(--fg-muted); + --accent: var(--surface); + --accent-foreground: var(--fg); + --destructive: var(--color-signal-error); + --border: var(--line); + --input: var(--color-ink-500); + --ring: var(--fg); } @layer base { * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; + @apply border-border; } html { @apply font-sans; + scroll-padding-top: 6rem; } -} \ No newline at end of file + body { + @apply bg-background text-foreground; + font-size: var(--text-body); + line-height: 1.6; + } + [data-theme] { + background-color: var(--bg); + color: var(--fg); + } + ::selection { + background: var(--fg); + color: var(--bg); + } +} + +:where(a, button, input, textarea, select, summary, [tabindex]):focus-visible { + outline: 2px solid var(--fg); + outline-offset: 3px; + box-shadow: 0 0 0 5px var(--bg); +} + +/* ── Smooth scroll (Lenis) ────────────────────────────────────────────── */ + +html.lenis, +html.lenis body { + height: auto; +} +.lenis:not(.lenis-autoToggle).lenis-stopped { + overflow: clip; +} +.lenis [data-lenis-prevent] { + overscroll-behavior: contain; +} +.lenis.lenis-smooth iframe { + pointer-events: none; +} + +/* ── Layout & type utilities ──────────────────────────────────────────── */ + +@utility container-site { + width: 100%; + max-width: var(--container-site); + margin-inline: auto; + padding-inline: var(--spacing-gutter); +} + +/* Poster type: heavy, condensed, uppercase */ +@utility type-poster { + font-variation-settings: "wght" 800, "wdth" 75; + text-transform: uppercase; + letter-spacing: -0.01em; + text-wrap: balance; +} + +@utility type-statement { + font-size: var(--text-statement); + font-variation-settings: "wght" 560, "wdth" 90; + line-height: 1; + letter-spacing: -0.025em; + text-wrap: balance; +} + +@utility type-title { + font-size: var(--text-title); + font-variation-settings: "wght" 600, "wdth" 90; + line-height: 1.1; + letter-spacing: -0.02em; + text-wrap: balance; +} + +@utility type-body { + line-height: 1.6; + max-width: 62ch; + text-wrap: pretty; +} + +@utility type-label { + font-family: var(--font-mono); + font-size: var(--text-label); + color: var(--fg-muted); +} + +/* + * Outlined type. Variable fonts have overlapping contours that a plain stroke + * would draw inside the letters, so the stroke is painted first at double + * width and the fill (in the background color) covers its inner half. + */ +@utility text-outline { + color: var(--bg); + -webkit-text-stroke: max(2px, 0.024em) var(--stroke-color, var(--fg)); + paint-order: stroke fill; +} + +/* ── Grain over everything ────────────────────────────────────────────── */ + +body::after { + content: ""; + position: fixed; + inset: -50%; + z-index: 90; + pointer-events: none; + opacity: 0.06; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 1.2 -0.1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); +} + +/* ── Glass ────────────────────────────────────────────────────────────── */ + +.glass { + position: relative; + isolation: isolate; + background: linear-gradient( + 135deg, + rgb(255 255 255 / 0.08), + rgb(255 255 255 / 0.02) + ); + backdrop-filter: blur(24px) saturate(1.2); + border: 1px solid rgb(255 255 255 / 0.12); + border-radius: var(--radius-glass); + box-shadow: + 0 1px 0 0 rgb(255 255 255 / 0.14) inset, + 0 30px 80px -30px rgb(0 0 0 / 0.6); +} +.glass::before { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + border-radius: inherit; + pointer-events: none; + background: radial-gradient( + 120% 90% at var(--light-x) var(--light-y), + rgb(255 255 255 / 0.1), + transparent 60% + ); +} +@media (prefers-reduced-transparency: reduce) { + .glass { + backdrop-filter: none; + background: var(--color-ink-900); + } +} + +/* ── Intro gating ─────────────────────────────────────────────────────── */ + +/* + * An inline script adds `is-intro` before the first paint when the preloader + * should play. Everything that animates in after the preloader is paused while + * that class is present. Without JavaScript the class never appears and the + * page simply animates in. + */ +.preloader { + display: none; + clip-path: inset(0 0 0 0); + transition: clip-path 1000ms var(--ease-in-out-quart); +} +html.is-intro .preloader, +.preloader[data-state="leaving"] { + display: flex; +} +.preloader[data-state="leaving"] { + clip-path: inset(0 0 100% 0); +} +html.is-intro { + overflow: hidden; +} +html.is-intro :is(.hero-char, .hero-fade) { + animation-play-state: paused; +} + +@keyframes char-in { + from { + translate: 0 105%; + rotate: 6deg; + } + to { + translate: 0 0; + rotate: 0deg; + } +} +@keyframes fade-up { + from { + opacity: 0; + translate: 0 1.5rem; + } +} + +.hero-char { + display: inline-block; + transform-origin: 0 100%; + animation: char-in 1100ms var(--ease-out-expo) calc(150ms + var(--i) * 28ms) both; +} +.hero-fade { + animation: fade-up 1200ms var(--ease-out-expo) calc(700ms + var(--d, 0ms)) both; +} + +/* ── Header: inverts itself over light and dark sections ──────────────── */ + +.site-header { + mix-blend-mode: difference; + color: white; + transition: translate 500ms var(--ease-in-out-quart); +} +.site-header[data-hidden="true"] { + translate: 0 -120%; +} + +/* ── Sound toggle equalizer ───────────────────────────────────────────── */ + +@keyframes equalizer { + 0%, + 100% { + scale: 1 0.25; + } + 50% { + scale: 1 1; + } +} +@utility animate-equalizer { + animation: equalizer 0.9s ease-in-out infinite; +} + +/* ── Custom cursor ────────────────────────────────────────────────────── */ + +@media (hover: hover) and (pointer: fine) { + html.has-cursor, + html.has-cursor * { + cursor: none !important; + } + html.has-cursor :is(input, textarea, select) { + cursor: text !important; + } +} + +/* ── Services: rows wipe to inverted on hover ─────────────────────────── */ + +.service-row { + position: relative; + isolation: isolate; +} +.service-row::before { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + background: var(--fg); + scale: 1 0; + transform-origin: 50% 100%; + transition: scale 600ms var(--ease-in-out-quart); +} +.service-row :is(.service-name, .service-meta) { + transition: + color 400ms var(--ease-in-out-quart), + translate 600ms var(--ease-in-out-quart); +} +.service-row .service-details { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 600ms var(--ease-in-out-quart); +} +@media (hover: hover) and (pointer: fine) { + .service-row:hover::before, + .service-row:focus-within::before { + scale: 1 1; + transform-origin: 50% 0%; + } + .service-row:is(:hover, :focus-within) :is(.service-name, .service-meta) { + color: var(--bg); + } + .service-row:is(:hover, :focus-within) .service-name { + translate: 1.5rem 0; + } + .service-row:is(:hover, :focus-within) .service-details { + grid-template-rows: 1fr; + } +} +/* Touch devices: details are always visible */ +@media not ((hover: hover) and (pointer: fine)) { + .service-row .service-details { + grid-template-rows: 1fr; + } +} + +/* ── Scroll-driven reveal (progressive enhancement) ──────────────────── */ + +@supports (animation-timeline: view()) { + .reveal { + animation: fade-up linear both; + animation-timeline: view(); + animation-range: entry 5% cover 30%; + } + .clip-reveal { + animation: clip-reveal linear both; + animation-timeline: view(); + animation-range: entry 0% cover 40%; + } + @keyframes clip-reveal { + from { + clip-path: inset(18% 8% 18% 8% round 3rem); + } + to { + clip-path: inset(0 0 0 0 round var(--radius-glass)); + } + } +} + +/* ── Reduced motion ───────────────────────────────────────────────────── */ + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-delay: 0ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + .reveal, + .clip-reveal { + animation: none !important; + } +} diff --git a/app/icon.svg b/app/icon.svg new file mode 100644 index 0000000..4f3109e --- /dev/null +++ b/app/icon.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/layout.tsx b/app/layout.tsx index 9852c15..fa6482d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,29 +1,69 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import type { Metadata, Viewport } from "next"; + +import { site } from "@/modules/shared/config/site"; +import { introScript } from "@/modules/shared/lib/intro"; +import { CustomCursor } from "@/modules/shared/ui/components/custom-cursor"; +import { InlineScript } from "@/modules/shared/ui/components/inline-script"; +import { Preloader } from "@/modules/shared/ui/components/preloader"; +import { SiteFooter } from "@/modules/shared/ui/components/site-footer"; +import { SiteHeader } from "@/modules/shared/ui/components/site-header"; +import { SmoothScroll } from "@/modules/shared/ui/components/smooth-scroll"; +import { SoundEffects } from "@/modules/shared/ui/components/sound-effects"; + +import { mono, sans } from "./fonts"; import "./globals.css"; -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + metadataBase: new URL(site.url), + title: { + default: "since3 – Websites und Software aus Steinfurt", + template: "%s – since3", + }, + description: site.description, + openGraph: { + type: "website", + locale: "de_DE", + siteName: site.name, + url: site.url, + }, +}; + +export const viewport: Viewport = { + themeColor: "#000000", + colorScheme: "dark", }; export default function RootLayout({ children }: LayoutProps<"/">) { return ( + // The intro script adds a class before hydration - {children} + + + + +
+ + Zum Inhalt springen + + + + + + + + +
+ {children} +
+ + ); } diff --git a/app/manifest.ts b/app/manifest.ts new file mode 100644 index 0000000..abe03c9 --- /dev/null +++ b/app/manifest.ts @@ -0,0 +1,26 @@ +import type { MetadataRoute } from "next"; + +import { site } from "@/modules/shared/config/site"; + +// Used when the site is added to a home screen (Android, desktop PWA install) +export default function manifest(): MetadataRoute.Manifest { + return { + name: "since3 – Digitalagentur aus Steinfurt", + short_name: site.name, + description: site.description, + start_url: "/", + display: "standalone", + background_color: "#000000", + theme_color: "#000000", + icons: [ + { src: "/icons/icon-192.png", sizes: "192x192", type: "image/png" }, + { src: "/icons/icon-512.png", sizes: "512x512", type: "image/png" }, + { + src: "/icons/icon-maskable-512.png", + sizes: "512x512", + type: "image/png", + purpose: "maskable", + }, + ], + }; +} diff --git a/app/page.tsx b/app/page.tsx index 8801369..d5cd104 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,8 +1,5 @@ +import { LandingPageView } from "@/modules/landingPage/ui/views/landing-page-view"; export default function Home() { - return ( -
- -
- ); + return ; } diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 997e0c7..97433b4 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -1,39 +1,36 @@ import { Button as ButtonPrimitive } from "@base-ui/react/button" import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "cn" +import { ArrowRight } from "lucide-react" + +import { cn } from "@/lib/utils" const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "group/button relative inline-flex shrink-0 items-center justify-center rounded-full border font-medium whitespace-nowrap transition-[background-color,border-color,color,scale] duration-200 ease-out select-none active:not-aria-[haspopup]:scale-[0.97] disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/80", + // Black on light, white on dark + solid: + "border-transparent bg-fg text-bg hover:bg-ink-800 dark:hover:bg-ink-200", + // Hairline only outline: - "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", - ghost: - "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", - destructive: - "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", - link: "text-primary underline-offset-4 hover:underline", + "border-fg/20 bg-transparent text-fg hover:border-fg/60 aria-expanded:border-fg/60", + // Only on top of glass surfaces or dark sections + glass: + "border-white/15 bg-white/10 text-white backdrop-blur-md hover:bg-white/20", + ghost: "border-transparent text-fg hover:bg-fg/5 aria-expanded:bg-fg/5", + link: "rounded-none border-transparent px-0 text-fg underline-offset-4 hover:underline", }, size: { - default: - "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", - lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - icon: "size-8", - "icon-xs": - "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", - "icon-sm": - "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", - "icon-lg": "size-9", + default: "h-11 gap-2 px-5 text-[0.9375rem]", + sm: "h-9 gap-1.5 px-4 text-sm", + lg: "h-13 gap-2 px-7 text-base", + icon: "size-10", + "icon-sm": "size-8", }, }, defaultVariants: { - variant: "default", + variant: "solid", size: "default", }, } @@ -41,7 +38,7 @@ const buttonVariants = cva( function Button({ className, - variant = "default", + variant = "solid", size = "default", ...props }: ButtonPrimitive.Props & VariantProps) { @@ -54,4 +51,22 @@ function Button({ ) } -export { Button, buttonVariants } +/** + * The arrow is not part of the label: it appears on hover only. + * Place it as the last child of a button or a link styled with `buttonVariants`. + */ +function ButtonArrow({ className }: { className?: string }) { + return ( + + ) +} + +export { Button, ButtonArrow, buttonVariants } diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..38d3386 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,96 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +/** + * Two strictly separate variants (DESIGN.md §8): + * - `flat`: hairline, no shadow, no blur. For lists. + * - `glass`: tool panels only. Never mix both in one section. + */ +const cardVariants = cva("flex flex-col text-card-foreground", { + variants: { + variant: { + flat: "card-flat rounded-2xl border border-line bg-transparent", + glass: "glass", + }, + }, + defaultVariants: { + variant: "flat", + }, +}) + +function Card({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"h3">) { + return ( +

+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"p">) { + return ( +

+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +

+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, + cardVariants, +} diff --git a/components/ui/input.tsx b/components/ui/input.tsx new file mode 100644 index 0000000..8886e9d --- /dev/null +++ b/components/ui/input.tsx @@ -0,0 +1,20 @@ +import * as React from "react" +import { Input as InputPrimitive } from "@base-ui/react/input" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/components/ui/label.tsx b/components/ui/label.tsx new file mode 100644 index 0000000..8219a7f --- /dev/null +++ b/components/ui/label.tsx @@ -0,0 +1,18 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Label({ className, ...props }: React.ComponentProps<"label">) { + return ( +