/* PrivaTools — design tokens "Cipher" edition. * * Editorial-privacy-tech aesthetic. A confident, refined workshop — * not a SaaS landing page. * * - Display: Fraunces (variable serif, optical sizing) * - UI: Inter (cv11/ss01) — keeps install-base, opted-in features * - Mono: JetBrains Mono * * - Warm paper light theme (#FAF7F0) with deep ink (#13110D) * - Almost-black dark theme (#0B0B0C) with bone foreground (#EDE8DA) * - Signal green accent (#0F9D58 light / #34D399 dark) — privacy = the green check * - Secondary accent: copper (#B7591A) used sparingly for "industry first" callouts * * Fonts are self-hosted from /fonts so page rendering never depends on a * third-party font origin. */ @font-face { font-family: 'Bricolage Grotesque'; font-style: normal; font-weight: 600; font-display: swap; src: url('/fonts/bricolage-grotesque-latin-600-normal.woff2') format('woff2'); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; } @font-face { font-family: 'Bricolage Grotesque'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/bricolage-grotesque-latin-700-normal.woff2') format('woff2'); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; } @font-face { font-family: 'Bricolage Grotesque'; font-style: normal; font-weight: 800; font-display: swap; src: url('/fonts/bricolage-grotesque-latin-800-normal.woff2') format('woff2'); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; } @font-face { font-family: 'Outfit'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/outfit-latin-400-normal.woff2') format('woff2'); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; } @font-face { font-family: 'Outfit'; font-style: normal; font-weight: 500; font-display: swap; src: url('/fonts/outfit-latin-500-normal.woff2') format('woff2'); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; } @font-face { font-family: 'Outfit'; font-style: normal; font-weight: 600; font-display: swap; src: url('/fonts/outfit-latin-600-normal.woff2') format('woff2'); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; } @font-face { font-family: 'Outfit'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/outfit-latin-700-normal.woff2') format('woff2'); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; } @font-face { font-family: 'DM Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/dm-mono-latin-400-normal.woff2') format('woff2'); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; } @font-face { font-family: 'DM Mono'; font-style: normal; font-weight: 500; font-display: swap; src: url('/fonts/dm-mono-latin-500-normal.woff2') format('woff2'); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; } @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --font-display: 'Bricolage Grotesque', 'Outfit', system-ui, sans-serif; --font-sans: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; --font-mono: 'DM Mono', 'SF Mono', Menlo, monospace; /* ── Light ───────────────────────────────────────────────────────── Teal primary with an amber highlight: a warm/cool complementary duo that reads cheerful rather than corporate. Teal is unclaimed as a primary here — Foxit uses it only as a tertiary — while five rivals share blue and two share orange. */ --background: 0 0% 100%; --foreground: 195 42% 8%; /* #0C1D22 teal-leaning near-black */ --paper: 0 0% 100%; --paper-2: 186 44% 97%; /* #F3FAFB */ --paper-3: 186 36% 93%; /* #E4F2F4 */ --card: 0 0% 100%; --card-foreground: 195 42% 8%; --card-tint: 186 50% 98%; --popover: 0 0% 100%; --popover-foreground: 195 42% 8%; --primary: 175 100% 26%; /* #00857A — 4.55:1 with white; 33% was 2.96:1 */ --primary-foreground: 0 0% 100%; --secondary: 186 40% 95%; --secondary-foreground: 195 42% 8%; --muted: 186 30% 96%; --muted-foreground: 196 14% 38%; /* #536B70 — AA on white */ /* Energy, not another button colour: highlights, badges, "new". */ --accent: 34 100% 32%; /* #A35C00 — 5.0:1 on white */ --accent-bright: 38 100% 50%; /* #FFA800 — fills only, never text */ --accent-foreground: 0 0% 100%; --accent-soft: 40 100% 94%; --copper: 28 92% 33%; /* warning — 4.7:1 on white */ --copper-soft: 32 100% 94%; /* Fallback only: Air and Play set their own in skins/experience/tokens.css. */ --warning: 28 92% 33%; --warning-soft: 32 100% 94%; --success: 158 80% 30%; --success-soft: 158 60% 94%; --destructive: 356 78% 48%; --destructive-foreground: 0 0% 100%; --border: 190 26% 89%; --border-strong: 192 18% 74%; --input: 190 26% 89%; --ring: 175 100% 26%; --radius: 1.25rem; /* Twelve families, all at the same saturation and lightness so they read as one wheel rather than a dozen unrelated defaults. Only the hue moves. */ --cat-organize: 210 72% 48%; --cat-edit: 265 72% 48%; --cat-optimize: 175 72% 38%; --cat-security: 350 72% 50%; --cat-to-pdf: 150 72% 36%; --cat-from-pdf: 32 82% 46%; --cat-advanced: 240 72% 56%; --cat-image: 320 72% 50%; --cat-video: 12 78% 50%; --cat-developer: 190 78% 40%; --cat-archive: 45 82% 44%; --cat-document: 95 62% 36%; --sidebar-background: 0 0% 100%; --sidebar-foreground: 195 42% 8%; --sidebar-primary: 175 100% 33%; --sidebar-primary-foreground: 0 0% 100%; --sidebar-accent: 186 40% 95%; --sidebar-accent-foreground: 195 42% 8%; --sidebar-border: 190 26% 89%; --sidebar-ring: 175 100% 33%; } /* ── Dark — true black ─────────────────────────────────────────── */ .dark { --foreground: 186 30% 95%; --card-foreground: 186 30% 95%; --popover-foreground: 186 30% 95%; --primary: 175 75% 47%; /* lifted so it holds on a dark ground */ --primary-foreground: 195 45% 6%; --accent: 40 100% 62%; --accent-bright: 40 100% 62%; --accent-foreground: 195 45% 6%; --accent-soft: 36 60% 16%; --copper: 32 96% 62%; --copper-soft: 28 45% 16%; --warning: 32 96% 62%; --warning-soft: 28 45% 16%; --success: 158 62% 48%; --success-soft: 158 40% 14%; --destructive: 356 85% 63%; --destructive-foreground: 0 0% 100%; --ring: 175 75% 47%; --sidebar-primary: 175 75% 47%; --sidebar-primary-foreground: 195 45% 6%; /* True black, with elevation carried by lightness alone. Neutral on purpose: it is what makes the category hues punch. */ --background: 0 0% 2.5%; --paper: 0 0% 2.5%; --paper-2: 0 0% 6%; --paper-3: 0 0% 10%; --card: 0 0% 5%; --card-tint: 0 0% 8%; --popover: 0 0% 5%; --secondary: 0 0% 12%; --secondary-foreground: 0 0% 88%; --muted: 0 0% 10%; --muted-foreground: 0 0% 66%; --border: 0 0% 15%; --border-strong: 0 0% 28%; --input: 0 0% 15%; --sidebar-background: 0 0% 2.5%; --sidebar-foreground: 186 30% 95%; --sidebar-accent: 0 0% 12%; --sidebar-accent-foreground: 186 30% 95%; --sidebar-border: 0 0% 15%; /* Same wheel, lifted for legibility on dark. */ --cat-organize: 210 88% 66%; --cat-edit: 265 88% 72%; --cat-optimize: 175 70% 50%; --cat-security: 350 88% 68%; --cat-to-pdf: 150 62% 48%; --cat-from-pdf: 32 92% 58%; --cat-advanced: 240 90% 74%; --cat-image: 320 85% 68%; --cat-video: 12 90% 64%; --cat-developer: 190 80% 54%; --cat-archive: 45 92% 58%; --cat-document: 95 60% 52%; } } @layer base { * { @apply border-border; } html { -webkit-text-size-adjust: 100%; text-rendering: optimizeLegibility; } body { @apply bg-background text-foreground; font-family: var(--font-sans); font-feature-settings: "cv11", "ss01", "ss03", "calt"; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-size: 15px; line-height: 1.55; /* Subtle paper grain — masked to keep it cheap and decorative */ background-image: radial-gradient(circle at 0.5px 0.5px, hsl(var(--foreground) / 0.025) 0.5px, transparent 0); background-size: 3px 3px; } .dark body { background-image: radial-gradient(circle at 0.5px 0.5px, hsl(var(--foreground) / 0.04) 0.5px, transparent 0); background-size: 3px 3px; } h1, h2, h3, h4, h5, h6 { font-family: var(--font-display); letter-spacing: -0.02em; line-height: 1.08; font-weight: 600; color: hsl(var(--foreground)); font-variation-settings: "opsz" 144; } h1 { font-weight: 700; letter-spacing: -0.035em; font-variation-settings: "opsz" 144, "SOFT" 50; } h2 { font-weight: 600; } h3, h4, h5, h6 { font-weight: 600; letter-spacing: -0.018em; } /* Code reads as typed: no face may join "--", "->" or "fi" in it. */ code, kbd, samp, pre { font-variant-ligatures: none; } } /* ── Legacy compat — older code still references these class names ── */ .font-sans-ui { font-family: var(--font-sans); } .font-mono-meta { font-family: var(--font-mono); } .font-heading { font-family: var(--font-display); letter-spacing: -0.02em; font-weight: 700; } .font-serif-body { font-family: var(--font-display); } .font-editorial { font-family: var(--font-display); } .font-display { font-family: var(--font-display); } .font-mono { font-family: var(--font-mono); } /* ── Editorial flourishes ──────────────────────────────────────────── */ /* § section markers — the recurring motif. */ .section-mark { display: inline-flex; align-items: center; gap: 0.5rem; font-size: 12px; font-weight: 500; letter-spacing: 0; color: hsl(var(--muted-foreground)); font-weight: 500; } .section-mark::before { content: ""; font-family: var(--font-display); font-style: italic; font-size: 14px; color: hsl(var(--accent)); font-weight: 600; } /* Hairline rules — single source of truth */ .rule-hair { border-top: 1px solid hsl(var(--border)); } .rule-thin { border-top: 1px solid hsl(var(--border)); } .rule-thick { border-top: 1px solid hsl(var(--border-strong)); } .rule-double { border-top: 3px double hsl(var(--border-strong)); } .rule-accent { border-top: 1px solid hsl(var(--accent) / 0.5); } .rule-accent-thick{ border-top: 2px solid hsl(var(--accent)); } .column-divider { border-right: 1px solid hsl(var(--border)); } /* Section flag — pill chip, monospace, hairline border */ .section-flag { display: inline-flex; align-items: center; gap: 0.375rem; height: 26px; padding: 0 0.75rem; background: hsl(var(--paper-2)); color: hsl(var(--muted-foreground)); font-size: 12.5px; font-weight: 500; letter-spacing: 0; border-radius: 999px; border-radius: 6px; border: 1px solid hsl(var(--border)); } .category-tag { font-size: 12px; font-weight: 500; letter-spacing: 0; color: hsl(var(--muted-foreground)); } .masthead-title { font-family: var(--font-display); font-size: 1.25rem; font-weight: 700; letter-spacing: -0.025em; } .masthead-meta { font-family: var(--font-mono); font-size: 11px; color: hsl(var(--muted-foreground)); } .masthead-nav { font-family: var(--font-sans); font-size: 14px; font-weight: 500; } .masthead-nav a, .masthead-nav button { color: hsl(var(--muted-foreground)); transition: color 0.15s ease; } .masthead-nav a:hover, .masthead-nav button:hover { color: hsl(var(--foreground)); } .masthead-nav a.active, .masthead-nav button.active { color: hsl(var(--foreground)); } /* ── Surfaces ─────────────────────────────────────────────────────── */ .surface-paper { background: hsl(var(--paper)); } .surface-paper-2 { background: hsl(var(--paper-2)); } .surface-card { background: hsl(var(--card)); border: 1px solid hsl(var(--border)); } /* ── Editorial insert (general bordered block) ────────────────────── */ .editorial-insert { background: hsl(var(--card)); border: 1px solid hsl(var(--border)); border-radius: var(--radius); transition: border-color 0.15s ease, box-shadow 0.18s ease; } .editorial-insert:hover { border-color: hsl(var(--border-strong)); box-shadow: 0 1px 2px 0 rgba(20,15,5,0.04), 0 6px 16px -6px rgba(20,15,5,0.08); } /* ── Tool card ────────────────────────────────────────────────────── */ /* Subtle by default, decisive on hover. The tile color tints the icon square and the hover halo so each category feels distinct without visual chaos. */ .tool-card { position: relative; isolation: isolate; background: hsl(var(--card)); border: 1px solid hsl(var(--border)); border-radius: 12px; transition: border-color 0.22s cubic-bezier(0.4, 0, 0.2, 1), transform 0.22s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.22s cubic-bezier(0.4, 0, 0.2, 1), background 0.22s cubic-bezier(0.4, 0, 0.2, 1); } .tool-card:active { transform: translateY(0); transition-duration: 0.06s; } .tool-card::after { /* hairline corner accent that flicks in on hover */ content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; background: linear-gradient(135deg, hsl(var(--tile, var(--accent)) / 0.0) 0 70%, hsl(var(--tile, var(--accent)) / 0.5) 100%); opacity: 0; transition: opacity 0.2s ease; z-index: 0; } .tool-card:hover { border-color: hsl(var(--tile, var(--accent)) / 0.45); transform: translateY(-1px); box-shadow: 0 1px 2px 0 rgba(20,15,5,0.04), 0 8px 24px -10px hsl(var(--tile, var(--accent)) / 0.30); } .dark .tool-card:hover { box-shadow: 0 1px 2px 0 rgba(0,0,0,0.5), 0 12px 32px -12px hsl(var(--tile, var(--accent)) / 0.35); } .tool-card:hover::after { opacity: 1; } .tool-card > * { position: relative; z-index: 1; } /* Hero tile — top-of-fold featured tools — more dramatic lift */ .hero-tile { position: relative; isolation: isolate; overflow: hidden; background: hsl(var(--card)); border: 1px solid hsl(var(--border)); border-radius: 20px; transition: border-color 0.28s cubic-bezier(0.22, 1, 0.36, 1), transform 0.28s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.28s cubic-bezier(0.22, 1, 0.36, 1); } .hero-tile:hover { transform: translateY(-4px); border-color: hsl(var(--tile, var(--primary)) / 0.55); box-shadow: 0 20px 44px -24px hsl(var(--tile, var(--primary)) / 0.7), 0 2px 8px -4px hsl(var(--foreground) / 0.08); } @media (prefers-reduced-motion: reduce) { .hero-tile, .hero-tile:hover { transition: none; transform: none; } } .hero-tile:active { transform: translateY(0); transition-duration: 0.06s; } .hero-tile::before { content: ""; position: absolute; inset: 0; background: radial-gradient(120% 80% at 50% 0%, hsl(var(--tile, var(--accent)) / 0.10), transparent 70%); opacity: 0; transition: opacity 0.25s ease; pointer-events: none; z-index: 0; } .hero-tile:hover { transform: translateY(-2px); border-color: hsl(var(--tile, var(--accent)) / 0.55); box-shadow: 0 1px 2px 0 rgba(20,15,5,0.05), 0 14px 32px -14px hsl(var(--tile, var(--accent)) / 0.35); } .dark .hero-tile:hover { box-shadow: 0 1px 2px 0 rgba(0,0,0,0.5), 0 16px 40px -14px hsl(var(--tile, var(--accent)) / 0.40); } .hero-tile:hover::before { opacity: 1; } .hero-tile > * { position: relative; z-index: 1; } /* ── Buttons ──────────────────────────────────────────────────────── */ /* Primary — ink rectangle with subtle inner highlight. Rectangular, not pill: feels editorial / desktop-app, not consumer-saas. */ .btn-editorial { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; height: 48px; padding: 0 1.75rem; background: hsl(var(--primary)); color: hsl(var(--primary-foreground)); font-family: var(--font-sans); font-size: 15.5px; font-weight: 600; border-radius: 999px; box-shadow: 0 2px 0 0 hsl(var(--primary) / 0.32), 0 8px 20px -8px hsl(var(--primary) / 0.5); transition: transform 0.18s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.18s cubic-bezier(0.22, 1, 0.36, 1), filter 0.15s ease; } .btn-editorial:hover { filter: brightness(1.04); transform: translateY(-2px); box-shadow: 0 4px 0 0 hsl(var(--primary) / 0.32), 0 16px 32px -10px hsl(var(--primary) / 0.55); } .btn-editorial:active { transform: translateY(1px); } @media (prefers-reduced-motion: reduce) { .btn-editorial, .btn-editorial:hover, .btn-editorial:active { transition: none; transform: none; } } .btn-editorial:active { transform: translateY(1px); } .dark .btn-editorial { box-shadow: inset 0 1px 0 rgba(255,255,255,0.10), 0 1px 2px 0 rgba(0,0,0,0.5); } .dark .btn-editorial:hover { box-shadow: inset 0 1px 0 rgba(255,255,255,0.14), 0 4px 14px -2px rgba(0,0,0,0.6), 0 0 0 3px hsl(var(--accent) / 0.25); } /* Accent button — solid signal green, for hero CTA when stronger pull is needed. Use sparingly — never alongside primary. */ .btn-accent { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; height: 48px; padding: 0 1.75rem; /* Coral primary, fully rounded. The main action on a consumer tool should look pressable from across the room. */ background: hsl(var(--primary)); color: hsl(var(--primary-foreground)); font-family: var(--font-sans); font-size: 15.5px; font-weight: 600; border-radius: 999px; /* Shadow carries the button's own hue rather than generic black — the difference between a button sitting on the page and one lit by it. */ box-shadow: 0 2px 0 0 hsl(var(--primary) / 0.35), 0 8px 20px -8px hsl(var(--primary) / 0.55); transition: transform 0.18s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.18s cubic-bezier(0.22, 1, 0.36, 1), filter 0.15s ease; } .btn-accent:hover { filter: brightness(1.04); transform: translateY(-2px); box-shadow: 0 4px 0 0 hsl(var(--primary) / 0.35), 0 16px 32px -10px hsl(var(--primary) / 0.6); } .btn-accent:active { transform: translateY(1px); box-shadow: 0 1px 0 0 hsl(var(--primary) / 0.35), 0 4px 12px -6px hsl(var(--primary) / 0.5); } .btn-accent:focus-visible { outline: none; box-shadow: 0 0 0 3px hsl(var(--background)), 0 0 0 6px hsl(var(--ring) / 0.6); } @media (prefers-reduced-motion: reduce) { .btn-accent, .btn-accent:hover, .btn-accent:active { transition: none; transform: none; } } /* Secondary outline — same geometry, transparent */ .btn-secondary-pill { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; height: 44px; padding: 0 1.25rem; background: hsl(var(--card)); color: hsl(var(--foreground)); border: 1px solid hsl(var(--border)); font-family: var(--font-sans); font-size: 14px; font-weight: 600; border-radius: 8px; transition: background 0.15s ease, border-color 0.15s ease; } .btn-secondary-pill:hover { background: hsl(var(--secondary)); border-color: hsl(var(--border-strong)); } /* ── Stat pill — used in masthead, hero metadata ──────────────────── */ .stat-pill { display: inline-flex; align-items: center; gap: 0.4rem; font-family: var(--font-mono); font-size: 11.5px; font-weight: 500; letter-spacing: 0.01em; color: hsl(var(--muted-foreground)); } .stat-pill .dot { width: 5px; height: 5px; border-radius: 999px; background: hsl(var(--accent)); display: inline-block; box-shadow: 0 0 0 3px hsl(var(--accent) / 0.18); } /* ── Drop zone ────────────────────────────────────────────────────── */ .drop-zone { border: 1.5px dashed hsl(var(--border-strong)); border-radius: 14px; background: hsl(var(--paper-2) / 0.5); transition: border-color 0.15s ease, background 0.15s ease; } .drop-zone:hover, .drop-zone.drag-over { border-color: hsl(var(--accent) / 0.7); background: hsl(var(--accent) / 0.04); } /* ── Hero backdrop ────────────────────────────────────────────────── */ /* No purple gradient slop. Warm dotted vignette + a single soft green ambient glow + an asymmetric paper-fold "blade" line. */ .hero-backdrop { position: absolute; inset: 0; z-index: 0; pointer-events: none; background: radial-gradient(45% 35% at 50% 0%, hsl(var(--accent) / 0.10), transparent 70%), radial-gradient(30% 20% at 85% 15%, hsl(var(--copper) / 0.06), transparent 60%); } .hero-backdrop::before { content: ""; position: absolute; inset: 0; background-image: radial-gradient(circle at 1px 1px, hsl(var(--foreground) / 0.07) 1px, transparent 0); background-size: 22px 22px; mask-image: radial-gradient(ellipse 75% 70% at 50% 35%, black 30%, transparent 80%); -webkit-mask-image: radial-gradient(ellipse 75% 70% at 50% 35%, black 30%, transparent 80%); opacity: 0.6; } .dark .hero-backdrop::before { opacity: 0.45; } /* Asymmetric "blade" — a single diagonal hairline that bleeds across the hero — establishes the editorial rhythm. */ .hero-blade { position: absolute; inset: 0; pointer-events: none; overflow: hidden; } .hero-blade::before { content: ""; position: absolute; top: 38%; left: -10%; width: 120%; height: 1px; background: linear-gradient(90deg, transparent 0%, hsl(var(--accent) / 0.4) 40%, hsl(var(--accent) / 0.4) 60%, transparent 100%); transform: rotate(-2deg); } /* Hero orb — single, restrained */ @keyframes orb-drift { 0%,100% { transform: translate(0,0) scale(1); } 50% { transform: translate(18px, -10px) scale(1.05); } } .hero-orb { position: absolute; border-radius: 9999px; filter: blur(80px); opacity: 0.35; pointer-events: none; z-index: 0; } .hero-orb-1 { width: 280px; height: 280px; left: 8%; top: 20%; background: hsl(var(--accent) / 0.55); animation: orb-drift 14s ease-in-out infinite; } .hero-orb-2 { width: 220px; height: 220px; right: 12%; bottom: 10%; background: hsl(var(--copper) / 0.32); animation: orb-drift 16s ease-in-out infinite reverse; } .dark .hero-orb-1 { opacity: 0.22; } .dark .hero-orb-2 { opacity: 0.18; } /* ── Animated Pipeline diagram ────────────────────────────────────── */ .pipeline-anim { position: relative; } .pipeline-file { position: absolute; left: 0; top: 50%; width: 20px; height: 24px; margin-top: -12px; border-radius: 4px; background: hsl(var(--accent)); color: hsl(var(--accent-foreground)); display: inline-flex; align-items: center; justify-content: center; box-shadow: 0 4px 14px hsl(var(--accent) / 0.45); animation: pipeline-drift 6.4s cubic-bezier(0.65, 0, 0.35, 1) infinite; pointer-events: none; z-index: 1; } .pipeline-chip { animation: pipeline-pulse 6.4s ease-in-out infinite; position: relative; z-index: 2; } @keyframes pipeline-drift { 0% { left: 4%; transform: scale(0.85); opacity: 0; } 6% { opacity: 1; transform: scale(1); } 94% { left: 96%; opacity: 1; transform: scale(1); } 100% { left: 96%; opacity: 0; transform: scale(0.85); } } @keyframes pipeline-pulse { 0%, 100% { box-shadow: none; transform: translateY(0); } 6% { box-shadow: 0 0 0 4px hsl(var(--accent) / 0.25); transform: translateY(-1px); } 12% { box-shadow: none; transform: translateY(0); } } @media (prefers-reduced-motion: reduce) { .pipeline-file { animation: none; left: 50%; opacity: 0.6; } .pipeline-chip { animation: none; } } /* ── Per-suite tile color (set --tile per element) ────────────────── */ .cat-organize { --tile: var(--cat-organize); } .cat-edit { --tile: var(--cat-edit); } .cat-optimize { --tile: var(--cat-optimize); } .cat-security { --tile: var(--cat-security); } .cat-to-pdf { --tile: var(--cat-to-pdf); } .cat-from-pdf { --tile: var(--cat-from-pdf); } .cat-advanced { --tile: var(--cat-advanced); } .cat-image { --tile: var(--cat-image); } .cat-video-audio { --tile: var(--cat-video); } .cat-developer { --tile: var(--cat-developer); } .cat-archive { --tile: var(--cat-archive); } .cat-document-office{ --tile: var(--cat-document); } .dark .cat-organize { --tile: 200 80% 60%; } .dark .cat-edit { --tile: 270 75% 70%; } .dark .cat-optimize { --tile: 152 70% 55%; } .dark .cat-security { --tile: 350 75% 65%; } .dark .cat-to-pdf { --tile: 22 85% 60%; } .dark .cat-from-pdf { --tile: 220 80% 70%; } .dark .cat-advanced { --tile: 250 80% 75%; } .dark .cat-image { --tile: 320 75% 70%; } .dark .cat-video-audio { --tile: 18 85% 65%; } .dark .cat-developer { --tile: 195 80% 60%; } .dark .cat-archive { --tile: 45 85% 60%; } .dark .cat-document-office{ --tile: 80 60% 60%; } /* Tinted icon tile — applied to a wrapper that also has .cat-* */ .icon-tile { display: inline-flex; align-items: center; justify-content: center; height: 42px; width: 42px; border-radius: 14px; color: #fff; background: hsl(var(--tile, var(--primary))); border: none; box-shadow: 0 8px 18px -10px hsl(var(--tile, var(--primary)) / 0.85); transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.28s cubic-bezier(0.22, 1, 0.36, 1); } .hero-tile:hover .icon-tile, .tool-card:hover .icon-tile { transform: scale(1.08) rotate(-3deg); box-shadow: 0 12px 26px -10px hsl(var(--tile, var(--primary)) / 0.95); } @media (prefers-reduced-motion: reduce) { .icon-tile, .hero-tile:hover .icon-tile, .tool-card:hover .icon-tile { transition: none; transform: none; } } .tool-card:hover .icon-tile, .hero-tile:hover .icon-tile { background: hsl(var(--tile, var(--accent)) / 0.16); border-color: hsl(var(--tile, var(--accent)) / 0.35); transform: scale(1.04); } .icon-tile-sm { height: 34px; width: 34px; border-radius: 8px; } .icon-tile-lg { height: 56px; width: 56px; border-radius: 14px; } .icon-tile-xl { height: 72px; width: 72px; border-radius: 16px; } /* ── Type-display utility classes ─────────────────────────────────── */ .h-display-1 { font-family: var(--font-display); font-weight: 700; letter-spacing: -0.045em; line-height: 0.95; font-variation-settings: "opsz" 144, "SOFT" 50; } .h-display-2 { font-family: var(--font-display); font-weight: 700; letter-spacing: -0.035em; line-height: 1.02; font-variation-settings: "opsz" 144; } .h-display-italic { font-family: var(--font-display); font-style: italic; font-weight: 500; } /* ── Tabular numerals — used in stat counters ─────────────────────── */ .nums-tabular { font-variant-numeric: tabular-nums; font-feature-settings: "tnum", "lnum"; } /* ── Animations ───────────────────────────────────────────────────── */ @keyframes fade-up { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes slide-in-right { from { opacity: 0; transform: translateX(12px); } to { opacity: 1; transform: translateX(0); } } @keyframes slide-in-left { from { opacity: 0; transform: translateX(-12px); } to { opacity: 1; transform: translateX(0); } } @keyframes slide-down { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: translateY(0); } } @keyframes scale-in { from { opacity: 0; transform: scale(0.98); } to { opacity: 1; transform: scale(1); } } @keyframes shimmer { 0% { background-position: -200% 0 } 100% { background-position: 200% 0 } } @keyframes pulse-border { 0%, 100% { border-color: hsl(var(--accent) / 0.3); } 50% { border-color: hsl(var(--accent) / 0.7); } } @keyframes accordion-down { from { height: 0; } to { height: var(--radix-accordion-content-height); } } @keyframes accordion-up { from { height: var(--radix-accordion-content-height); } to { height: 0; } } .animate-fade-up { animation: fade-up 0.5s ease-out forwards; } .animate-fade-in { animation: fade-in 0.3s ease-out forwards; } .animate-slide-in-right { animation: slide-in-right 0.35s ease-out forwards; } /* Mobile drawer — workshop ease-out, 240ms, no overshoot. */ .animate-slide-in-left { animation: slide-in-left 0.24s cubic-bezier(0.16, 0.84, 0.44, 1) forwards; } .animate-slide-down { animation: slide-down 0.25s ease-out forwards; } .animate-scale-in { animation: scale-in 0.2s ease-out forwards; } .animate-pulse-border { animation: pulse-border 2s ease-in-out infinite; } .stagger-1 { animation-delay: 0.04s; } .stagger-2 { animation-delay: 0.08s; } .stagger-3 { animation-delay: 0.12s; } .stagger-4 { animation-delay: 0.16s; } .stagger-5 { animation-delay: 0.20s; } .stagger-6 { animation-delay: 0.24s; } .stagger-7 { animation-delay: 0.28s; } .stagger-8 { animation-delay: 0.32s; } /* ── Page transition — fade-up the workspace on route change ─────── */ /* Applied to the workspace child via a key={pathname} on the wrapper. */ .workspace-enter { animation: workspace-fade-up 0.32s cubic-bezier(0.16, 0.84, 0.44, 1) both; } @keyframes workspace-fade-up { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } /* ── Success state — corner marks scale in, check pops ───────────── */ @keyframes success-pop { 0% { transform: scale(0.6); opacity: 0; } 60% { transform: scale(1.08); opacity: 1; } 100% { transform: scale(1); } } .animate-success-pop { animation: success-pop 0.45s cubic-bezier(0.22, 1, 0.36, 1) both; } @keyframes corner-extend { from { opacity: 0; transform: scale(0.5); } to { opacity: 1; transform: scale(1); } } .animate-corner-extend > .corner-mark { animation: corner-extend 0.35s ease-out both; } .animate-corner-extend > .corner-mark:nth-child(1) { animation-delay: 0.05s; } .animate-corner-extend > .corner-mark:nth-child(2) { animation-delay: 0.10s; } .animate-corner-extend > .corner-mark:nth-child(3) { animation-delay: 0.15s; } .animate-corner-extend > .corner-mark:nth-child(4) { animation-delay: 0.20s; } /* The drop zone is the product on 219 pages. 56 components had rolled their own with the same Tailwind geometry, so rather than editing 56 files this upgrades all of them at once: softer corners, more room, and a lift on hover so the target feels alive rather than like a fieldset. */ /* Route transition. Content arrives rather than blinking into place; the header and footer stay put so only the changing region moves. */ @keyframes route-in { from { opacity: 0; transform: translate3d(0, 10px, 0); } to { opacity: 1; transform: translate3d(0, 0, 0); } } .workspace-enter { animation: route-in 0.42s cubic-bezier(0.22, 1, 0.36, 1) both; } /* Anything interactive that should feel like an object rather than a rectangle. */ .press { transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.2s cubic-bezier(0.22, 1, 0.36, 1); } .press:hover { transform: translateY(-2px); } .press:active { transform: translateY(1px); transition-duration: 0.08s; } /* A slow drift for ambient background shapes. Deliberately long and subtle — ambient motion should be noticed only if you look for it. */ @keyframes drift { 0%, 100% { transform: translate3d(0, 0, 0) scale(1); } 50% { transform: translate3d(0, -14px, 0) scale(1.04); } } .drift { animation: drift 14s ease-in-out infinite; } .drift-slow { animation: drift 22s ease-in-out infinite; } @media (prefers-reduced-motion: reduce) { .workspace-enter { animation: none; } .press, .press:hover, .press:active { transition: none; transform: none; } .drift, .drift-slow { animation: none; } } /* ── Motion ─────────────────────────────────────────────────────────────── Nothing should mount all at once. A grid that appears as a single block reads as a screenshot; the same grid cascading in reads as a place that is doing something. Y-translation plus opacity only — both GPU-composited. */ @keyframes rise-in { from { opacity: 0; transform: translate3d(0, 14px, 0); } to { opacity: 1; transform: translate3d(0, 0, 0); } } .stagger-in > * { opacity: 0; animation: rise-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) both; } /* Capped at 12 — past that the last card would wait most of a second, which reads as jank rather than choreography. */ .stagger-in > *:nth-child(1) { animation-delay: 0.02s; } .stagger-in > *:nth-child(2) { animation-delay: 0.05s; } .stagger-in > *:nth-child(3) { animation-delay: 0.08s; } .stagger-in > *:nth-child(4) { animation-delay: 0.11s; } .stagger-in > *:nth-child(5) { animation-delay: 0.14s; } .stagger-in > *:nth-child(6) { animation-delay: 0.17s; } .stagger-in > *:nth-child(7) { animation-delay: 0.20s; } .stagger-in > *:nth-child(8) { animation-delay: 0.23s; } .stagger-in > *:nth-child(9) { animation-delay: 0.26s; } .stagger-in > *:nth-child(10) { animation-delay: 0.29s; } .stagger-in > *:nth-child(11) { animation-delay: 0.32s; } .stagger-in > *:nth-child(n+12) { animation-delay: 0.35s; } /* Scroll reveal — sections settle in as they enter the viewport. */ .reveal { opacity: 0; transform: translate3d(0, 20px, 0); transition: opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1), transform 0.6s cubic-bezier(0.22, 1, 0.36, 1); will-change: opacity, transform; } .reveal.is-visible { opacity: 1; transform: translate3d(0, 0, 0); will-change: auto; } @media (prefers-reduced-motion: reduce) { .stagger-in > * { animation: none; opacity: 1; } .reveal { opacity: 1; transform: none; transition: none; } } /* Card shadow carries the card's own hue rather than generic black, so a grid of tools reads as coloured objects on a page instead of grey rectangles. */ .tool-card:hover { box-shadow: 0 18px 40px -22px var(--hue), 0 2px 8px -4px hsl(var(--foreground) / 0.08); } .dropzone-surface { border-radius: 28px !important; padding-top: 3.5rem !important; padding-bottom: 3.5rem !important; transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1), background-color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s cubic-bezier(0.22, 1, 0.36, 1); } .dropzone-surface:hover { transform: translateY(-2px); border-color: hsl(var(--primary) / 0.6); box-shadow: 0 16px 44px -24px hsl(var(--primary) / 0.5); } @media (prefers-reduced-motion: reduce) { .dropzone-surface, .dropzone-surface:hover { transition: none; transform: none; } } /* Corner brackets are the last of the technical-drawing motif. Hidden here rather than deleted from 96 components: the JSX stays valid, the marks stop rendering, and reverting is one line. */ .corner-mark { display: none !important; } /* ── Dropzone interaction — corner marks gently extend on hover ── */ .corner-mark { transition: transform 0.25s ease-out, opacity 0.25s ease-out; } /* On hover of any dashed-border dropzone, marks shift outward subtly. */ .border-dashed:hover > .corner-mark:nth-child(odd):nth-child(-n+2) { transform: translate(-2px, -2px); } .border-dashed:hover > .corner-mark:nth-child(2) { transform: translate( 2px, -2px); } .border-dashed:hover > .corner-mark:nth-child(3) { transform: translate(-2px, 2px); } .border-dashed:hover > .corner-mark:nth-child(4) { transform: translate( 2px, 2px); } /* ── Indeterminate progress bar — shimmer travel ─────────────────── */ @keyframes progress-indeterminate { 0% { transform: translateX(-100%); } 100% { transform: translateX(300%); } } .progress-indeterminate { animation: progress-indeterminate 1.4s cubic-bezier(0.4, 0, 0.2, 1) infinite; } /* ── Spinner — slower, calmer than default ─────────────────────── */ @keyframes spinner-calm { to { transform: rotate(360deg); } } .spinner-calm { animation: spinner-calm 1.2s linear infinite; } /* ── Active route marker on sidebar (vertical accent bar) ──────── */ @keyframes underline-grow { from { transform: translateY(-50%) scaleY(0); opacity: 0; } to { transform: translateY(-50%) scaleY(1); opacity: 1; } } .animate-underline-grow { animation: underline-grow 0.25s ease-out both; transform-origin: center; } /* ── Pin a pulse animation reserved for accent dot indicators ─── */ @keyframes accent-pulse { 0%, 100% { box-shadow: 0 0 0 0 hsl(var(--accent) / 0.4); } 50% { box-shadow: 0 0 0 6px hsl(var(--accent) / 0); } } .animate-accent-pulse { animation: accent-pulse 2s ease-out infinite; } /* ── Status dot — gentler than animate-pulse ───────────────────── */ @keyframes status-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.55; transform: scale(0.92); } } .animate-status-pulse { animation: status-pulse 2.4s ease-in-out infinite; } /* ── Pipeline spark — used by connectors ──────────────────────── */ @keyframes pipeline-spark-down { 0% { top: -8px; opacity: 0; } 20% { opacity: 1; } 80% { opacity: 1; } 100% { top: 100%; opacity: 0; } } .pipeline-spark-down { animation: pipeline-spark-down 1.2s ease-in-out infinite; } @keyframes pipeline-spark-x { 0% { left: -6px; opacity: 0; } 18% { opacity: 1; } 82% { opacity: 1; } 100% { left: calc(100% - 6px); opacity: 0; } } .animate-pipeline-spark-x { animation: pipeline-spark-x 1.8s ease-in-out infinite; } /* ── Subtle scale-in for fresh items in a list ────────────────── */ @keyframes pop-in { 0% { opacity: 0; transform: scale(0.85) translateY(4px); } 100% { opacity: 1; transform: scale(1) translateY(0); } } .animate-pop-in { animation: pop-in 0.28s cubic-bezier(0.22, 1, 0.36, 1) both; } /* ── Crossfade — for swapping content (e.g. status bar facts) ── */ @keyframes crossfade { 0% { opacity: 0; transform: translateY(4px); } 100% { opacity: 1; transform: translateY(0); } } .animate-crossfade { animation: crossfade 0.4s ease-out both; } /* ── Card hover lift with shine sweep ──────────────────────────── */ .shine-sweep { position: relative; overflow: hidden; isolation: isolate; } .shine-sweep::after { content: ""; position: absolute; inset: 0; background: linear-gradient(105deg, transparent 30%, hsl(var(--accent) / 0.10) 50%, transparent 70%); transform: translateX(-100%); transition: transform 0.7s cubic-bezier(0.4, 0, 0.2, 1); pointer-events: none; z-index: 1; } .shine-sweep:hover::after { transform: translateX(100%); } /* ── Slide-up + stagger for grid items ─────────────────────────── */ @keyframes grid-item-enter { 0% { opacity: 0; transform: translateY(12px) scale(0.97); } 100% { opacity: 1; transform: translateY(0) scale(1); } } .animate-grid-enter > * { animation: grid-item-enter 0.4s cubic-bezier(0.16, 0.84, 0.44, 1) both; } .animate-grid-enter > *:nth-child(1) { animation-delay: 0.02s; } .animate-grid-enter > *:nth-child(2) { animation-delay: 0.05s; } .animate-grid-enter > *:nth-child(3) { animation-delay: 0.08s; } .animate-grid-enter > *:nth-child(4) { animation-delay: 0.11s; } .animate-grid-enter > *:nth-child(5) { animation-delay: 0.14s; } .animate-grid-enter > *:nth-child(6) { animation-delay: 0.17s; } .animate-grid-enter > *:nth-child(7) { animation-delay: 0.20s; } .animate-grid-enter > *:nth-child(8) { animation-delay: 0.23s; } .animate-grid-enter > *:nth-child(9) { animation-delay: 0.26s; } .animate-grid-enter > *:nth-child(10) { animation-delay: 0.29s; } /* ── Input focus — subtle scale + accent ring ──────────────────── */ @keyframes input-focus-ring { 0% { box-shadow: 0 0 0 0 hsl(var(--accent) / 0.4); } 100% { box-shadow: 0 0 0 3px hsl(var(--accent) / 0.18); } } /* ── Number tick — counter animation ───────────────────────────── */ @keyframes count-up { 0% { opacity: 0; transform: translateY(8px); } 100% { opacity: 1; transform: translateY(0); } } .animate-count-up { animation: count-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) both; } /* ── Marquee — used by privacy ticker ─────────────────────────────── */ @keyframes marquee-x { from { transform: translateX(0); } to { transform: translateX(-50%); } } .marquee-track { display: inline-flex; gap: 3rem; white-space: nowrap; animation: marquee-x 38s linear infinite; } @media (prefers-reduced-motion: reduce) { .marquee-track { animation-duration: 120s; } } /* ── Scrollbars ───────────────────────────────────────────────────── */ /* `.no-scrollbar` hides; default is a slim track with an accent-tinted thumb on hover so it doubles as a position indicator without being noisy. */ .no-scrollbar::-webkit-scrollbar { display: none; } .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } /* Scoped to this design. A ported skin (src/skins/) brings the scrollbar treatment its own source had, and ours interfered with it in a way that showed up as layout: `scrollbar-color` flips Chromium to overlay scrollbars, and `*::-webkit-scrollbar` forces classic ones — either way the viewport reserved a different gutter than the original design did, and every route came out 10px wide or narrow. */ /* Firefox — thin, muted thumb. */ :root:not([data-skin]) * { scrollbar-width: thin; scrollbar-color: hsl(var(--border-strong)) transparent; } /* WebKit/Blink — slim, rounded, accent on hover. */ :root:not([data-skin]) *::-webkit-scrollbar { width: 10px; height: 10px; } :root:not([data-skin]) *::-webkit-scrollbar-track { background: transparent; } :root:not([data-skin]) *::-webkit-scrollbar-thumb { background: hsl(var(--border-strong)); border-radius: 999px; border: 2px solid hsl(var(--background)); transition: background 0.18s ease; } :root:not([data-skin]) *::-webkit-scrollbar-thumb:hover { background: hsl(var(--accent) / 0.55); } :root:not([data-skin]) *::-webkit-scrollbar-thumb:active { background: hsl(var(--accent) / 0.70); } :root:not([data-skin]) *::-webkit-scrollbar-corner { background: transparent; } ::selection { background: hsl(var(--accent) / 0.25); color: hsl(var(--foreground)); } /* ── Focus ring ───────────────────────────────────────────────────── */ /* Single source of truth — every interactive element shows the same ring. Includes for collapsibles and role-bearing divs (some shadcn primitives forward role="button" to non-button elements). */ :where(a, button, input, textarea, select, summary, [role="button"], [role="link"], [role="menuitem"], [tabindex]):focus-visible { outline: 2px solid hsl(var(--accent)); outline-offset: 2px; border-radius: 6px; } /* Accent-coloured buttons need a contrasting ring — the default accent outline disappears on .btn-accent's accent background. Override with the foreground (ink) colour so the ring is always visible. */ .btn-accent:focus-visible, button.btn-accent:focus-visible { outline: 2px solid hsl(var(--foreground)); outline-offset: 3px; } /* ── Ported-design overflow guard ─────────────────────────────────── */ /* Grid and flex items default to `min-width: auto`, which refuses to shrink below the content's intrinsic width. Both imported designs that lay out in fixed tracks hit this at 375px: - Aurora puts a full URL in a two-column grid cell, and that one unbreakable string widened the document to 410px. - Structured's sticky header could not shrink its search control below its own label, so the page laid out at 508px inside a 375px viewport and the browser shrank the entire design to fit. Letting those boxes shrink, and letting a long URL wrap, fixes both without touching any design's own sizing: min-width:0 only ever permits shrinking, never forces it, and the wrap is scoped to links and buttons, which never hold prose. Structured has no CSS breakpoints at all — it switches layouts in JavaScript off window.innerWidth — so there was no media query to fix this in. */ [data-skin] :where(div, button, a, p, span, li, header, nav, section, main, aside) { min-width: 0; } /* `break-word`, not `anywhere`: both wrap a long URL once the box is narrow, but `anywhere` also lowers the element's min-content width, which let a flex button collapse to one character and stack "Filter" vertically. */ [data-skin] :where(button, a) { overflow-wrap: break-word; } /* Aurora's responsive ladder stops at 767px, where its four-up grid becomes two-up and stays there. At 375px that leaves 165px columns — cramped for the cards, and too narrow for the one that holds a full URL, which spilled 35px past the viewport and made the page scroll sideways. `:root[…]` rather than `[…]` so this outranks skin-native.css regardless of import order. */ @media (max-width: 480px) { } /* The designs wrap their data tables in a rounded panel that sets `overflow: hidden` to keep the corners clean. On a phone that clips the table instead of scrolling it, so the right-hand columns — Local, Server, Offline on the tools table — simply could not be reached. Wide content should scroll inside its own box, which is what this makes it do. */ @media (max-width: 767px) { [data-skin] :has(> table) { overflow-x: auto !important; } } /* A keyboard shortcut hint is noise on a device with no keyboard, and these sit inside the search control where they were the last thing making its row too wide to fit. */ @media (pointer: coarse) { :where(kbd) { display: none; } } /* ── Touch target floor ───────────────────────────────────────────── */ /* WCAG 2.5.8 asks for a 24px minimum target. A dozen standalone links and buttons sat at 17-20px tall — fine with a mouse, fiddly with a thumb. `min-height` has no effect on `display:inline`, so this reaches exactly the controls that need it: standalone links and buttons, which are inline-flex or block. Links inside running prose stay inline and untouched, which is also what 2.5.8's inline exception wants. Zero specificity, so any element that deliberately sizes itself larger still wins. */ @media (pointer: coarse) { :where(a, button, [role="button"], summary) { min-height: 24px; } /* Width too: icon buttons run 14-23px across, which height alone would leave as a tall sliver rather than a real target. Inert on inline elements, so links inside prose are untouched — same as the height. */ :where(a, button, [role="button"]) { min-width: 24px; } /* A checkbox is square, so height alone would leave a 16x24 sliver. */ :where(input[type="checkbox"], input[type="radio"]) { min-height: 24px; min-width: 24px; } } /* ── Hover-revealed row actions ───────────────────────────────────── */ /* List rows hide their delete/reorder buttons behind `group-hover:opacity-100`. Hover is the one interaction a keyboard has no access to, so those buttons stayed at opacity:0 while still being focusable — a keyboard user could tab to "remove" and never see which row it belonged to. Touch has no hover either, so the same controls were unreachable on a phone. Hooking Tailwind's own generated class fixes every occurrence at once, and keeps future ones correct without anyone remembering this rule. */ .group:focus-within .group-hover\:opacity-100 { opacity: 1; } @media (hover: none) { .group .group-hover\:opacity-100 { opacity: 1; } } /* High-contrast environment — bump the ring to 3px and use foreground for maximum delta against any backdrop. */ @media (prefers-contrast: more) { :where(a, button, input, textarea, select, summary, [role="button"], [role="link"], [role="menuitem"], [tabindex]):focus-visible { outline: 3px solid hsl(var(--foreground)); outline-offset: 3px; } } /* Dark-mode glow */ .dark .glow-primary { box-shadow: 0 0 24px -6px hsl(var(--accent) / 0.45); } .glow-primary { box-shadow: 0 4px 14px -4px hsl(var(--accent) / 0.25); } /* Reduced-motion safety net. - Decorative loops (orbs, sparks, pulses) are killed outright. - One-shot reveals (fade-up, scale-in) snap to instant — content still appears in its final state, just without travel/opacity tweens. - Spinners and the looping privacy marquee fall through too — keep the visual cue but stop the motion. */ @media (prefers-reduced-motion: reduce) { .hero-orb, .hero-orb-1, .hero-orb-2, .animate-pulse-border, .animate-shimmer, .dz-breathing, .progress-indeterminate, .spinner-calm, .animate-accent-pulse, .animate-status-pulse, .pipeline-spark-down, .animate-pipeline-spark-x, .animate-confetti, /* New ambient loops added with the micro-interaction pass. */ .animate-processing-pulse, /* Looping decorative elements that previously slipped through. */ .pipeline-file, .pipeline-chip, .marquee-track, /* tailwind utility that lucide spinners use; kill the rotation. */ .animate-spin { animation: none !important; } .animate-fade-up, .animate-fade-in, .animate-slide-in-left, .animate-slide-in-right, .animate-slide-down, .animate-scale-in, .workspace-enter, .animate-success-pop, .animate-pop-in, .animate-corner-extend > .corner-mark, .animate-underline-grow, .animate-grid-enter > *, .animate-count-up, .animate-crossfade, /* One-shot reveals added by the micro-interaction pass. */ .animate-copy-flash, .animate-queue-row-enter, .animate-queue-row-leave, .animate-dropzone-landed, .section-mark.reveal-underline::after { animation-duration: 0.001s !important; animation-delay: 0s !important; } /* Dragging — snap end-state, kill the transition wobble. */ .dragging { transition: none !important; transform: none !important; } /* Sidebar width transition — snap to final width with no animation. */ aside.transition-\[width\] { transition: none !important; } /* Sticky TOC marker — snap to position, don't animate the slide. */ .toc-rail-marker { transition: none !important; } /* Reading-progress bar uses transition-[width] in BlogPost / Privacy / Terms; snap it to instant via the role selector that all three share. */ [role="progressbar"] { transition: none !important; } /* Shine sweep on hover — kill the slow horizontal sweep. */ .shine-sweep::after { transition: none !important; } /* Tailwind animate.css-style classes (used by Radix portals + framer-motion wrappers) — disable the slide-in for dialog and palette so users with prefers-reduced-motion don't see a 200ms slide on every open. */ [class*="animate-in"], [class*="fade-in-"], [class*="slide-in-"] { animation-duration: 0.001s !important; animation-delay: 0s !important; } } /* ── Hero artwork (composition for the right side of the hero) ────── */ .hero-artwork { perspective: 1000px; } @keyframes hero-orbit-slow { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes hero-orbit-rev { from { transform: rotate(0deg); } to { transform: rotate(-360deg); } } .hero-art-ring { border-radius: 9999px; border: 1px dashed hsl(var(--border-strong)); opacity: 0.55; animation: hero-orbit-slow 50s linear infinite; } .hero-art-ring-2 { border: 1px dashed hsl(var(--accent) / 0.25); animation: hero-orbit-rev 38s linear infinite; } @keyframes hero-chip-float { 0%, 100% { transform: translate(var(--tx), var(--ty)) translateY(0); } 50% { transform: translate(var(--tx), var(--ty)) translateY(-6px); } } .hero-art-chip { animation: hero-chip-float 4.5s ease-in-out infinite; will-change: transform; } @keyframes hero-doc-drift-l { 0%, 100% { transform: translateY(-50%) rotate(-7deg); } 50% { transform: translateY(calc(-50% - 8px)) rotate(-8deg); } } @keyframes hero-doc-drift-r { 0%, 100% { transform: translateY(-50%) rotate(8deg); } 50% { transform: translateY(calc(-50% - 8px)) rotate(9deg); } } .hero-art-doc-left { animation: hero-doc-drift-l 6.5s ease-in-out infinite; } .hero-art-doc-right { animation: hero-doc-drift-r 7s ease-in-out infinite 0.4s; } @keyframes hero-shield-breathe { 0%, 100% { transform: translate(-50%, -50%) scale(1); } 50% { transform: translate(-50%, -50%) scale(1.04); } } .hero-art-shield { animation: hero-shield-breathe 3.8s ease-in-out infinite; } @media (prefers-reduced-motion: reduce) { .hero-art-ring, .hero-art-ring-2, .hero-art-chip, .hero-art-doc-left, .hero-art-doc-right, .hero-art-shield { animation: none !important; } } /* Drop-cap — kept neutralised for blog backwards-compat */ .drop-cap::first-letter { font-size: inherit; float: none; padding: 0; color: inherit; } /* ── Blog prose — editorial Fraunces body ─────────────────────────── */ .blog-prose { font-family: var(--font-display); font-weight: 400; font-size: 18px; line-height: 1.7; color: hsl(var(--foreground)); font-variation-settings: "opsz" 14; } .blog-prose p { margin: 0 0 1.25rem; } .blog-prose h2 { font-family: var(--font-display); font-size: 1.625rem; font-weight: 700; letter-spacing: -0.02em; margin: 2.5rem 0 0.75rem; color: hsl(var(--foreground)); } .blog-prose h3 { font-family: var(--font-display); font-size: 1.225rem; font-weight: 600; letter-spacing: -0.015em; margin: 1.75rem 0 0.5rem; color: hsl(var(--foreground)); } /* ── Heading hash-link anchors ──────────────────────────────────── Headings tagged with .heading-anchor (injected by BlogPostPage) get a visible § marker on hover. The marker is positioned to the left so the heading text stays put. */ .blog-prose .heading-anchor { position: relative; transition: color 150ms ease; } .blog-prose .heading-anchor::before { content: "§"; position: absolute; left: -1.25rem; top: 0.05em; color: hsl(var(--accent)); opacity: 0; transition: opacity 150ms ease, transform 150ms ease; transform: translateX(4px); font-weight: 600; } .blog-prose .heading-anchor:hover::before { opacity: 1; transform: translateX(0); } .blog-prose .heading-anchor:hover { color: hsl(var(--accent)); } @media (max-width: 640px) { .blog-prose .heading-anchor::before { display: none; } } .blog-prose ul, .blog-prose ol { margin: 0 0 1.25rem 1.5rem; padding: 0; } .blog-prose ul { list-style: disc; } .blog-prose ol { list-style: decimal; } .blog-prose li { margin-bottom: 0.4rem; } .blog-prose a { color: hsl(var(--accent)); text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; } .blog-prose a:hover { text-decoration-thickness: 2px; } .blog-prose strong { font-weight: 600; } .blog-prose em { font-style: italic; } .blog-prose code { font-family: var(--font-mono); font-size: 0.85em; background: hsl(var(--muted)); padding: 0.15em 0.4em; border-radius: 4px; } .blog-prose pre { font-family: var(--font-mono); font-size: 0.85rem; background: hsl(var(--paper-2)); border: 1px solid hsl(var(--border)); border-radius: 8px; padding: 1rem 1.25rem; overflow-x: auto; margin: 0 0 1.5rem; } .blog-prose pre code { background: none; padding: 0; font-size: inherit; } .blog-prose table { display: block; width: 100%; max-width: 100%; overflow-x: auto; border-collapse: collapse; margin: 0 0 1.5rem; font-family: var(--font-sans); font-size: 0.95rem; -webkit-overflow-scrolling: touch; } .blog-prose th { text-align: left; padding: 0.6rem 0.85rem; border-bottom: 2px solid hsl(var(--border-strong)); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; color: hsl(var(--muted-foreground)); white-space: nowrap; font-family: var(--font-mono); font-weight: 500; } .blog-prose td { padding: 0.6rem 0.85rem; border-bottom: 1px solid hsl(var(--border)); } .blog-prose tr:nth-child(even) td { background: hsl(var(--paper-2) / 0.6); } .blog-prose blockquote { margin: 1.5rem 0; padding: 0.25rem 0 0.25rem 1.5rem; border-left: 1px solid hsl(var(--border-strong)); color: hsl(var(--muted-foreground)); font-style: italic; font-family: var(--font-display); } .blog-prose hr { border: 0; border-top: 1px solid hsl(var(--border)); margin: 2.5rem 0; } .blog-prose img { max-width: 100%; height: auto; border-radius: 8px; margin: 1.5rem 0; border: 1px solid hsl(var(--border)); } .blog-prose h2:first-child, .blog-prose h3:first-child { margin-top: 0; } /* "confetti" reused by tool success states */ @keyframes confetti-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.005); } } .animate-confetti { animation: confetti-pulse 2.5s ease-in-out infinite; } /* ── Workshop micro-interactions ───────────────────────────────────── Confident, precise — never bouncy. Out-expo easing `cubic-bezier(0.16, 0.84, 0.44, 1)` is the house default. */ /* Copy-success flash — accent ring pulse on the Copy button after the click. Drives every "Copied" affordance (hash, hex, snippet, etc.). */ @keyframes copy-flash { 0% { box-shadow: 0 0 0 0 hsl(var(--accent) / 0.55); background-color: hsl(var(--accent) / 0.12); } 60% { box-shadow: 0 0 0 4px hsl(var(--accent) / 0.12); background-color: hsl(var(--accent) / 0.06); } 100% { box-shadow: 0 0 0 0 hsl(var(--accent) / 0); background-color: transparent; } } .animate-copy-flash { animation: copy-flash 0.5s cubic-bezier(0.16, 0.84, 0.44, 1) both; } /* Drag-drop ghost — applied to the row/cell while it's being dragged. Subtle scale + cursor-grabbing + accent ring. Tactile, not jumpy. */ .dragging { cursor: grabbing !important; transform: scale(0.98); box-shadow: 0 0 0 1px hsl(var(--accent) / 0.55), 0 8px 22px -6px hsl(var(--accent) / 0.25), 0 2px 6px -1px rgba(20,15,5,0.10); opacity: 0.92; transition: transform 160ms cubic-bezier(0.16, 0.84, 0.44, 1), box-shadow 160ms cubic-bezier(0.16, 0.84, 0.44, 1); } /* File-add slide-in — used when a freshly-uploaded row lands in a queue. */ @keyframes queue-row-enter { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: translateY(0); } } .animate-queue-row-enter { animation: queue-row-enter 0.28s cubic-bezier(0.16, 0.84, 0.44, 1) both; } @keyframes queue-row-leave { from { opacity: 1; transform: translateY(0); max-height: 80px; } to { opacity: 0; transform: translateY(-6px); max-height: 0; } } .animate-queue-row-leave { animation: queue-row-leave 0.22s cubic-bezier(0.4, 0, 1, 1) both; } /* Processing pulse — the currently-active row in a queue gets a quiet, confident accent-border breath. 1.6s, infinite. */ @keyframes processing-pulse { 0%, 100% { box-shadow: inset 0 0 0 1px hsl(var(--accent) / 0.35); } 50% { box-shadow: inset 0 0 0 1px hsl(var(--accent) / 0.85), 0 0 0 3px hsl(var(--accent) / 0.10); } } .animate-processing-pulse { animation: processing-pulse 1.6s ease-in-out infinite; border-color: transparent; } /* Drop-zone landed — quick accent flash after a successful drop, then the zone re-renders as the file card. */ @keyframes dropzone-landed { 0% { background-color: hsl(var(--accent) / 0.18); border-color: hsl(var(--accent)); } 60% { background-color: hsl(var(--accent) / 0.08); } 100% { background-color: transparent; border-color: hsl(var(--border-strong)); } } .animate-dropzone-landed { animation: dropzone-landed 0.45s cubic-bezier(0.16, 0.84, 0.44, 1) both; } /* Button press — a subtle inner-ring pulse on .btn-accent:active for tactile feedback. Layered on top of the existing translateY(1px). */ @keyframes button-press-ring { 0% { box-shadow: inset 0 0 0 2px hsl(var(--accent-foreground) / 0.55); } 100% { box-shadow: inset 0 0 0 2px hsl(var(--accent-foreground) / 0); } } .btn-accent:active { animation: button-press-ring 0.18s cubic-bezier(0.4, 0, 0.2, 1) both; } /* Underline reveal — § kicker labels grow a delicate hairline on first view. Apply via `.section-mark.reveal-underline` (opt-in, above the fold) so we don't pay for it on every § in the page. */ @keyframes underline-reveal { from { transform: scaleX(0); opacity: 0; } to { transform: scaleX(1); opacity: 1; } } .section-mark.reveal-underline { position: relative; padding-bottom: 4px; } .section-mark.reveal-underline::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: linear-gradient(90deg, hsl(var(--accent) / 0.65), transparent 95%); transform-origin: left center; animation: underline-reveal 0.6s cubic-bezier(0.16, 0.84, 0.44, 1) 0.2s both; } /* Sticky TOC active marker — single rail that slides vertically. Consumed via inline `style={{ transform: 'translateY(Npx)' }}` so the transition is the only thing that lives in CSS. */ .toc-rail-marker { position: absolute; left: 0; top: 0; width: 2px; height: 16px; border-radius: 999px; background: hsl(var(--accent)); transition: transform 280ms cubic-bezier(0.16, 0.84, 0.44, 1), opacity 200ms ease-out; will-change: transform; pointer-events: none; } /* ── Print styles ─────────────────────────────────────────────────── */ /* When users print a blog post, About, Privacy, or Terms page we want a clean editorial document — no app chrome, no reading aids, just the article body with URL footnotes. */ @media print { /* Hide app shell — topbar, sidebar, statusbar, mobile drawer, modals. The article's own
with the post title must survive print, so we exclude
elements nested inside an article. */ header:not(article *):not(article header), footer:not(article *):not(article footer), aside[aria-label="Tool navigation"], aside[aria-label="Mobile menu"], [aria-label="Primary"], [aria-label="Breadcrumb"], [aria-label="Mobile primary navigation"], [role="dialog"], .pipeline-anim, .hero-orb, .hero-backdrop, .hero-artwork, .hero-blade, button, [role="button"]:not([data-print]), .icon-tile, .animate-confetti, .code-copy-btn, /* Reading aids — progress bar, sticky TOC, scroll-to-top, status bar. */ .sticky.top-0.z-30, .fixed.bottom-8, [aria-label="Table of contents"], [aria-label="Document navigation"], /* Toasts and announce regions. */ [data-sonner-toaster], [data-radix-toast-viewport] { display: none !important; } /* Force light theme regardless of user's preference — dark backgrounds waste ink and Fraunces is engineered for paper, not screens. */ html, body, main, section, article, aside, .blog-prose { background: white !important; color: black !important; } .blog-prose strong, .blog-prose h1, .blog-prose h2, .blog-prose h3 { color: black !important; } .blog-prose blockquote { color: #333 !important; border-left-color: #999 !important; } /* Links — print full URL inline so the printed copy is self-contained. */ a { color: black !important; text-decoration: underline; } a[href^="http"]::after, a[href^="mailto:"]::after { content: " (" attr(href) ")"; font-size: 0.85em; color: #555; } /* Don't print URLs for in-document anchors. */ a[href^="#"]::after, a[href^="javascript"]::after { content: ""; } @page { margin: 1.6cm; } .blog-prose { font-size: 11pt; line-height: 1.55; max-width: none; } .blog-prose pre, .blog-prose code { background: #f5f5f5 !important; color: black !important; border: 1px solid #ddd !important; } /* Pagination hints — avoid orphaned headings and split tables/code blocks. */ h1, h2, h3 { break-after: avoid; page-break-after: avoid; } pre, table, figure { break-inside: avoid; page-break-inside: avoid; } p, li { orphans: 3; widows: 3; } } /* ── Baseline a11y bumps for low-opacity text ─────────────────────── */ /* Many list / caption strings use `text-muted-foreground/50|/60|/70` for the editorial dimmed look. On the warm-paper background that drops below the WCAG AA 4.5:1 threshold. Raise the floor by mapping them to /85 in normal mode (still visibly secondary, but readable) and to full foreground when the user has requested more contrast. We only patch the opacity classes used for actual text — decorative dividers keep their original opacity via the `bg-` / `border-` prefixes which we leave alone. */ .text-muted-foreground\/50 { color: hsl(var(--muted-foreground) / 0.85) !important; } .text-muted-foreground\/60 { color: hsl(var(--muted-foreground) / 0.88) !important; } .text-muted-foreground\/70 { color: hsl(var(--muted-foreground) / 0.92) !important; } .text-foreground\/40, .text-foreground\/50 { color: hsl(var(--foreground) / 0.75) !important; } .text-muted-foreground\/40 { color: hsl(var(--muted-foreground) / 0.80) !important; } /* ── High-contrast adjustments ────────────────────────────────────── */ /* `prefers-contrast: more` is honored by macOS "Increase contrast" and Windows high-contrast modes. Thin borders at /30 opacity are the main problem — strengthen them so panels remain visually delimited. */ @media (prefers-contrast: more) { .border-accent\/30, .border-accent\/35, .border-accent\/45 { border-color: hsl(var(--accent)) !important; } .bg-accent\/\[0\.05\], .bg-accent\/\[0\.06\] { background-color: hsl(var(--accent) / 0.12) !important; } /* Editorial inserts and tool cards get a stronger hairline. */ .editorial-insert, .tool-card { border-color: hsl(var(--border-strong)) !important; } /* Text dimmed below normal contrast — bump foreground. */ .text-muted-foreground, .text-muted-foreground\/50, .text-muted-foreground\/60, .text-muted-foreground\/70, .text-muted-foreground\/85 { color: hsl(var(--foreground) / 0.90) !important; } .text-foreground\/40, .text-foreground\/50 { color: hsl(var(--foreground) / 0.95) !important; } } /* ── Reduced motion: the backstop ─────────────────────────────────── */ /* The blocks above disable animation selector by selector, which means every new keyframe has to remember to add itself — and several did not: the recovered hover transforms in skin-interactions.css, and the ported designs' own keyframes, were still moving for someone who asked the OS not to. This catches everything, including whatever gets added next. 0.01ms rather than `none` on purpose: the animation and transition still run, so anything waiting on animationend or transitionend still fires and nothing gets stuck half-shown. It is simply over before it is seen. */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; animation-delay: 0ms !important; transition-duration: 0.01ms !important; transition-delay: 0ms !important; scroll-behavior: auto !important; } }