Files
FlowReader/web/src/index.css
T
Antigravity AgentandClaude Opus 5.5 360f3cf012 feat(web): unified reader with reading settings, faster lists, a11y
- New Reader (desktop + mobile): loads full content on open, prefetches
  the next article, reading progress and time, "Aa" settings (light /
  sepia / dark / auto theme, serif / sans / Atkinson Hyperlegible font,
  size, line height, measure), Space-to-next, keyboard shortcuts, focus
  management, inert background, URL-driven (?a=) so Back closes it.
- Dashboard: cursor pagination, unread/all filter, CSS grid (no masonry),
  no re-sorting under the reader, optimistic cache patches instead of
  refetch storms, "new articles" banner, error/offline states, j/k/o/m/s/v
  shortcuts and help dialog.
- Cards: keyboard-openable, lazy <img>, memoized, touch-only swipe.
- Focus mode: framer drag (drop @use-gesture), Read button, undo restores
  unread state, arrow keys.
- WebSocket: single connection, backoff, cache patching, no console logs.
- Contrast fixes (on-nature tokens), 44px targets, lang="fr", reduced
  motion, self-hosted fonts, theme applied before first paint.
- Code splitting + vendor chunk, prompt-based PWA updates, smaller
  precache, offline cache for opened articles and images, caches cleared
  on logout; upgrade react-router (CVE), drop unused deps and assets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:34:09 +02:00

414 lines
13 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@import "tailwindcss";
/* ============================================================
FlowReader — Design System "Éditorial Botanique"
Light: Organic Warmth · Sepia: Paper · Dark: Deep Forest
Fonts are self-hosted (see main.tsx) — no third-party requests.
============================================================ */
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* Surfaces — warm paper */
--color-carbon: #F7F4EC; /* app background */
--color-carbon-light: #FFFFFF; /* raised surface / cards */
--color-carbon-dark: #E9E4D8; /* borders / sunken */
/* Brand — forest green */
--color-nature: #15643A;
--color-nature-light: #1E8A4F;
--color-nature-dark: #0A2E1B;
--color-on-nature: #FFFFFF; /* text on a nature-filled surface */
/* Accent — terracotta (used sparingly) */
--color-earth: #A9502F;
--color-earth-light: #C9714F;
--color-on-earth: #FFFFFF;
/* Ink */
--color-paper-white: #1B1714; /* primary text */
--color-paper-muted: #635C53; /* secondary text (≥ 5:1 on paper) */
/* Semantic */
--color-ring: #1E8A4F;
--color-danger: #B3390A;
/* Typography */
--font-serif: "Playfair Display Variable", "Playfair Display", Georgia, serif;
--font-sans: "Inter Variable", "Inter", system-ui, sans-serif;
--font-reading: "Lora Variable", "Lora", Georgia, serif;
--font-legible: "Atkinson Hyperlegible", "Inter Variable", system-ui, sans-serif;
/* Radii scale */
--radius-xs: 0.375rem;
--radius-sm: 0.625rem;
--radius-md: 0.875rem;
--radius-lg: 1.25rem;
--radius-xl: 1.75rem;
/* Shadows */
--shadow-soft: 0 2px 8px -2px rgba(27, 23, 20, 0.08), 0 4px 24px -6px rgba(27, 23, 20, 0.06);
--shadow-lift: 0 12px 32px -12px rgba(27, 23, 20, 0.18);
--shadow-float: 0 24px 60px -20px rgba(10, 46, 27, 0.28);
}
/* ---------- Sepia theme: long-form paper ---------- */
.sepia {
--color-carbon: #F4ECD8;
--color-carbon-light: #FBF5E6;
--color-carbon-dark: #E6DCC3;
--color-nature: #2D5F34;
--color-nature-light: #3B7A44;
--color-nature-dark: #1C3320;
--color-earth: #9A4A2A;
--color-paper-white: #3B2F24;
--color-paper-muted: #6B5A47;
--color-ring: #3B7A44;
}
/* ---------- Dark theme: Deep Forest (no pure black / pure white) ---------- */
.dark {
--color-carbon: #0E130F;
--color-carbon-light: #161D17;
--color-carbon-dark: #090D0A;
--color-nature: #34D27B;
--color-nature-light: #5DE89A;
--color-nature-dark: #0E1C12;
--color-on-nature: #06210F; /* dark ink on bright green: ~9:1 */
--color-earth: #E08B6B;
--color-earth-light: #E59A7E;
--color-on-earth: #2A0F05;
--color-paper-white: #E9E5DA;
--color-paper-muted: #A39E92;
--color-ring: #34D27B;
--color-danger: #F87045;
--shadow-soft: 0 2px 10px -2px rgba(0, 0, 0, 0.5);
--shadow-lift: 0 16px 40px -16px rgba(0, 0, 0, 0.6);
--shadow-float: 0 28px 70px -24px rgba(0, 0, 0, 0.7);
}
/* ============================================================
Custom utilities
============================================================ */
@utility paper-panel {
background: var(--color-carbon-light);
border: 1px solid color-mix(in oklab, var(--color-paper-muted) 16%, transparent);
box-shadow: var(--shadow-soft);
}
@utility text-balance {
text-wrap: balance;
}
@utility text-pretty {
text-wrap: pretty;
}
/* Skip rendering work for off-screen cards in long lists. */
@utility cv-auto {
content-visibility: auto;
contain-intrinsic-size: auto 440px;
}
/* ============================================================
Base
============================================================ */
@layer base {
*,
*::before,
*::after {
border-color: color-mix(in oklab, var(--color-paper-muted) 18%, transparent);
}
html {
-webkit-text-size-adjust: 100%;
}
body {
@apply bg-carbon text-paper-white font-sans antialiased;
margin: 0;
min-height: 100vh;
border-top: 3px solid var(--color-nature);
background-image: radial-gradient(ellipse 80% 50% at 50% -10%, color-mix(in oklab, var(--color-nature) 8%, transparent), transparent);
background-repeat: no-repeat;
transition: background-color 0.3s ease, color 0.3s ease;
}
h1, h2, h3, h4, h5, h6 {
@apply font-serif tracking-tight leading-tight text-paper-white;
}
a {
@apply transition-colors duration-200;
}
/* Accessible focus — keyboard only */
:focus-visible {
outline: 2px solid var(--color-ring);
outline-offset: 2px;
border-radius: var(--radius-xs);
}
:focus:not(:focus-visible) {
outline: none;
}
/* Scrollbars */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background: color-mix(in oklab, var(--color-paper-muted) 28%, transparent);
border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover {
background: color-mix(in oklab, var(--color-paper-muted) 45%, transparent);
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
}
/* ============================================================
Components
============================================================ */
@layer components {
/* Eyebrow / kicker label */
.eyebrow {
@apply text-[11px] uppercase tracking-[0.3em] font-bold text-nature;
}
/* Buttons */
.btn-primary {
@apply inline-flex items-center justify-center gap-2 bg-nature text-on-nature font-bold
min-h-11 px-6 py-2.5 rounded-full text-xs uppercase tracking-[0.16em]
border border-transparent transition-all duration-200
hover:bg-nature-light hover:shadow-[0_8px_24px_-8px_rgba(21,100,58,0.5)]
active:scale-[0.97] disabled:opacity-50 disabled:pointer-events-none;
}
.btn-secondary {
@apply inline-flex items-center justify-center gap-2 bg-transparent text-nature font-bold
min-h-11 px-5 py-2.5 rounded-full text-xs uppercase tracking-[0.16em]
border border-nature/35 transition-all duration-200
hover:bg-nature hover:text-on-nature hover:border-nature
active:scale-[0.97] disabled:opacity-50 disabled:pointer-events-none;
}
.btn-ghost {
@apply inline-flex items-center justify-center gap-2 text-paper-muted font-medium
min-h-11 px-4 py-2 rounded-full text-sm transition-all duration-200
hover:text-nature hover:bg-nature/5;
}
.icon-btn {
@apply inline-flex items-center justify-center w-11 h-11 shrink-0 rounded-full text-nature
border border-nature/20 transition-all duration-200
hover:bg-nature/10 hover:border-nature/35 active:scale-95
disabled:opacity-40 disabled:pointer-events-none;
}
/* Segmented control (filters, reader settings) */
.segmented {
@apply inline-flex p-1 gap-1 rounded-full bg-carbon-dark/70 border border-paper-muted/15;
}
.segmented > button {
@apply min-h-9 px-3.5 rounded-full text-xs font-semibold text-paper-muted transition-colors
hover:text-paper-white;
}
.segmented > button[aria-pressed="true"],
.segmented > button[aria-checked="true"] {
@apply bg-carbon-light text-paper-white shadow-sm;
}
/* Inputs */
.input-field {
@apply w-full bg-carbon-light text-paper-white px-4 py-3 rounded-xl
border border-paper-muted/25 shadow-sm transition-all duration-200
placeholder:text-paper-muted
focus:border-nature/50 focus:ring-2 focus:ring-nature/25 outline-none;
}
/* Chip / badge */
.chip {
@apply inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full
text-[11px] font-bold uppercase tracking-[0.14em]
bg-nature/10 text-nature;
}
/* Surface card */
.surface-card {
@apply bg-carbon-light rounded-2xl border border-paper-muted/12;
box-shadow: var(--shadow-soft);
}
/* Keyboard hint */
.kbd {
@apply inline-flex items-center justify-center min-w-6 h-6 px-1.5 rounded-md border border-paper-muted/30
bg-carbon-light text-[11px] font-mono font-semibold text-paper-white shadow-sm;
}
/* ---------- Article body ----------
Driven by reader preferences via CSS variables set on the reader:
--reader-size, --reader-leading, --reader-font. Values follow
long-form reading guidance (50–75 chars, 1.5–1.7 line-height). */
.magazine-content {
font-family: var(--reader-font, var(--font-reading));
font-size: var(--reader-size, 19px);
line-height: var(--reader-leading, 1.65);
color: color-mix(in oklab, var(--color-paper-white) 92%, transparent);
overflow-wrap: break-word;
font-kerning: normal;
font-variant-ligatures: common-ligatures;
}
.magazine-content > * + * { margin-top: 1.15em; }
.magazine-content p,
.magazine-content li { text-wrap: pretty; }
@media (max-width: 640px) {
.magazine-content { hyphens: auto; }
}
.magazine-content a {
@apply text-nature font-medium;
text-decoration: underline;
text-decoration-color: color-mix(in oklab, currentColor 45%, transparent);
text-decoration-thickness: 1px;
text-underline-offset: 0.18em;
}
.magazine-content a:hover { text-decoration-color: currentColor; }
.magazine-content h1,
.magazine-content h2,
.magazine-content h3,
.magazine-content h4 {
@apply text-paper-white font-serif leading-snug;
margin-top: 2em;
margin-bottom: 0.6em;
text-wrap: balance;
}
.magazine-content h1 { font-size: 1.6em; }
.magazine-content h2 { font-size: 1.4em; }
.magazine-content h3 { font-size: 1.2em; }
.magazine-content h4 { font-size: 1.05em; @apply font-semibold; }
.magazine-content h5,
.magazine-content h6 {
@apply font-sans font-bold uppercase tracking-wide text-paper-muted;
font-size: 0.8em;
margin-top: 1.8em;
}
.magazine-content h2 + *,
.magazine-content h3 + *,
.magazine-content h4 + * { margin-top: 0; }
.magazine-content img,
.magazine-content video {
@apply rounded-xl mx-auto max-w-full h-auto;
margin-top: 1.6em;
margin-bottom: 1.6em;
}
/* Tracking pixels and spacer GIFs */
.magazine-content img[width="1"],
.magazine-content img[height="1"] { display: none; }
.magazine-content figure { margin: 1.8em 0; }
.magazine-content figure img { margin: 0 auto; }
.magazine-content figcaption {
@apply mt-3 text-sm text-paper-muted font-sans text-center leading-normal;
}
.magazine-content blockquote {
@apply border-l-4 border-nature/40 pl-5 italic text-paper-white/85;
}
.magazine-content ul,
.magazine-content ol { padding-left: 1.4em; }
.magazine-content ul { list-style: disc; }
.magazine-content ol { list-style: decimal; }
.magazine-content li + li { margin-top: 0.4em; }
.magazine-content li { @apply marker:text-nature; }
.magazine-content hr {
@apply border-0 h-px bg-paper-muted/25 mx-auto w-1/3;
margin-top: 2.4em;
margin-bottom: 2.4em;
}
.magazine-content pre {
@apply bg-carbon-dark text-paper-white rounded-xl p-4 overflow-x-auto text-sm leading-relaxed;
}
.magazine-content code {
@apply font-mono text-[0.88em];
}
.magazine-content :not(pre) > code {
@apply bg-carbon-dark/80 px-1.5 py-0.5 rounded text-earth;
}
.magazine-content pre code { color: inherit; font-size: inherit; }
.magazine-content table {
display: block;
max-width: 100%;
overflow-x: auto;
border-collapse: collapse;
@apply text-[0.85em] font-sans;
}
.magazine-content th,
.magazine-content td {
@apply border border-paper-muted/25 px-3 py-2 text-left align-top;
}
.magazine-content th { @apply bg-carbon-dark font-semibold; }
/* Drop cap on the first paragraph (desktop reader only) */
@media (min-width: 640px) {
.drop-cap > p:first-child::first-letter {
@apply font-serif text-nature float-left mr-3 mt-1;
font-size: 3.6em;
line-height: 0.85;
}
}
}
/* ============================================================
Keyframes & motion utilities
============================================================ */
@layer utilities {
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
.no-scrollbar::-webkit-scrollbar { display: none; }
.animate-fade-in { animation: fade-in 0.35s ease-out both; }
.animate-rise { animation: rise 0.45s cubic-bezier(0.22, 1, 0.36, 1) both; }
.animate-bounce-slow { animation: bounce-slow 3s infinite; }
.animate-shake { animation: shake 0.4s ease-in-out; }
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes rise {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes bounce-slow {
0%, 100% { transform: translateY(-5%); animation-timing-function: cubic-bezier(0.8, 0, 1, 1); }
50% { transform: translateY(0); animation-timing-function: cubic-bezier(0, 0, 0.2, 1); }
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
}