The source code for our eny.social landing page, which is mirrored in a different repository as part of the CI setup. eny.social
social-network eny local-first
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(pages): add legal pages and fix some minor issues

Sam Sauer 6579ad9f 48eba915

+264 -146
+7 -7
app/components/Footer.tsx
··· 6 6 7 7 export default function Footer() { 8 8 return ( 9 - <footer className="border-t border-charcoal/10 px-6 py-8"> 9 + <footer className="px-6 py-8"> 10 10 <div className="mx-auto grid max-w-[1536px] grid-cols-3 items-center gap-6 px-6"> 11 11 {/* Links */} 12 12 <NavMenu 13 13 items={[ 14 - { label: "imprint", href: "#" }, 15 - { label: "support", href: "#" }, 16 - { label: "privacy", href: "#" }, 14 + { label: "imprint", href: "/imprint" }, 15 + { label: "contact", href: "/contact" }, 16 + { label: "privacy", href: "https://krekeny.com/data-protection" }, 17 17 ]} 18 18 className="flex flex-col items-start gap-0" 19 19 baseDelay={0} ··· 30 30 className="text-charcoal/50 transition-colors hover:text-pacific" 31 31 aria-label="Bluesky" 32 32 > 33 - <svg className="h-5 w-5" viewBox="0 0 568 501" fill="currentColor"> 33 + <svg className="h-8 w-8" viewBox="0 0 568 501" fill="currentColor"> 34 34 <path d="M123.121 33.6637C188.241 82.5526 258.281 181.681 284 234.873C309.719 181.681 379.759 82.5526 444.879 33.6637C491.866 -1.61183 568 -28.9064 568 57.9464C568 75.2916 558.055 203.659 552.222 224.501C531.947 296.954 458.067 315.434 392.347 304.249C507.222 323.8 536.444 388.56 473.333 453.32C353.473 576.312 301.061 422.461 287.631 383.039C285.169 374.577 284.043 370.593 284 373.549C283.957 370.593 282.831 374.577 280.369 383.039C266.939 422.461 214.527 576.312 94.6667 453.32C31.5556 388.56 60.7778 323.8 175.653 304.249C109.933 315.434 36.0533 296.954 15.7778 224.501C9.94445 203.659 0 75.2916 0 57.9464C0 -28.9064 76.1345 -1.61183 123.121 33.6637Z" /> 35 35 </svg> 36 36 </a> ··· 43 43 className="text-charcoal/50 transition-colors hover:text-pacific" 44 44 aria-label="LinkedIn" 45 45 > 46 - <LinkedinLogo className="h-6 w-6" weight="regular" /> 46 + <LinkedinLogo className="h-9 w-9" weight="regular" /> 47 47 </a> 48 48 </FadeIn> 49 49 <FadeIn delay={480}> ··· 54 54 className="text-charcoal/50 transition-colors hover:text-pacific" 55 55 aria-label="Instagram" 56 56 > 57 - <InstagramLogo className="h-6 w-6" weight="regular" /> 57 + <InstagramLogo className="h-9 w-9" weight="regular" /> 58 58 </a> 59 59 </FadeIn> 60 60 </div>
+39 -69
app/components/GrainedBlob.tsx
··· 1 1 import { GRAIN_ENABLED } from "./ui/GrainFilter"; 2 2 3 + const NOISE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/><feColorMatrix type="saturate" values="0"/></filter><rect width="150" height="150" filter="url(#n)" opacity="1"/></svg>`; 4 + const noiseHref = `data:image/svg+xml,${encodeURIComponent(NOISE_SVG)}`; 5 + 6 + const ANIMATE_VALUES = ` 7 + M994.079 254.763C1273.76 285.396 1370.84 647.705 1143.95 814.073C1074.74 864.815 1030.37 943.802 1023.8 1029.36C1002.5 1306.34 644.568 1402.24 487.638 1173.02C439.161 1102.22 361.242 1056 275.94 1046.65C-3.73878 1016.02 -100.819 653.713 126.073 487.345C195.276 436.603 239.646 357.615 246.224 272.056C267.52 -4.91779 625.452 -100.825 782.381 128.393C830.858 199.2 908.777 245.42 994.079 254.763Z; 8 + M1014.08 244.76C1293.76 275.39 1360.84 657.71 1133.95 824.07C1064.74 874.82 1040.37 933.80 1033.80 1019.36C1012.50 1296.34 654.57 1412.24 497.64 1183.02C449.16 1112.22 351.24 1066.00 265.94 1056.65C-13.74 1026.02 -90.82 643.71 136.07 477.35C205.28 426.60 249.65 347.62 256.22 262.06C277.52 -14.92 635.45 -110.83 792.38 118.39C840.86 189.20 918.78 235.42 1014.08 244.76Z; 9 + M984.08 264.76C1263.76 295.40 1380.84 637.71 1153.95 804.07C1084.74 854.82 1020.37 953.80 1013.80 1039.36C992.50 1316.34 634.57 1392.24 477.64 1163.02C429.16 1092.22 371.24 1046.00 285.94 1036.65C6.26 1006.02 -110.82 663.71 116.07 497.35C185.28 446.60 229.65 367.62 236.22 282.06C257.52 5.08 615.45 -90.83 772.38 138.39C820.86 209.20 898.78 255.42 984.08 264.76Z; 10 + M994.079 254.763C1273.76 285.396 1370.84 647.705 1143.95 814.073C1074.74 864.815 1030.37 943.802 1023.8 1029.36C1002.5 1306.34 644.568 1402.24 487.638 1173.02C439.161 1102.22 361.242 1056 275.94 1046.65C-3.73878 1016.02 -100.819 653.713 126.073 487.345C195.276 436.603 239.646 357.615 246.224 272.056C267.52 -4.91779 625.452 -100.825 782.381 128.393C830.858 199.2 908.777 245.42 994.079 254.763Z 11 + `; 12 + 13 + const INITIAL_D = 14 + "M994.079 254.763C1273.76 285.396 1370.84 647.705 1143.95 814.073C1074.74 864.815 1030.37 943.802 1023.8 1029.36C1002.5 1306.34 644.568 1402.24 487.638 1173.02C439.161 1102.22 361.242 1056 275.94 1046.65C-3.73878 1016.02 -100.819 653.713 126.073 487.345C195.276 436.603 239.646 357.615 246.224 272.056C267.52 -4.91779 625.452 -100.825 782.381 128.393C830.858 199.2 908.777 245.42 994.079 254.763Z"; 15 + 3 16 interface GrainedBlobProps { 4 17 className?: string; 5 18 } ··· 12 25 fill="none" 13 26 xmlns="http://www.w3.org/2000/svg" 14 27 > 15 - <g filter={GRAIN_ENABLED ? "url(#grain-filter)" : undefined}> 28 + {GRAIN_ENABLED && ( 29 + <defs> 30 + <pattern 31 + id="blob-grain" 32 + patternUnits="userSpaceOnUse" 33 + width="50" 34 + height="50" 35 + > 36 + <image href={noiseHref} width="150" height="150" /> 37 + </pattern> 38 + </defs> 39 + )} 40 + <path d={INITIAL_D} fill="var(--cotton-candy)"> 41 + <animate 42 + attributeName="d" 43 + dur="14s" 44 + repeatCount="indefinite" 45 + values={ANIMATE_VALUES} 46 + /> 47 + </path> 48 + {GRAIN_ENABLED && ( 16 49 <path 17 - d="M994.079 254.763C1273.76 285.396 1370.84 647.705 1143.95 814.073C1074.74 864.815 1030.37 943.802 1023.8 1029.36C1002.5 1306.34 644.568 1402.24 487.638 1173.02C439.161 1102.22 361.242 1056 275.94 1046.65C-3.73878 1016.02 -100.819 653.713 126.073 487.345C195.276 436.603 239.646 357.615 246.224 272.056C267.52 -4.91779 625.452 -100.825 782.381 128.393C830.858 199.2 908.777 245.42 994.079 254.763Z" 18 - fill="var(--cotton-candy)" 50 + d={INITIAL_D} 51 + fill="url(#blob-grain)" 52 + opacity="0.45" 53 + style={{ mixBlendMode: "multiply" }} 19 54 > 20 55 <animate 21 56 attributeName="d" 22 57 dur="14s" 23 58 repeatCount="indefinite" 24 - values=" 25 - M994.079 254.763C1273.76 285.396 1370.84 647.705 1143.95 814.073C1074.74 864.815 1030.37 943.802 1023.8 1029.36C1002.5 1306.34 644.568 1402.24 487.638 1173.02C439.161 1102.22 361.242 1056 275.94 1046.65C-3.73878 1016.02 -100.819 653.713 126.073 487.345C195.276 436.603 239.646 357.615 246.224 272.056C267.52 -4.91779 625.452 -100.825 782.381 128.393C830.858 199.2 908.777 245.42 994.079 254.763Z; 26 - M1014.08 244.76C1293.76 275.39 1360.84 657.71 1133.95 824.07C1064.74 874.82 1040.37 933.80 1033.80 1019.36C1012.50 1296.34 654.57 1412.24 497.64 1183.02C449.16 1112.22 351.24 1066.00 265.94 1056.65C-13.74 1026.02 -90.82 643.71 136.07 477.35C205.28 426.60 249.65 347.62 256.22 262.06C277.52 -14.92 635.45 -110.83 792.38 118.39C840.86 189.20 918.78 235.42 1014.08 244.76Z; 27 - M984.08 264.76C1263.76 295.40 1380.84 637.71 1153.95 804.07C1084.74 854.82 1020.37 953.80 1013.80 1039.36C992.50 1316.34 634.57 1392.24 477.64 1163.02C429.16 1092.22 371.24 1046.00 285.94 1036.65C6.26 1006.02 -110.82 663.71 116.07 497.35C185.28 446.60 229.65 367.62 236.22 282.06C257.52 5.08 615.45 -90.83 772.38 138.39C820.86 209.20 898.78 255.42 984.08 264.76Z; 28 - M994.079 254.763C1273.76 285.396 1370.84 647.705 1143.95 814.073C1074.74 864.815 1030.37 943.802 1023.8 1029.36C1002.5 1306.34 644.568 1402.24 487.638 1173.02C439.161 1102.22 361.242 1056 275.94 1046.65C-3.73878 1016.02 -100.819 653.713 126.073 487.345C195.276 436.603 239.646 357.615 246.224 272.056C267.52 -4.91779 625.452 -100.825 782.381 128.393C830.858 199.2 908.777 245.42 994.079 254.763Z 29 - " 59 + values={ANIMATE_VALUES} 30 60 /> 31 61 </path> 32 - </g> 33 - {GRAIN_ENABLED && ( 34 - <defs> 35 - <filter 36 - id="grain-filter" 37 - x="0" 38 - y="0" 39 - width="1270.02" 40 - height="1301.42" 41 - filterUnits="userSpaceOnUse" 42 - colorInterpolationFilters="sRGB" 43 - > 44 - <feFlood floodOpacity="0" result="BackgroundImageFix" /> 45 - <feBlend 46 - mode="normal" 47 - in="SourceGraphic" 48 - in2="BackgroundImageFix" 49 - result="shape" 50 - /> 51 - <feTurbulence 52 - type="fractalNoise" 53 - baseFrequency="0.75 0.75" 54 - stitchTiles="stitch" 55 - numOctaves={1} 56 - result="noise" 57 - seed={939} 58 - /> 59 - <feColorMatrix 60 - in="noise" 61 - type="luminanceToAlpha" 62 - result="alphaNoise" 63 - /> 64 - <feComponentTransfer in="alphaNoise" result="coloredNoise1"> 65 - <feFuncA 66 - type="discrete" 67 - tableValues="1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0" 68 - /> 69 - </feComponentTransfer> 70 - <feComposite 71 - operator="in" 72 - in2="shape" 73 - in="coloredNoise1" 74 - result="noise1Clipped" 75 - /> 76 - <feFlood 77 - floodColor="color-mix(in srgb, var(--cotton-candy) 75%, #DF6666)" 78 - result="color1Flood" 79 - /> 80 - <feComposite 81 - operator="in" 82 - in2="noise1Clipped" 83 - in="color1Flood" 84 - result="color1" 85 - /> 86 - <feMerge result="effect1_noise"> 87 - <feMergeNode in="shape" /> 88 - <feMergeNode in="color1" /> 89 - </feMerge> 90 - </filter> 91 - </defs> 92 62 )} 93 63 </svg> 94 64 );
+1 -3
app/components/Hero.tsx
··· 103 103 </strong>{" "} 104 104 shaping your reality. 105 105 <br /> 106 - Built and hosted in <strong>Europe</strong>. 107 - <br /> 108 - Decentralized. 106 + Decentralized. Built and hosted in <strong>Europe</strong>. 109 107 </p> 110 108 </FadeIn> 111 109 </div>
+3 -8
app/components/ValueCards.tsx
··· 1 1 import FadeIn from "./ui/FadeIn"; 2 - import { GRAIN_ENABLED } from "./ui/GrainFilter"; 2 + import { GRAIN_ENABLED, grainStyle } from "./ui/GrainFilter"; 3 3 4 4 export default function ValueCards() { 5 5 const cards = [ ··· 63 63 {GRAIN_ENABLED && ( 64 64 <div 65 65 className="pointer-events-none absolute inset-0" 66 - style={{ filter: "url(#grain)" }} 67 - > 68 - <div 69 - className="h-full w-full" 70 - style={{ backgroundColor: card.bg }} 71 - /> 72 - </div> 66 + style={grainStyle} 67 + /> 73 68 )} 74 69 {(card.label || card.body) && ( 75 70 <div
+7 -4
app/components/Waitlist.tsx
··· 144 144 width: avatar.size, 145 145 height: avatar.size, 146 146 transitionDelay: isVisible ? `${i * 120}ms` : "0ms", 147 - animation: isVisible 148 - ? `${i % 2 === 0 ? "float" : "float-slow"} ${ 149 - 6 + i 150 - }s ease-in-out infinite` 147 + animationName: isVisible 148 + ? i % 2 === 0 149 + ? "float" 150 + : "float-slow" 151 151 : "none", 152 + animationDuration: `${6 + i}s`, 153 + animationTimingFunction: "ease-in-out", 154 + animationIterationCount: "infinite", 152 155 animationDelay: `${avatar.delay}s`, 153 156 }} 154 157 >
+27 -52
app/components/ui/GrainFilter.tsx
··· 1 1 /** 2 - * Shared SVG grain filter definitions. 3 - * Render once (e.g. in layout or page root) — all components reference by id. 2 + * Shared SVG definitions + CSS grain overlay. 3 + * 4 + * Uses a tiny tiled SVG noise pattern as a CSS background — rendered once 5 + * by the browser, then composited as a static raster layer. This is 6 + * dramatically cheaper than live feTurbulence filters, especially on mobile. 4 7 * 5 8 * Toggle GRAIN_ENABLED to disable all grain effects site-wide. 6 9 */ 7 10 8 11 export const GRAIN_ENABLED = false; 9 12 13 + /** 14 + * Inline SVG data-URI that produces a 200×200 fractalNoise tile. 15 + * The browser rasterises it once; repeating it via background-image is free. 16 + */ 17 + const NOISE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/><feColorMatrix type="saturate" values="0"/></filter><rect width="200" height="200" filter="url(#n)" opacity="1"/></svg>`; 18 + 19 + export const noiseDataUri = `url("data:image/svg+xml,${encodeURIComponent( 20 + NOISE_SVG 21 + )}")`; 22 + 23 + /** 24 + * CSS properties you can spread onto any element to add a grain overlay. 25 + * Apply via a pseudo-element or an overlay div with pointer-events-none. 26 + */ 27 + export const grainStyle: React.CSSProperties = { 28 + backgroundImage: noiseDataUri, 29 + backgroundRepeat: "repeat", 30 + backgroundSize: "200px 200px", 31 + opacity: 0.4, 32 + mixBlendMode: "multiply", 33 + }; 34 + 10 35 export default function GrainFilter() { 11 36 return ( 12 37 <svg className="absolute h-0 w-0" aria-hidden="true"> ··· 18 43 d="M269.049 70.5681C302.25 60.8121 326.225 102.338 301.176 126.212C285.271 141.371 288.453 167.64 307.572 178.467L314.263 182.256C343.038 198.551 329.55 242.538 296.568 240.162C274.702 238.586 258.156 259.678 264.971 280.513C275.346 312.232 234.971 335.542 212.689 310.698C198.052 294.378 171.514 298.162 161.945 317.886C147.512 347.638 102.674 337.325 102.95 304.258L103.014 296.569C103.197 274.598 82.0388 258.707 60.9583 264.902C27.7579 274.658 3.78289 233.132 28.832 209.258C44.737 194.099 41.5545 167.83 22.4355 157.003L15.7443 153.214C-13.0301 136.919 0.457428 92.9322 33.44 95.3084C55.3054 96.8837 71.8515 75.7925 65.0365 54.9565C54.6619 23.238 95.0367 -0.0723286 117.318 24.7715C131.955 41.0915 158.494 37.3078 168.062 17.5841C182.496 -12.1677 227.333 -1.85504 227.058 31.2118L226.994 38.9011C226.811 60.8722 247.969 76.7627 269.049 70.5681Z" 19 44 /> 20 45 </clipPath> 21 - 22 - {/* Generic dark grain — used by ValueCards shapes */} 23 - {GRAIN_ENABLED && ( 24 - <filter 25 - id="grain" 26 - x="0" 27 - y="0" 28 - width="100%" 29 - height="100%" 30 - filterUnits="objectBoundingBox" 31 - colorInterpolationFilters="sRGB" 32 - > 33 - <feTurbulence 34 - type="fractalNoise" 35 - baseFrequency="0.5" 36 - numOctaves="3" 37 - stitchTiles="stitch" 38 - seed="1581" 39 - result="noise" 40 - /> 41 - <feColorMatrix 42 - in="noise" 43 - type="luminanceToAlpha" 44 - result="alphaNoise" 45 - /> 46 - <feComponentTransfer in="alphaNoise" result="coloredNoise"> 47 - <feFuncA 48 - type="discrete" 49 - tableValues="1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0" 50 - /> 51 - </feComponentTransfer> 52 - <feComposite 53 - operator="in" 54 - in2="SourceGraphic" 55 - in="coloredNoise" 56 - result="noiseClipped" 57 - /> 58 - <feFlood floodColor="rgba(0, 0, 0, 0.05)" result="colorFlood" /> 59 - <feComposite 60 - operator="in" 61 - in2="noiseClipped" 62 - in="colorFlood" 63 - result="colorNoise" 64 - /> 65 - <feMerge> 66 - <feMergeNode in="SourceGraphic" /> 67 - <feMergeNode in="colorNoise" /> 68 - </feMerge> 69 - </filter> 70 - )} 71 46 </defs> 72 47 </svg> 73 48 );
+77
app/contact/page.tsx
··· 1 + import Nav from "../components/Nav"; 2 + import Footer from "../components/Footer"; 3 + 4 + export default function ContactPage() { 5 + return ( 6 + <div className="overflow-x-hidden"> 7 + <Nav /> 8 + <main className="px-6 pt-32 pb-24"> 9 + <div className="mx-auto max-w-3xl"> 10 + <h1 className="mb-12">Contact</h1> 11 + 12 + <div className="prose space-y-6 text-charcoal/80"> 13 + <p> 14 + Have a question, idea, or just want to say hello? We&apos;d love 15 + to hear from you. 16 + </p> 17 + 18 + <h2 className="headline-label mt-10">Email</h2> 19 + <p> 20 + <a 21 + href="mailto:hello@krekeny.com" 22 + className="text-pacific underline" 23 + > 24 + hello@krekeny.com 25 + </a> 26 + </p> 27 + 28 + <h2 className="headline-label mt-10">Phone</h2> 29 + <p>+49 (0) 69 710 402 30</p> 30 + 31 + <h2 className="headline-label mt-10">Social</h2> 32 + <p> 33 + <a 34 + href="https://bsky.app/profile/eny.social" 35 + target="_blank" 36 + rel="noopener noreferrer" 37 + className="text-pacific underline" 38 + > 39 + Bluesky 40 + </a> 41 + {" · "} 42 + <a 43 + href="https://www.linkedin.com/company/krekeny/" 44 + target="_blank" 45 + rel="noopener noreferrer" 46 + className="text-pacific underline" 47 + > 48 + LinkedIn 49 + </a> 50 + {" · "} 51 + <a 52 + href="https://instagram.com/krekeny" 53 + target="_blank" 54 + rel="noopener noreferrer" 55 + className="text-pacific underline" 56 + > 57 + Instagram 58 + </a> 59 + </p> 60 + 61 + <h2 className="headline-label mt-10">Address</h2> 62 + <p> 63 + Krekeny GmbH 64 + <br /> 65 + Karlstr. 54 66 + <br /> 67 + 63065 Offenbach am Main 68 + <br /> 69 + Germany 70 + </p> 71 + </div> 72 + </div> 73 + </main> 74 + <Footer /> 75 + </div> 76 + ); 77 + }
+5
app/globals.css
··· 210 210 text-decoration-thickness: auto; 211 211 text-underline-offset: auto; 212 212 text-underline-position: from-font; 213 + transition: color 0.2s ease; 214 + } 215 + 216 + .nav-link:hover { 217 + color: var(--pacific-blue); 213 218 } 214 219 215 220 /* Quote */
+86
app/imprint/page.tsx
··· 1 + import Nav from "../components/Nav"; 2 + import Footer from "../components/Footer"; 3 + 4 + export default function ImprintPage() { 5 + return ( 6 + <div className="overflow-x-hidden"> 7 + <Nav /> 8 + <main className="px-6 pt-32 pb-24"> 9 + <div className="mx-auto max-w-3xl"> 10 + <h1 className="mb-12">Imprint</h1> 11 + 12 + <div className="prose space-y-6 text-charcoal/80"> 13 + <h2 className="headline-label">Information according to § 5 TMG</h2> 14 + <p> 15 + Krekeny GmbH 16 + <br /> 17 + Karlstr. 54 18 + <br /> 19 + 63065 Offenbach am Main 20 + <br /> 21 + Germany 22 + </p> 23 + 24 + <h2 className="headline-label mt-10">Represented by</h2> 25 + <p>Sam Sauer &amp; Michael Ehrich</p> 26 + 27 + <h2 className="headline-label mt-10">Contact</h2> 28 + <p> 29 + Phone: +49 (0) 69 710 402 30 30 + <br /> 31 + Fax: +49 (0) 69 407 667 81 32 + <br /> 33 + Email:{" "} 34 + <a 35 + href="mailto:hello@krekeny.com" 36 + className="text-pacific underline" 37 + > 38 + hello@krekeny.com 39 + </a> 40 + </p> 41 + 42 + <h2 className="headline-label mt-10">Commercial register</h2> 43 + <p> 44 + Registered at Amtsgericht Offenbach am Main 45 + <br /> 46 + Registration number: HRB 53756 47 + </p> 48 + 49 + <h2 className="headline-label mt-10">VAT ID</h2> 50 + <p>DE 3436 4277 9</p> 51 + 52 + <h2 className="headline-label mt-10"> 53 + Responsible for content according to § 55 Abs. 2 RStV 54 + </h2> 55 + <p> 56 + Michael Ehrich 57 + <br /> 58 + Karlstr. 54 59 + <br /> 60 + 63065 Offenbach am Main 61 + </p> 62 + 63 + <h2 className="headline-label mt-10">Dispute resolution</h2> 64 + <p> 65 + The European Commission provides a platform for online dispute 66 + resolution (OS):{" "} 67 + <a 68 + href="https://ec.europa.eu/consumers/odr" 69 + target="_blank" 70 + rel="noopener noreferrer" 71 + className="text-pacific underline" 72 + > 73 + https://ec.europa.eu/consumers/odr 74 + </a> 75 + . 76 + <br /> 77 + We are not willing or obliged to participate in dispute resolution 78 + proceedings before a consumer arbitration board. 79 + </p> 80 + </div> 81 + </div> 82 + </main> 83 + <Footer /> 84 + </div> 85 + ); 86 + }
+12 -3
app/offenbach/page.tsx
··· 5 5 import FadeIn from "../components/ui/FadeIn"; 6 6 import SectionIntroLabel from "../components/ui/SectionIntroLabel"; 7 7 import ButtonCta from "../components/ui/ButtonCta"; 8 + import { GRAIN_ENABLED, grainStyle } from "../components/ui/GrainFilter"; 8 9 9 10 export default function OffenbachPage() { 10 11 const features = [ ··· 69 70 {features.map((feature, i) => ( 70 71 <FadeIn key={feature.title} delay={i * 100}> 71 72 <div 72 - className="rounded-3xl p-8" 73 + className="relative overflow-hidden rounded-3xl p-8" 73 74 style={{ backgroundColor: feature.color }} 74 75 > 75 - <h3 className="headline-label mb-4">{feature.title}</h3> 76 - <p className="section-copy">{feature.description}</p> 76 + {GRAIN_ENABLED && ( 77 + <div 78 + className="pointer-events-none absolute inset-0" 79 + style={grainStyle} 80 + /> 81 + )} 82 + <div className="relative"> 83 + <h3 className="headline-label mb-4">{feature.title}</h3> 84 + <p className="section-copy">{feature.description}</p> 85 + </div> 77 86 </div> 78 87 </FadeIn> 79 88 ))}