grain.social is a photo sharing platform built on atproto.

add support pages and legal stuff, update config settings for pds

Changed files
+531 -10
local-infra
services
src
static
+5 -1
local-infra/pds.env
··· 11 11 PDS_DID_PLC_URL=http://plc:8080 12 12 PDS_HOSTNAME=pds.dev.grain.social 13 13 PDS_EMAIL_SMTP_URL=smtp://maildev:1025 14 - PDS_EMAIL_FROM_ADDRESS=admin@grain.social 14 + PDS_EMAIL_FROM_ADDRESS=support@grain.social 15 15 PDS_SERVICE_NAME=Grain Social 16 16 PDS_INVITE_REQUIRED=0 17 17 PDS_OAUTH_PROVIDER_NAME=Grain Social ··· 21 21 PDS_PRIMARY_COLOR=#00a6f4 22 22 PDS_PRIMARY_COLOR_CONTRAST=#fff 23 23 PDS_PRIMARY_COLOR_HUE=#fff 24 + PDS_TERMS_OF_SERVICE_URL = 'http://localhost:8080/support/terms' 25 + PDS_PRIVACY_POLICY_URL = 'http://localhost:8080/support/privacy' 26 + PDS_SUPPORT_URL= 'http://localhost:8080/support' 27 + PDS_CONTACT_EMAIL_ADDRESS = 'support@grain.social'
+1 -1
services/pds/.env.example
··· 22 22 23 23 # private: email 24 24 PDS_EMAIL_SMTP_URL=smtps://resend:<secret api key here>@smtp.resend.com:465/ 25 - PDS_EMAIL_FROM_ADDRESS=admin@your.domain 25 + PDS_EMAIL_FROM_ADDRESS=support@your.domain
+4
services/pds/fly.toml
··· 29 29 PDS_PRIMARY_COLOR = '#00a6f4' 30 30 PDS_PRIMARY_COLOR_CONTRAST = '#fff' 31 31 PDS_PRIMARY_COLOR_HUE = '#fff' 32 + PDS_TERMS_OF_SERVICE_URL = 'https://grain.social/support/terms' 33 + PDS_PRIVACY_POLICY_URL = 'https://grain.social/support/privacy' 34 + PDS_SUPPORT_URL= 'https://grain.social/support' 35 + PDS_CONTACT_EMAIL_ADDRESS = 'support@grain.social' 32 36 33 37 [[mounts]] 34 38 source = 'pdsdata'
+37 -8
src/components/LoginPage.tsx
··· 17 17 infoText="e.g., user.bsky.social, user.grain.social, example.com, https://pds.example.com" 18 18 infoClass="text-white text-sm! bg-zinc-950/70 p-4 font-mono" 19 19 /> 20 - <div class="absolute bottom-2 right-2 text-white text-sm"> 21 - Photo by{" "} 22 - <a 23 - href={profileLink("chadtmiller.com")} 24 - class="hover:underline font-semibold" 25 - > 26 - @chadtmiller.com 27 - </a> 20 + <div class="absolute bottom-2 left-2 right-2 flex flex-col sm:flex-row justify-between items-start sm:items-end text-white text-sm gap-1 sm:gap-0"> 21 + <div class="flex flex-col sm:flex-row"> 22 + <span> 23 + © 2025 Grain Social. All rights reserved. 24 + </span> 25 + <span class="flex flex-row items-center flex-wrap"> 26 + <a 27 + href="/support/terms" 28 + class="underline hover:no-underline ml-0 sm:ml-2 mt-1 sm:mt-0" 29 + > 30 + Terms 31 + </a> 32 + <span class="mx-1">|</span> 33 + <a 34 + href="/support/privacy" 35 + class="underline hover:no-underline ml-0 sm:ml-1 mt-1 sm:mt-0" 36 + > 37 + Privacy 38 + </a> 39 + <span class="mx-1">|</span> 40 + <a 41 + href="/support/copyright" 42 + class="underline hover:no-underline ml-0 sm:ml-1 mt-1 sm:mt-0" 43 + > 44 + Copyright 45 + </a> 46 + </span> 47 + </div> 48 + <div> 49 + Photo by{" "} 50 + <a 51 + href={profileLink("chadtmiller.com")} 52 + class="underline hover:no-underline" 53 + > 54 + @chadtmiller.com 55 + </a> 56 + </div> 28 57 </div> 29 58 </div> 30 59 );
+326
src/legal.tsx
··· 1 + import { ComponentChildren } from "preact"; 2 + 3 + type SectionProps = { 4 + title: string; 5 + children: ComponentChildren; 6 + }; 7 + 8 + const Section = ({ title, children }: SectionProps) => ( 9 + <section className="mb-8"> 10 + <h2 className="text-xl font-bold mb-2 text-zinc-800 dark:text-zinc-100"> 11 + {title} 12 + </h2> 13 + <div className="space-y-2 text-zinc-700 dark:text-zinc-300 text-sm"> 14 + {children} 15 + </div> 16 + </section> 17 + ); 18 + 19 + export function Terms() { 20 + return ( 21 + <div className="px-4 py-4"> 22 + <nav className="mb-4 text-sm text-zinc-500 dark:text-zinc-300"> 23 + <a href="/support" className="text-sky-500 hover:underline"> 24 + support 25 + </a>{" "} 26 + <span className="mx-1">&gt;</span>{" "} 27 + <span className="text-zinc-700 dark:text-zinc-100">terms</span> 28 + </nav> 29 + <h1 className="text-3xl font-bold mb-6 text-zinc-900 dark:text-white"> 30 + Terms and Conditions 31 + </h1> 32 + 33 + <div className="mb-6 text-sm text-zinc-900 dark:text-white"> 34 + Last Updated: June 3, 2025 35 + </div> 36 + 37 + <Section title="Overview"> 38 + <p> 39 + Grain is a photo sharing app built on the{" "} 40 + <a 41 + href="https://atproto.com/" 42 + className="text-sky-500 hover:underline" 43 + target="_blank" 44 + rel="noopener noreferrer" 45 + > 46 + AT Protocol 47 + </a> 48 + . All data, including photos, galleries, favorites, and metadata, is 49 + public and stored on the AT Protocol network. Users can upload photos, 50 + create and favorite galleries, and view non-location EXIF metadata. 51 + </p> 52 + <p> 53 + Grain is an open source project. These Terms apply to your use of the 54 + hosted version at{" "} 55 + <code>grain.social</code>, not to self-hosted instances or forks of 56 + the source code. 57 + </p> 58 + </Section> 59 + 60 + <Section title="Account and Data Ownership"> 61 + <p> 62 + Grain uses the AT Protocol, so users retain full control over their 63 + data. We are an independent project and not affiliated with Bluesky or 64 + the AT Protocol. 65 + </p> 66 + <p> 67 + If you use a <code>grain.social</code>{" "} 68 + handle, your data may be stored on our own self-hosted{" "} 69 + <a 70 + href="https://atproto.com/guides/glossary#pds-personal-data-server" 71 + className="text-sky-500 hover:underline" 72 + target="_blank" 73 + rel="noopener noreferrer" 74 + > 75 + PDS (Personal Data Server) 76 + </a>{" "} 77 + in accordance with protocol standards. 78 + </p> 79 + </Section> 80 + 81 + <Section title="Content"> 82 + <p> 83 + You are responsible for any content you share. Do not upload content 84 + you do not have rights to. All uploads are publicly visible and cannot 85 + currently be set as private. 86 + </p> 87 + </Section> 88 + 89 + <Section title="Analytics"> 90 + <p> 91 + We use{" "} 92 + <a 93 + href="https://www.goatcounter.com/" 94 + className="text-sky-500 hover:underline" 95 + > 96 + Goatcounter 97 + </a>{" "} 98 + for basic analytics. No personal data is collected, tracked, or sold. 99 + </p> 100 + </Section> 101 + 102 + <Section title="Prohibited Conduct"> 103 + <p> 104 + Do not upload illegal content, harass users, impersonate others, or 105 + attempt to disrupt the network. 106 + </p> 107 + </Section> 108 + 109 + <Section title="Disclaimers"> 110 + <p> 111 + Grain is provided "as is." We do not guarantee uptime, data retention, 112 + or uninterrupted access. 113 + </p> 114 + </Section> 115 + 116 + <Section title="Termination"> 117 + <p> 118 + We reserve the right to suspend or terminate your access to Grain at 119 + any time, without prior notice, for conduct that we believe violates 120 + these Terms, our community standards, or is harmful to other users or 121 + the AT Protocol network. Terminated accounts may lose access to 122 + uploaded content unless retained through the protocol’s data 123 + persistence mechanisms. 124 + </p> 125 + </Section> 126 + 127 + <Section title="Changes"> 128 + <p> 129 + We may update these terms periodically. Continued use means acceptance 130 + of any changes. 131 + </p> 132 + </Section> 133 + 134 + <Section title="Contact"> 135 + <p> 136 + For any questions about these Terms, your account, or issues with the 137 + app, you can contact us at{" "} 138 + <a 139 + href="mailto:support@grain.social" 140 + className="text-sky-500 hover:underline" 141 + > 142 + support@grain.social 143 + </a> 144 + . 145 + </p> 146 + </Section> 147 + </div> 148 + ); 149 + } 150 + 151 + export function PrivacyPolicy() { 152 + return ( 153 + <div className="px-4 py-4"> 154 + <nav className="mb-4 text-sm text-zinc-500 dark:text-zinc-300"> 155 + <a href="/support" className="text-sky-500 hover:underline"> 156 + support 157 + </a>{" "} 158 + <span className="mx-1">&gt;</span>{" "} 159 + <span className="text-zinc-700 dark:text-zinc-100">privacy</span> 160 + </nav> 161 + <h1 className="text-3xl font-bold mb-6 text-zinc-900 dark:text-white"> 162 + Privacy Policy 163 + </h1> 164 + 165 + <div className="mb-6 text-sm text-zinc-900 dark:text-white"> 166 + Last Updated: June 3, 2025 167 + </div> 168 + 169 + <Section title="Data Storage and Access"> 170 + <p> 171 + Your data is stored on the AT Protocol. If you use a{" "} 172 + <code>grain.social</code>{" "} 173 + handle, it may be stored on our PDS. We do not store or access data 174 + beyond the protocol’s standard behavior. 175 + </p> 176 + </Section> 177 + 178 + <Section title="Public Data"> 179 + <p> 180 + All content on Grain is public. Private uploads are not currently 181 + supported. 182 + </p> 183 + </Section> 184 + 185 + { 186 + /* Coming soon */ 187 + /* <Section title="EXIF Metadata"> 188 + <p> 189 + We optionally collect and display EXIF metadata (excluding location) 190 + from your photos. At upload time, you can choose whether to allow this 191 + metadata to be collected. The metadata is stored according to standard 192 + AT Protocol storage mechanisms and is not retained outside the 193 + protocol or used for other purposes. 194 + </p> 195 + <p> 196 + You can learn more about the types of metadata commonly embedded in 197 + photos at{" "} 198 + <a 199 + href="https://exiv2.org/tags.html" 200 + className="text-sky-500 hover:underline" 201 + target="_blank" 202 + rel="noopener noreferrer" 203 + > 204 + exiv2.org 205 + </a> 206 + . 207 + </p> 208 + </Section> */ 209 + } 210 + 211 + <Section title="Analytics"> 212 + <p> 213 + We use{" "} 214 + <a 215 + href="https://www.goatcounter.com/" 216 + className="text-sky-500 hover:underline" 217 + > 218 + Goatcounter 219 + </a>{" "} 220 + for analytics. It is privacy-focused: no IP addresses, cookies, or 221 + personal data is collected. 222 + </p> 223 + </Section> 224 + 225 + <Section title="No Ads or Tracking"> 226 + <p>We do not serve ads, use third-party tracking, or sell user data.</p> 227 + </Section> 228 + 229 + <Section title="Children’s Privacy"> 230 + <p>Grain is not intended for users under 13 years of age.</p> 231 + </Section> 232 + 233 + <Section title="Changes to Policy"> 234 + <p> 235 + This policy may be updated. Material changes will be communicated via 236 + the app or site. 237 + </p> 238 + </Section> 239 + 240 + <Section title="Contact"> 241 + <p> 242 + For privacy questions, contact us at{" "} 243 + <a 244 + href="mailto:support@grain.social" 245 + className="text-sky-500 hover:underline" 246 + > 247 + support@grain.social 248 + </a> 249 + . 250 + </p> 251 + </Section> 252 + </div> 253 + ); 254 + } 255 + 256 + export function CopyrightPolicy() { 257 + return ( 258 + <div className="px-4 py-4"> 259 + <nav className="mb-4 text-sm text-zinc-500 dark:text-zinc-300"> 260 + <a href="/support" className="text-sky-500 hover:underline"> 261 + support 262 + </a>{" "} 263 + <span className="mx-1">&gt;</span>{" "} 264 + <span className="text-zinc-700 dark:text-zinc-100">copyright</span> 265 + </nav> 266 + <h1 className="text-3xl font-bold mb-6 text-zinc-900 dark:text-white"> 267 + Copyright Policy 268 + </h1> 269 + 270 + <div className="mb-6 text-sm text-zinc-900 dark:text-white"> 271 + Last Updated: June 3, 2025 272 + </div> 273 + 274 + <Section title="Copyright Infringement"> 275 + <p> 276 + Grain respects the intellectual property rights of others and expects 277 + users to do the same. If you believe your copyrighted work has been 278 + used in a way that constitutes infringement, please notify us 279 + promptly. 280 + </p> 281 + </Section> 282 + 283 + <Section title="Notice Requirements"> 284 + <p> 285 + Your infringement notice must include: (1) a description of the 286 + copyrighted work, (2) the location of the infringing material, (3) 287 + your contact information, (4) a statement that you believe in good 288 + faith the use is not authorized, and (5) a statement, under penalty of 289 + perjury, that the information is accurate. 290 + </p> 291 + </Section> 292 + 293 + <Section title="DMCA Compliance"> 294 + <p> 295 + Grain complies with the Digital Millennium Copyright Act (DMCA). If 296 + you are a copyright holder and believe your rights have been violated, 297 + you may file a DMCA notice with the required information to our 298 + designated agent. We will promptly respond to all valid DMCA notices 299 + and take appropriate action, including removal of the infringing 300 + content and disabling access. 301 + </p> 302 + </Section> 303 + 304 + <Section title="Repeat Infringers"> 305 + <p> 306 + Accounts that repeatedly infringe copyright may be suspended or 307 + removed in accordance with AT Protocol and Grain Social’s moderation 308 + guidelines. 309 + </p> 310 + </Section> 311 + 312 + <Section title="Contact"> 313 + <p> 314 + To report infringement or submit a DMCA notice, contact us at{" "} 315 + <a 316 + href="mailto:support@grain.social" 317 + className="text-sky-500 hover:underline" 318 + > 319 + support@grain.social 320 + </a> 321 + . 322 + </p> 323 + </Section> 324 + </div> 325 + ); 326 + }
+6
src/main.tsx
··· 8 8 import * as dialogHandlers from "./routes/dialogs.tsx"; 9 9 import { handler as exploreHandler } from "./routes/explore.tsx"; 10 10 import { handler as galleryHandler } from "./routes/gallery.tsx"; 11 + import * as legalHandlers from "./routes/legal.tsx"; 11 12 import { handler as notificationsHandler } from "./routes/notifications.tsx"; 12 13 import { handler as onboardHandler } from "./routes/onboard.tsx"; 13 14 import { handler as profileHandler } from "./routes/profile.tsx"; 14 15 import { handler as recordHandler } from "./routes/record.ts"; 16 + import { handler as supportHandler } from "./routes/support.tsx"; 15 17 import { handler as timelineHandler } from "./routes/timeline.tsx"; 16 18 import { handler as uploadHandler } from "./routes/upload.tsx"; 17 19 import { appStateMiddleware, type State } from "./state.ts"; ··· 57 59 route("/profile/:handle/gallery/:rkey", galleryHandler), 58 60 route("/upload", uploadHandler), 59 61 route("/onboard", onboardHandler), 62 + route("/support", supportHandler), 63 + route("/support/privacy", legalHandlers.privacyHandler), 64 + route("/support/terms", legalHandlers.termsHandler), 65 + route("/support/copyright", legalHandlers.copyrightHandler), 60 66 route("/dialogs/create-account", dialogHandlers.createAccount), 61 67 route("/dialogs/gallery/new", dialogHandlers.createGallery), 62 68 route("/dialogs/gallery/:rkey", dialogHandlers.editGallery),
+33
src/routes/legal.tsx
··· 1 + import { BffContext, RouteHandler } from "@bigmoves/bff"; 2 + import { CopyrightPolicy, PrivacyPolicy, Terms } from "../legal.tsx"; 3 + import type { State } from "../state.ts"; 4 + 5 + export const termsHandler: RouteHandler = ( 6 + _req, 7 + _params, 8 + ctx: BffContext<State>, 9 + ) => { 10 + return ctx.render( 11 + <Terms />, 12 + ); 13 + }; 14 + 15 + export const privacyHandler: RouteHandler = ( 16 + _req, 17 + _params, 18 + ctx: BffContext<State>, 19 + ) => { 20 + return ctx.render( 21 + <PrivacyPolicy />, 22 + ); 23 + }; 24 + 25 + export const copyrightHandler: RouteHandler = ( 26 + _req, 27 + _params, 28 + ctx: BffContext<State>, 29 + ) => { 30 + return ctx.render( 31 + <CopyrightPolicy />, 32 + ); 33 + };
+33
src/routes/support.tsx
··· 1 + import { RouteHandler } from "@bigmoves/bff"; 2 + 3 + export const handler: RouteHandler = (_req, _params, ctx) => { 4 + ctx.state.meta = [{ title: "Support — Grain" }]; 5 + return ctx.render( 6 + <div className="px-4 py-4"> 7 + <h1 className="text-3xl font-bold mb-4 text-zinc-900 dark:text-white"> 8 + Support 9 + </h1> 10 + <p className="mb-4 text-zinc-700 dark:text-zinc-300"> 11 + For help, questions, or to report issues, please email us at{" "} 12 + <a 13 + href="mailto:support@grain.social" 14 + className="text-sky-500 hover:underline" 15 + > 16 + support@grain.social 17 + </a>. 18 + </p> 19 + <p className="mb-2 text-zinc-700 dark:text-zinc-300"> 20 + You can also review our{" "} 21 + <a href="/support/terms" className="text-sky-500 hover:underline"> 22 + Terms 23 + </a>,{" "} 24 + <a href="/support/privacy" className="text-sky-500 hover:underline"> 25 + Privacy Policy 26 + </a>, and{" "} 27 + <a href="/support/copyright" className="text-sky-500 hover:underline"> 28 + Copyright Policy 29 + </a>. 30 + </p> 31 + </div>, 32 + ); 33 + };
+86
static/styles.css
··· 11 11 --color-zinc-50: oklch(98.5% 0 0); 12 12 --color-zinc-100: oklch(96.7% 0.001 286.375); 13 13 --color-zinc-200: oklch(92% 0.004 286.32); 14 + --color-zinc-300: oklch(87.1% 0.006 286.286); 14 15 --color-zinc-500: oklch(55.2% 0.016 285.938); 15 16 --color-zinc-600: oklch(44.2% 0.017 285.786); 17 + --color-zinc-700: oklch(37% 0.013 285.805); 16 18 --color-zinc-800: oklch(27.4% 0.006 286.033); 17 19 --color-zinc-900: oklch(21% 0.006 285.885); 18 20 --color-zinc-950: oklch(14.1% 0.005 285.823); ··· 30 32 --text-xl--line-height: calc(1.75 / 1.25); 31 33 --text-2xl: 1.5rem; 32 34 --text-2xl--line-height: calc(2 / 1.5); 35 + --text-3xl: 1.875rem; 36 + --text-3xl--line-height: calc(2.25 / 1.875); 33 37 --text-4xl: 2.25rem; 34 38 --text-4xl--line-height: calc(2.5 / 2.25); 35 39 --font-weight-semibold: 600; ··· 184 188 } 185 189 } 186 190 @layer utilities { 191 + .visible { 192 + visibility: visible; 193 + } 187 194 .sr-only { 188 195 position: absolute; 189 196 width: 1px; ··· 276 283 max-width: 96rem; 277 284 } 278 285 } 286 + .mx-1 { 287 + margin-inline: calc(var(--spacing) * 1); 288 + } 279 289 .mx-auto { 280 290 margin-inline: auto; 281 291 } ··· 291 301 .my-30 { 292 302 margin-block: calc(var(--spacing) * 30); 293 303 } 304 + .mt-1 { 305 + margin-top: calc(var(--spacing) * 1); 306 + } 294 307 .mt-2 { 295 308 margin-top: calc(var(--spacing) * 2); 296 309 } ··· 308 321 } 309 322 .mb-4 { 310 323 margin-bottom: calc(var(--spacing) * 4); 324 + } 325 + .mb-6 { 326 + margin-bottom: calc(var(--spacing) * 6); 327 + } 328 + .mb-8 { 329 + margin-bottom: calc(var(--spacing) * 8); 330 + } 331 + .ml-0 { 332 + margin-left: calc(var(--spacing) * 0); 311 333 } 312 334 .flex { 313 335 display: flex; ··· 454 476 .flex-col { 455 477 flex-direction: column; 456 478 } 479 + .flex-row { 480 + flex-direction: row; 481 + } 457 482 .flex-wrap { 458 483 flex-wrap: wrap; 459 484 } ··· 462 487 } 463 488 .items-center { 464 489 align-items: center; 490 + } 491 + .items-start { 492 + align-items: flex-start; 465 493 } 466 494 .justify-between { 467 495 justify-content: space-between; ··· 648 676 font-size: var(--text-2xl); 649 677 line-height: var(--tw-leading, var(--text-2xl--line-height)); 650 678 } 679 + .text-3xl { 680 + font-size: var(--text-3xl); 681 + line-height: var(--tw-leading, var(--text-3xl--line-height)); 682 + } 651 683 .text-4xl { 652 684 font-size: var(--text-4xl); 653 685 line-height: var(--tw-leading, var(--text-4xl--line-height)); ··· 694 726 .text-white { 695 727 color: var(--color-white); 696 728 } 729 + .text-zinc-500 { 730 + color: var(--color-zinc-500); 731 + } 697 732 .text-zinc-600 { 698 733 color: var(--color-zinc-600); 699 734 } 735 + .text-zinc-700 { 736 + color: var(--color-zinc-700); 737 + } 738 + .text-zinc-800 { 739 + color: var(--color-zinc-800); 740 + } 700 741 .text-zinc-900 { 701 742 color: var(--color-zinc-900); 702 743 } ··· 706 747 .lowercase { 707 748 text-transform: lowercase; 708 749 } 750 + .underline { 751 + text-decoration-line: underline; 752 + } 709 753 .ring-sky-500 { 710 754 --tw-ring-color: var(--color-sky-500); 711 755 } 712 756 .group-data-\[added\=true\]\:block { 713 757 &:is(:where(.group)[data-added="true"] *) { 714 758 display: block; 759 + } 760 + } 761 + .hover\:no-underline { 762 + &:hover { 763 + @media (hover: hover) { 764 + text-decoration-line: none; 765 + } 715 766 } 716 767 } 717 768 .hover\:underline { ··· 777 828 left: calc(var(--spacing) * 0); 778 829 } 779 830 } 831 + .sm\:mt-0 { 832 + @media (width >= 40rem) { 833 + margin-top: calc(var(--spacing) * 0); 834 + } 835 + } 836 + .sm\:ml-1 { 837 + @media (width >= 40rem) { 838 + margin-left: calc(var(--spacing) * 1); 839 + } 840 + } 841 + .sm\:ml-2 { 842 + @media (width >= 40rem) { 843 + margin-left: calc(var(--spacing) * 2); 844 + } 845 + } 780 846 .sm\:h-screen { 781 847 @media (width >= 40rem) { 782 848 height: 100vh; ··· 832 898 align-items: center; 833 899 } 834 900 } 901 + .sm\:items-end { 902 + @media (width >= 40rem) { 903 + align-items: flex-end; 904 + } 905 + } 835 906 .sm\:justify-between { 836 907 @media (width >= 40rem) { 837 908 justify-content: space-between; ··· 840 911 .sm\:justify-end { 841 912 @media (width >= 40rem) { 842 913 justify-content: flex-end; 914 + } 915 + } 916 + .sm\:gap-0 { 917 + @media (width >= 40rem) { 918 + gap: calc(var(--spacing) * 0); 843 919 } 844 920 } 845 921 .sm\:space-x-2 { ··· 907 983 .dark\:text-zinc-50 { 908 984 @media (prefers-color-scheme: dark) { 909 985 color: var(--color-zinc-50); 986 + } 987 + } 988 + .dark\:text-zinc-100 { 989 + @media (prefers-color-scheme: dark) { 990 + color: var(--color-zinc-100); 991 + } 992 + } 993 + .dark\:text-zinc-300 { 994 + @media (prefers-color-scheme: dark) { 995 + color: var(--color-zinc-300); 910 996 } 911 997 } 912 998 .dark\:text-zinc-500 {