Mobile : menu déroulant animé + sélecteur de consentement témoins + page confidentialité
- Header : hamburger animé (-> X), panneau déroulant avec liens en cascade (Logements, Carte, Stats, Sources, Confidentialité), backdrop flouté, verrouillage du défilement, fermeture à la navigation - CookieConsent : bandeau au premier chargement (aucun témoin tiers — honnête), 3 choix (tout accepter / essentiels / personnaliser avec cases préférences et statistiques), mémorisé en localStorage, ré-ouvrable depuis le pied de page et la page confidentialité - /confidentialite : politique de confidentialité et de témoins - Vue carte synchronisée avec l'URL (?view=carte depuis le menu) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Showing 5 changed files with +333 and −2
modified
frontend/src/App.tsx
+65 −2
@@ -4,10 +4,12 @@ | ||
| 4 | 4 | // App.tsx : layout global (header + ticker en direct + footer encre) et routage |
| 5 | 5 | // ----------------------------------------------------------------------------- |
| 6 | 6 | import { useEffect, useState } from "react"; |
| 7 | −import { NavLink, Route, Routes } from "react-router-dom"; | |
| 7 | +import { NavLink, Route, Routes, useLocation } from "react-router-dom"; | |
| 8 | 8 | import { fetchFacets, fetchSources, fetchStats, registerSourceNames, sourceName } from "./api"; |
| 9 | +import CookieConsent from "./components/CookieConsent"; | |
| 9 | 10 | import Home from "./pages/Home"; |
| 10 | 11 | import ListingPage from "./pages/Listing"; |
| 12 | +import PrivacyPage from "./pages/Privacy"; | |
| 11 | 13 | import SourcesPage from "./pages/Sources"; |
| 12 | 14 | import StatsPage from "./pages/Stats"; |
| 13 | 15 | |
@@ -47,7 +49,25 @@ function Ticker() { | ||
| 47 | 49 | ); |
| 48 | 50 | } |
| 49 | 51 | |
| 52 | +const NAV_LINKS = [ | |
| 53 | + { to: "/", label: "Logements", icon: "🏠", end: true }, | |
| 54 | + { to: "/?view=carte", label: "Carte", icon: "◈", end: false, force: true }, | |
| 55 | + { to: "/stats", label: "Stats", icon: "📊", end: false }, | |
| 56 | + { to: "/sources", label: "Sources", icon: "🗂", end: false }, | |
| 57 | + { to: "/confidentialite", label: "Confidentialité", icon: "🔒", end: false }, | |
| 58 | +]; | |
| 59 | + | |
| 50 | 60 | function Header() { |
| 61 | + const [open, setOpen] = useState(false); | |
| 62 | + const location = useLocation(); | |
| 63 | + | |
| 64 | + // fermer le menu à chaque navigation + verrouiller le défilement en dessous | |
| 65 | + useEffect(() => { setOpen(false); }, [location]); | |
| 66 | + useEffect(() => { | |
| 67 | + document.body.style.overflow = open ? "hidden" : ""; | |
| 68 | + return () => { document.body.style.overflow = ""; }; | |
| 69 | + }, [open]); | |
| 70 | + | |
| 51 | 71 | return ( |
| 52 | 72 | <> |
| 53 | 73 | <header className="header"> |
@@ -56,7 +76,7 @@ function Header() { | ||
| 56 | 76 | Lou<span className="ka">Ka</span> |
| 57 | 77 | <span className="brand-tag">Québec · Lévis · Montréal — toujours à jour</span> |
| 58 | 78 | </NavLink> |
| 59 | − <nav className="nav"> | |
| 79 | + <nav className="nav" aria-label="Navigation principale"> | |
| 60 | 80 | <NavLink to="/" end className={({ isActive }) => (isActive ? "active" : "")}> |
| 61 | 81 | Logements |
| 62 | 82 | </NavLink> |
@@ -67,8 +87,40 @@ function Header() { | ||
| 67 | 87 | Sources |
| 68 | 88 | </NavLink> |
| 69 | 89 | </nav> |
| 90 | + <button | |
| 91 | + className={`menu-btn ${open ? "open" : ""}`} | |
| 92 | + aria-expanded={open} | |
| 93 | + aria-label={open ? "Fermer le menu" : "Ouvrir le menu"} | |
| 94 | + onClick={() => setOpen(!open)} | |
| 95 | + > | |
| 96 | + <span /><span /><span /> | |
| 97 | + </button> | |
| 98 | + </div> | |
| 99 | + | |
| 100 | + {/* menu déroulant mobile */} | |
| 101 | + <div className={`mobile-menu ${open ? "open" : ""}`} role="navigation" aria-label="Menu mobile"> | |
| 102 | + {NAV_LINKS.map((l, i) => ( | |
| 103 | + <NavLink | |
| 104 | + key={l.to} | |
| 105 | + to={l.to} | |
| 106 | + end={l.end} | |
| 107 | + style={{ transitionDelay: open ? `${60 + i * 45}ms` : "0ms" }} | |
| 108 | + className={({ isActive }) => | |
| 109 | + `mm-link ${isActive && !l.force ? "active" : ""}`} | |
| 110 | + onClick={() => setOpen(false)} | |
| 111 | + > | |
| 112 | + <span className="mm-ico" aria-hidden="true">{l.icon}</span> | |
| 113 | + {l.label} | |
| 114 | + <span className="mm-arrow" aria-hidden="true">→</span> | |
| 115 | + </NavLink> | |
| 116 | + ))} | |
| 117 | + <div className="mm-foot"> | |
| 118 | + Agrégateur indépendant — mis à jour automatiquement, chaque fiche | |
| 119 | + renvoie à l'annonce originale. | |
| 120 | + </div> | |
| 70 | 121 | </div> |
| 71 | 122 | </header> |
| 123 | + {open && <div className="mm-backdrop" onClick={() => setOpen(false)} aria-hidden="true" />} | |
| 72 | 124 | <Ticker /> |
| 73 | 125 | </> |
| 74 | 126 | ); |
@@ -90,6 +142,15 @@ function Footer() { | ||
| 90 | 142 | </div> |
| 91 | 143 | <div className="fmono"> |
| 92 | 144 | © {new Date().getFullYear()} Simon-Pierre Boucher — contact@spboucher.ai |
| 145 | + {" · "} | |
| 146 | + <NavLink to="/confidentialite">Confidentialité</NavLink> | |
| 147 | + {" · "} | |
| 148 | + <button | |
| 149 | + className="flink" | |
| 150 | + onClick={() => window.dispatchEvent(new Event("louka:openConsent"))} | |
| 151 | + > | |
| 152 | + Gérer mes témoins | |
| 153 | + </button> | |
| 93 | 154 | </div> |
| 94 | 155 | </div> |
| 95 | 156 | </footer> |
@@ -106,6 +167,7 @@ export default function App() { | ||
| 106 | 167 | <Route path="/logement/:uid" element={<ListingPage />} /> |
| 107 | 168 | <Route path="/stats" element={<StatsPage />} /> |
| 108 | 169 | <Route path="/sources" element={<SourcesPage />} /> |
| 170 | + <Route path="/confidentialite" element={<PrivacyPage />} /> | |
| 109 | 171 | <Route |
| 110 | 172 | path="*" |
| 111 | 173 | element={ |
@@ -119,6 +181,7 @@ export default function App() { | ||
| 119 | 181 | </Routes> |
| 120 | 182 | </main> |
| 121 | 183 | <Footer /> |
| 184 | + <CookieConsent /> | |
| 122 | 185 | </> |
| 123 | 186 | ); |
| 124 | 187 | } |
added
frontend/src/components/CookieConsent.tsx
+103 −0
@@ -0,0 +1,103 @@ | ||
| 1 | +// ----------------------------------------------------------------------------- | |
| 2 | +// Lou-Ka — Agrégateur de logements à louer (province de Québec) | |
| 3 | +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 4 | +// components/CookieConsent.tsx : bandeau de consentement (témoins/stockage) | |
| 5 | +// Honnête par design : Lou-Ka n'utilise AUCUN témoin tiers ni traceur. | |
| 6 | +// Le sélecteur couvre le stockage local (préférences) et une éventuelle | |
| 7 | +// mesure d'audience anonyme future. Choix mémorisé en localStorage, | |
| 8 | +// ré-ouvrable via l'événement « louka:openConsent » (lien du pied de page). | |
| 9 | +// ----------------------------------------------------------------------------- | |
| 10 | +import { useEffect, useState } from "react"; | |
| 11 | +import { Link } from "react-router-dom"; | |
| 12 | + | |
| 13 | +const KEY = "louka-consent-v1"; | |
| 14 | + | |
| 15 | +export interface Consent { | |
| 16 | + essential: true; // toujours actif (fonctionnement du site) | |
| 17 | + preferences: boolean; // mémoriser filtres et vue liste/carte | |
| 18 | + statistics: boolean; // mesure d'audience anonyme (si activée un jour) | |
| 19 | + ts: number; | |
| 20 | +} | |
| 21 | + | |
| 22 | +export function getConsent(): Consent | null { | |
| 23 | + try { | |
| 24 | + const raw = localStorage.getItem(KEY); | |
| 25 | + return raw ? (JSON.parse(raw) as Consent) : null; | |
| 26 | + } catch { | |
| 27 | + return null; | |
| 28 | + } | |
| 29 | +} | |
| 30 | + | |
| 31 | +function save(preferences: boolean, statistics: boolean): Consent { | |
| 32 | + const c: Consent = { essential: true, preferences, statistics, ts: Date.now() }; | |
| 33 | + try { localStorage.setItem(KEY, JSON.stringify(c)); } catch { /* stockage bloqué */ } | |
| 34 | + return c; | |
| 35 | +} | |
| 36 | + | |
| 37 | +export default function CookieConsent() { | |
| 38 | + const [visible, setVisible] = useState(false); | |
| 39 | + const [custom, setCustom] = useState(false); | |
| 40 | + const [prefs, setPrefs] = useState(true); | |
| 41 | + const [stats, setStats] = useState(false); | |
| 42 | + | |
| 43 | + useEffect(() => { | |
| 44 | + if (getConsent() === null) setVisible(true); | |
| 45 | + const open = () => { setVisible(true); setCustom(true); }; | |
| 46 | + window.addEventListener("louka:openConsent", open); | |
| 47 | + return () => window.removeEventListener("louka:openConsent", open); | |
| 48 | + }, []); | |
| 49 | + | |
| 50 | + if (!visible) return null; | |
| 51 | + | |
| 52 | + const close = (p: boolean, s: boolean) => { save(p, s); setVisible(false); setCustom(false); }; | |
| 53 | + | |
| 54 | + return ( | |
| 55 | + <div className="cookie-banner" role="dialog" aria-modal="false" aria-label="Gestion des témoins"> | |
| 56 | + <div className="cookie-inner"> | |
| 57 | + <div className="cookie-text"> | |
| 58 | + <b>🍪 Vos témoins, vos choix.</b>{" "} | |
| 59 | + Lou-Ka n'utilise aucun traceur publicitaire ni témoin tiers. Le site | |
| 60 | + mémorise localement vos préférences (filtres, vue carte) et votre choix | |
| 61 | + de consentement. <Link to="/confidentialite">Politique de confidentialité</Link> | |
| 62 | + </div> | |
| 63 | + | |
| 64 | + {custom && ( | |
| 65 | + <div className="cookie-options"> | |
| 66 | + <label className="cookie-opt"> | |
| 67 | + <input type="checkbox" checked disabled /> | |
| 68 | + <span><b>Essentiels</b> — fonctionnement du site (toujours actifs)</span> | |
| 69 | + </label> | |
| 70 | + <label className="cookie-opt"> | |
| 71 | + <input type="checkbox" checked={prefs} onChange={(e) => setPrefs(e.target.checked)} /> | |
| 72 | + <span><b>Préférences</b> — retenir vos filtres et votre vue liste/carte</span> | |
| 73 | + </label> | |
| 74 | + <label className="cookie-opt"> | |
| 75 | + <input type="checkbox" checked={stats} onChange={(e) => setStats(e.target.checked)} /> | |
| 76 | + <span><b>Statistiques</b> — mesure d'audience anonyme, sans profilage</span> | |
| 77 | + </label> | |
| 78 | + </div> | |
| 79 | + )} | |
| 80 | + | |
| 81 | + <div className="cookie-actions"> | |
| 82 | + {!custom && ( | |
| 83 | + <button className="btn btn-ghost" onClick={() => setCustom(true)}> | |
| 84 | + Personnaliser | |
| 85 | + </button> | |
| 86 | + )} | |
| 87 | + <button className="btn btn-ghost" onClick={() => close(false, false)}> | |
| 88 | + Essentiels seulement | |
| 89 | + </button> | |
| 90 | + {custom ? ( | |
| 91 | + <button className="btn btn-primary" onClick={() => close(prefs, stats)}> | |
| 92 | + Enregistrer mes choix | |
| 93 | + </button> | |
| 94 | + ) : ( | |
| 95 | + <button className="btn btn-primary" onClick={() => close(true, true)}> | |
| 96 | + Tout accepter | |
| 97 | + </button> | |
| 98 | + )} | |
| 99 | + </div> | |
| 100 | + </div> | |
| 101 | + </div> | |
| 102 | + ); | |
| 103 | +} | |
modified
frontend/src/pages/Home.tsx
+4 −0
@@ -37,6 +37,10 @@ export default function Home() { | ||
| 37 | 37 | // vue liste ou carte (mémorisée dans l'URL : /?view=carte) |
| 38 | 38 | const [view, setView] = useState<"liste" | "carte">( |
| 39 | 39 | params.get("view") === "carte" ? "carte" : "liste"); |
| 40 | + useEffect(() => { | |
| 41 | + // suivre l'URL quand on navigue via le menu (« Carte » -> /?view=carte) | |
| 42 | + setView(params.get("view") === "carte" ? "carte" : "liste"); | |
| 43 | + }, [params]); | |
| 40 | 44 | const filters: ListingFilters = useMemo( |
| 41 | 45 | () => ({ q, city, source, price_max: priceMax, unit_type: unitType }), |
| 42 | 46 | [q, city, source, priceMax, unitType]); |
added
frontend/src/pages/Privacy.tsx
+66 −0
@@ -0,0 +1,66 @@ | ||
| 1 | +// ----------------------------------------------------------------------------- | |
| 2 | +// Lou-Ka — Agrégateur de logements à louer (province de Québec) | |
| 3 | +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 4 | +// pages/Privacy.tsx : politique de confidentialité et de témoins (cookies) | |
| 5 | +// ----------------------------------------------------------------------------- | |
| 6 | + | |
| 7 | +export default function PrivacyPage() { | |
| 8 | + const reopen = () => window.dispatchEvent(new Event("louka:openConsent")); | |
| 9 | + | |
| 10 | + return ( | |
| 11 | + <div className="container legal"> | |
| 12 | + <span className="kicker">Confidentialité & témoins</span> | |
| 13 | + <h1>Votre vie privée, simplement.</h1> | |
| 14 | + | |
| 15 | + <section> | |
| 16 | + <h3>Ce que Lou-Ka collecte</h3> | |
| 17 | + <p> | |
| 18 | + Rien qui vous identifie. Lou-Ka n'a pas de compte utilisateur, pas de | |
| 19 | + formulaire d'inscription, pas de pixel publicitaire et ne vend aucune | |
| 20 | + donnée. Le site consulte des annonces publiques de gestionnaires | |
| 21 | + immobiliers et vous les présente — c'est tout. | |
| 22 | + </p> | |
| 23 | + </section> | |
| 24 | + | |
| 25 | + <section> | |
| 26 | + <h3>Témoins et stockage local</h3> | |
| 27 | + <p> | |
| 28 | + Lou-Ka n'utilise <b>aucun témoin (cookie) tiers</b>. Le seul stockage | |
| 29 | + se fait dans votre navigateur (« localStorage ») : | |
| 30 | + </p> | |
| 31 | + <ul> | |
| 32 | + <li><b>Essentiels</b> — votre choix de consentement lui-même.</li> | |
| 33 | + <li><b>Préférences</b> (si acceptées) — vos filtres de recherche et votre vue liste/carte.</li> | |
| 34 | + <li><b>Statistiques</b> (si acceptées) — une éventuelle mesure d'audience anonyme, | |
| 35 | + sans identifiant ni profilage. Aucune n'est active à ce jour.</li> | |
| 36 | + </ul> | |
| 37 | + <p> | |
| 38 | + <button className="btn btn-ghost" onClick={reopen}>Modifier mes choix de témoins</button> | |
| 39 | + </p> | |
| 40 | + </section> | |
| 41 | + | |
| 42 | + <section> | |
| 43 | + <h3>Services externes</h3> | |
| 44 | + <ul> | |
| 45 | + <li><b>Photos des annonces</b> — chargées depuis les serveurs des gestionnaires | |
| 46 | + immobiliers ; leur politique s'applique à ces requêtes.</li> | |
| 47 | + <li><b>Carte</b> — tuiles vectorielles d'OpenFreeMap (données © OpenStreetMap), | |
| 48 | + chargées uniquement si vous ouvrez la vue carte.</li> | |
| 49 | + <li><b>Commodités de proximité</b> — calculées côté serveur à partir | |
| 50 | + d'OpenStreetMap ; votre navigateur ne contacte pas ces services.</li> | |
| 51 | + </ul> | |
| 52 | + </section> | |
| 53 | + | |
| 54 | + <section> | |
| 55 | + <h3>Vos annonces, vos sources</h3> | |
| 56 | + <p> | |
| 57 | + Chaque fiche renvoie vers l'annonce originale du gestionnaire. Pour | |
| 58 | + faire retirer un contenu ou pour toute question :{" "} | |
| 59 | + <a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a>. | |
| 60 | + </p> | |
| 61 | + </section> | |
| 62 | + | |
| 63 | + <div className="fine">Dernière mise à jour : août 2026.</div> | |
| 64 | + </div> | |
| 65 | + ); | |
| 66 | +} | |
modified
frontend/src/styles.css
+95 −0
@@ -586,3 +586,98 @@ img { display: block; } | ||
| 586 | 586 | .poi-ico { width: 20px; text-align: center; flex: 0 0 auto; } |
| 587 | 587 | .poi-name { flex: 1; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| 588 | 588 | .poi-dist { font-family: var(--font-mono); font-size: 11.5px; color: var(--ink); font-weight: 600; flex: 0 0 auto; } |
| 589 | + | |
| 590 | +/* --- Menu déroulant mobile ------------------------------------------------- */ | |
| 591 | +.menu-btn { | |
| 592 | + display: none; position: relative; z-index: 60; | |
| 593 | + width: 44px; height: 44px; margin-left: auto; | |
| 594 | + border: 1.5px solid var(--ink); border-radius: var(--r-ctl); | |
| 595 | + background: var(--surface); cursor: pointer; padding: 0; | |
| 596 | + flex-direction: column; align-items: center; justify-content: center; gap: 5px; | |
| 597 | + box-shadow: 3px 3px 0 rgba(20, 24, 20, 0.15); | |
| 598 | + transition: box-shadow 0.15s ease, transform 0.15s ease; | |
| 599 | +} | |
| 600 | +.menu-btn:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 rgba(20,24,20,0.15); } | |
| 601 | +.menu-btn span { | |
| 602 | + display: block; width: 18px; height: 2px; background: var(--ink); | |
| 603 | + border-radius: 2px; transition: transform 0.25s ease, opacity 0.2s ease; | |
| 604 | +} | |
| 605 | +.menu-btn.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); } | |
| 606 | +.menu-btn.open span:nth-child(2) { opacity: 0; } | |
| 607 | +.menu-btn.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); } | |
| 608 | + | |
| 609 | +.mobile-menu { | |
| 610 | + display: none; position: absolute; left: 0; right: 0; top: 100%; | |
| 611 | + background: var(--paper); border-bottom: 2px solid var(--ink); | |
| 612 | + box-shadow: 0 18px 30px rgba(20, 24, 20, 0.18); | |
| 613 | + padding: 8px 16px calc(16px + env(safe-area-inset-bottom)); | |
| 614 | + max-height: 0; overflow: hidden; opacity: 0; | |
| 615 | + transition: max-height 0.3s ease, opacity 0.22s ease; | |
| 616 | +} | |
| 617 | +.mobile-menu.open { max-height: 480px; opacity: 1; } | |
| 618 | +.mm-link { | |
| 619 | + display: flex; align-items: center; gap: 12px; | |
| 620 | + padding: 15px 10px; min-height: 52px; | |
| 621 | + font-family: var(--font-display); font-weight: 700; font-size: 19px; | |
| 622 | + letter-spacing: -0.02em; border-bottom: 1.5px dashed var(--line); | |
| 623 | + opacity: 0; transform: translateY(-8px); | |
| 624 | + transition: opacity 0.25s ease, transform 0.25s ease; | |
| 625 | +} | |
| 626 | +.mobile-menu.open .mm-link { opacity: 1; transform: translateY(0); } | |
| 627 | +.mm-link.active { color: var(--green); } | |
| 628 | +.mm-link.active .mm-arrow { opacity: 1; } | |
| 629 | +.mm-ico { width: 26px; text-align: center; font-size: 17px; } | |
| 630 | +.mm-arrow { margin-left: auto; opacity: 0.25; transition: opacity 0.15s ease; } | |
| 631 | +.mm-link:active { background: var(--lime-soft); border-radius: var(--r-ctl); } | |
| 632 | +.mm-foot { | |
| 633 | + padding: 12px 10px 4px; font-family: var(--font-mono); | |
| 634 | + font-size: 10.5px; color: var(--ink-3); letter-spacing: 0.04em; | |
| 635 | +} | |
| 636 | +.mm-backdrop { | |
| 637 | + position: fixed; inset: 0; z-index: 40; | |
| 638 | + background: rgba(20, 24, 20, 0.35); backdrop-filter: blur(2px); | |
| 639 | +} | |
| 640 | +@media (max-width: 760px) { | |
| 641 | + .menu-btn { display: flex; } | |
| 642 | + .nav { display: none; } | |
| 643 | + .mobile-menu { display: block; } | |
| 644 | +} | |
| 645 | + | |
| 646 | +/* --- Bandeau de consentement (témoins) ------------------------------------- */ | |
| 647 | +.cookie-banner { | |
| 648 | + position: fixed; left: 12px; right: 12px; | |
| 649 | + bottom: calc(12px + env(safe-area-inset-bottom)); z-index: 80; | |
| 650 | + animation: cookie-in 0.35s ease; | |
| 651 | +} | |
| 652 | +@keyframes cookie-in { from { transform: translateY(24px); opacity: 0; } } | |
| 653 | +.cookie-inner { | |
| 654 | + max-width: 720px; margin: 0 auto; background: var(--surface); | |
| 655 | + border: 2px solid var(--ink); border-radius: var(--r-card); | |
| 656 | + box-shadow: var(--shadow-off); padding: 16px 18px; | |
| 657 | +} | |
| 658 | +.cookie-text { font-size: 13.5px; color: var(--ink-2); line-height: 1.5; } | |
| 659 | +.cookie-text b { color: var(--ink); } | |
| 660 | +.cookie-text a { text-decoration: underline; text-underline-offset: 2px; } | |
| 661 | +.cookie-options { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; } | |
| 662 | +.cookie-opt { | |
| 663 | + display: flex; align-items: flex-start; gap: 10px; font-size: 13px; | |
| 664 | + color: var(--ink-2); cursor: pointer; | |
| 665 | +} | |
| 666 | +.cookie-opt input { margin-top: 2px; accent-color: var(--green); width: 16px; height: 16px; } | |
| 667 | +.cookie-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; justify-content: flex-end; } | |
| 668 | +@media (max-width: 640px) { | |
| 669 | + .cookie-actions { justify-content: stretch; } | |
| 670 | + .cookie-actions .btn { flex: 1; text-align: center; justify-content: center; } | |
| 671 | +} | |
| 672 | + | |
| 673 | +/* --- Page confidentialité + lien pied de page ------------------------------- */ | |
| 674 | +.legal { padding: 48px 0 70px; max-width: 760px; } | |
| 675 | +.legal h1 { font-size: clamp(30px, 5vw, 46px); margin: 12px 0 8px; letter-spacing: -0.03em; } | |
| 676 | +.legal section { margin-top: 26px; } | |
| 677 | +.legal h3 { font-size: 18px; margin-bottom: 8px; } | |
| 678 | +.legal p, .legal li { color: var(--ink-2); font-size: 14.5px; } | |
| 679 | +.legal ul { padding-left: 20px; display: flex; flex-direction: column; gap: 6px; } | |
| 680 | +.flink { | |
| 681 | + border: 0; background: none; padding: 0; cursor: pointer; | |
| 682 | + color: inherit; font: inherit; text-decoration: underline; text-underline-offset: 2px; | |
| 683 | +} | |
| 589 | 684 | |