SPB Git

Repository Public

Commit

Correctifs front-end mobile : menu hamburger, carte calee sur la vraie hauteur du header, viewport-fit=cover, socle mobile ka-ui

- App.tsx : menu mobile hamburger sous 700px (bouton 44x44 aria-expanded, panneau deroulant sous le header, backdrop fixe HORS du header, verrou de scroll body + cleanup, fermeture route/Escape/backdrop) ; ResizeObserver sur header.site qui pose --header-h sur <html>
- styles.css : .map-wrap = calc(100vh/100dvh - var(--header-h,58px)) (fini le saut de barre navigateur et le 58px faux sur header 2 lignes) ; styles .menu-btn/.mobile-menu/.mm-backdrop avec var(--z-modal)/var(--z-overlay) ; header.site a var(--z-header) ; champs filtres a 16px sous 700px (anti-zoom iOS) ; extension tactile ::after -8px sur a.logo
- index.html : meta viewport avec viewport-fit=cover (safe-area iOS)
- src/ka/tokens.css : section SOCLE MOBILE OBLIGATOIRE (z-index unifies, --vh100 dvh, verrous de scroll, filet anti-zoom 16px, extension tactile gk-badge)
- public/ka-agent.js : agent corrige (safe-area, textarea 16px, plein ecran <=640px + verrou scroll)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed · Aug 19, 2026 parent 5b72db6
5 changed files +259 −19 Browse files
modified frontend/index.html +1 −1
@@ -12,7 +12,7 @@ Créé : 2026-08-17 Modifié : 2026-08-17
12 <html lang="fr"> 12 <html lang="fr">
13 <head> 13 <head>
14 <meta charset="UTF-8" /> 14 <meta charset="UTF-8" />
15− <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 15+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
16 <meta name="description" content="Job·Ka — toutes les offres d'emploi des employeurs québécois, directement à la source. Salaires transparents, zéro boîte noire. Un service Groupe KA." /> 16 <meta name="description" content="Job·Ka — toutes les offres d'emploi des employeurs québécois, directement à la source. Salaires transparents, zéro boîte noire. Un service Groupe KA." />
17 <title>Job-Ka — Un service Groupe KA</title> 17 <title>Job-Ka — Un service Groupe KA</title>
18 <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 18 <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
modified frontend/public/ka-agent.js +25 −7
@@ -17,18 +17,19 @@
17 17
18 /* ---------- styles ---------- */ 18 /* ---------- styles ---------- */
19 var css = ` 19 var css = `
20− .kaa-btn{position:fixed;right:18px;bottom:18px;z-index:2147483000;width:58px;height:58px; 20+ .kaa-btn{position:fixed;right:18px;bottom:calc(18px + env(safe-area-inset-bottom,0px));z-index:2147483000;width:58px;height:58px;
21 border-radius:50%;border:2px solid var(--ink,#141814);background:var(--accent,#d9f26b); 21 border-radius:50%;border:2px solid var(--ink,#141814);background:var(--accent,#d9f26b);
22 color:var(--on-accent,#141814);font:700 17px/1 "Space Grotesk",system-ui,sans-serif; 22 color:var(--on-accent,#141814);font:700 17px/1 "Space Grotesk",system-ui,sans-serif;
23 cursor:pointer;box-shadow:4px 4px 0 rgba(20,24,20,.85);display:flex;align-items:center; 23 cursor:pointer;box-shadow:4px 4px 0 rgba(20,24,20,.85);display:flex;align-items:center;
24− justify-content:center;transition:transform .15s;padding-bottom:env(safe-area-inset-bottom,0)} 24+ justify-content:center;transition:transform .15s}
25 .kaa-btn:hover{transform:translate(-2px,-2px)} 25 .kaa-btn:hover{transform:translate(-2px,-2px)}
26− .kaa-panel{position:fixed;right:16px;bottom:86px;z-index:2147483001;width:min(392px,calc(100vw - 24px)); 26+ .kaa-panel{position:fixed;right:16px;bottom:calc(86px + env(safe-area-inset-bottom,0px));z-index:2147483001;width:min(392px,calc(100vw - 24px));
27− height:min(580px,calc(100dvh - 110px));display:none;flex-direction:column;background:var(--paper,#f5f3ee); 27+ height:min(580px,calc(100vh - 110px));height:min(580px,calc(100dvh - 110px));display:none;flex-direction:column;background:var(--paper,#f5f3ee);
28 border:2px solid var(--ink,#141814);border-radius:14px;box-shadow:8px 8px 0 rgba(20,24,20,.85); 28 border:2px solid var(--ink,#141814);border-radius:14px;box-shadow:8px 8px 0 rgba(20,24,20,.85);
29 overflow:hidden;font-family:Inter,system-ui,sans-serif} 29 overflow:hidden;font-family:Inter,system-ui,sans-serif}
30 .kaa-panel.kaa-open{display:flex} 30 .kaa-panel.kaa-open{display:flex}
31− .kaa-panel.kaa-full{right:0;bottom:0;width:100vw;height:100dvh;border-radius:0;border:0;box-shadow:none} 31+ .kaa-panel.kaa-full{right:0;bottom:0;width:100vw;height:100vh;height:100dvh;border-radius:0;border:0;box-shadow:none}
32+ html.kaa-lock,html.kaa-lock body{overflow:hidden!important;overscroll-behavior:none}
32 .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814); 33 .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814);
33 color:var(--paper,#f5f3ee);flex:none} 34 color:var(--paper,#f5f3ee);flex:none}
34 .kaa-head b{font:700 15px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.02em} 35 .kaa-head b{font:700 15px "Space Grotesk",system-ui,sans-serif;letter-spacing:-.02em}
@@ -53,7 +54,7 @@
53 border-radius:999px;background:var(--surface-2,#faf9f5)} 54 border-radius:999px;background:var(--surface-2,#faf9f5)}
54 .kaa-in{display:flex;gap:8px;padding:11px;border-top:2px solid var(--ink,#141814);background:#fff;flex:none; 55 .kaa-in{display:flex;gap:8px;padding:11px;border-top:2px solid var(--ink,#141814);background:#fff;flex:none;
55 padding-bottom:calc(11px + env(safe-area-inset-bottom,0))} 56 padding-bottom:calc(11px + env(safe-area-inset-bottom,0))}
56− .kaa-in textarea{flex:1;resize:none;min-height:44px;max-height:110px;padding:11px 12px;font:13.5px/1.4 Inter,system-ui,sans-serif; 57+ .kaa-in textarea{flex:1;resize:none;min-height:44px;max-height:110px;padding:10px 12px;font:16px/1.4 Inter,system-ui,sans-serif;
57 border:1.5px solid var(--ink,#141814);border-radius:9px;background:var(--paper,#f5f3ee);outline:none} 58 border:1.5px solid var(--ink,#141814);border-radius:9px;background:var(--paper,#f5f3ee);outline:none}
58 .kaa-in button{min-width:52px;min-height:44px;border:1.5px solid var(--ink,#141814);border-radius:9px; 59 .kaa-in button{min-width:52px;min-height:44px;border:1.5px solid var(--ink,#141814);border-radius:9px;
59 background:var(--ink,#141814);color:var(--accent,#d9f26b);font:700 16px "Space Grotesk",system-ui;cursor:pointer} 60 background:var(--ink,#141814);color:var(--accent,#d9f26b);font:700 16px "Space Grotesk",system-ui;cursor:pointer}
@@ -142,15 +143,32 @@
142 } 143 }
143 144
144 /* ---------- ouverture / plein écran ---------- */ 145 /* ---------- ouverture / plein écran ---------- */
146+ /* Sur téléphone : panneau plein écran d'office + scroll d'arrière-plan
147+ verrouillé tant qu'il est ouvert (html.kaa-lock). Pas de focus clavier
148+ forcé au tap (le clavier ne s'ouvre que si l'utilisateur touche le champ). */
149+ var mobileMq = window.matchMedia("(max-width: 640px)");
150+ var fineInput = window.matchMedia("(hover: hover) and (pointer: fine)");
151+ function syncLock() {
152+ var open = panel.classList.contains("kaa-open");
153+ if (open && mobileMq.matches) panel.classList.add("kaa-full");
154+ document.documentElement.classList.toggle(
155+ "kaa-lock", open && panel.classList.contains("kaa-full"));
156+ }
145 btn.addEventListener("click", function () { 157 btn.addEventListener("click", function () {
146 panel.classList.toggle("kaa-open"); 158 panel.classList.toggle("kaa-open");
147− if (panel.classList.contains("kaa-open")) { render(); ta.focus(); } 159+ if (panel.classList.contains("kaa-open")) {
160+ render();
161+ if (fineInput.matches) ta.focus();
162+ }
163+ syncLock();
148 }); 164 });
149 panel.querySelector(".kaa-x2").addEventListener("click", function () { 165 panel.querySelector(".kaa-x2").addEventListener("click", function () {
150 panel.classList.remove("kaa-open", "kaa-full"); 166 panel.classList.remove("kaa-open", "kaa-full");
167+ syncLock();
151 }); 168 });
152 panel.querySelector(".kaa-x1").addEventListener("click", function () { 169 panel.querySelector(".kaa-x1").addEventListener("click", function () {
153 panel.classList.toggle("kaa-full"); 170 panel.classList.toggle("kaa-full");
171+ syncLock();
154 scroll(); 172 scroll();
155 }); 173 });
156 174
modified frontend/src/App.tsx +84 −8
@@ -10,7 +10,7 @@
10 * ============================================================================= 10 * =============================================================================
11 */ 11 */
12 import { lazy, Suspense, useEffect, useState } from "react"; 12 import { lazy, Suspense, useEffect, useState } from "react";
13−import { NavLink, Route, Routes } from "react-router-dom"; 13+import { NavLink, Route, Routes, useLocation } from "react-router-dom";
14 import Home from "./pages/Home"; 14 import Home from "./pages/Home";
15 import JobPage from "./pages/Job"; 15 import JobPage from "./pages/Job";
16 import Sources from "./pages/Sources"; 16 import Sources from "./pages/Sources";
@@ -64,26 +64,102 @@ function KaAuth() {
64 ); 64 );
65 } 65 }
66 66
67+/** Liens de navigation principaux (header desktop + menu mobile). */
68+const NAV_LINKS: Array<{ to: string; label: string; end?: boolean }> = [
69+ { to: "/", label: "Offres", end: true },
70+ { to: "/carte", label: "Carte" },
71+ { to: "/sources", label: "Employeurs" },
72+ { to: "/stats", label: "Stats" },
73+ { to: "/employeurs", label: "Publier" },
74+ { to: "/contact", label: "Contact" },
75+];
76+
67 export default function App() { 77 export default function App() {
78+ const [menuOpen, setMenuOpen] = useState(false);
79+ const location = useLocation();
80+
81+ // Hauteur réelle du header → --header-h sur <html> (la carte s'y cale :
82+ // le 58 px codé en dur était faux quand le header wrappe sur 2 lignes).
83+ useEffect(() => {
84+ const el = document.querySelector<HTMLElement>("header.site");
85+ if (!el) return;
86+ const set = () =>
87+ document.documentElement.style.setProperty("--header-h", `${el.offsetHeight}px`);
88+ set();
89+ const ro = new ResizeObserver(set);
90+ ro.observe(el);
91+ return () => ro.disconnect();
92+ }, []);
93+
94+ // Menu mobile : fermeture au changement de route.
95+ useEffect(() => {
96+ setMenuOpen(false);
97+ }, [location.pathname]);
98+
99+ // Menu ouvert : verrou du scroll d'arrière-plan + fermeture à Escape (avec cleanup).
100+ useEffect(() => {
101+ if (!menuOpen) return;
102+ const prev = document.body.style.overflow;
103+ document.body.style.overflow = "hidden";
104+ const onKey = (e: KeyboardEvent) => {
105+ if (e.key === "Escape") setMenuOpen(false);
106+ };
107+ window.addEventListener("keydown", onKey);
108+ return () => {
109+ document.body.style.overflow = prev;
110+ window.removeEventListener("keydown", onKey);
111+ };
112+ }, [menuOpen]);
113+
68 return ( 114 return (
69 <> 115 <>
70− <header className="site"> 116+ <header className={menuOpen ? "site menu-open" : "site"}>
71 <div className="container"> 117 <div className="container">
72 <NavLink to="/" className="logo">Job<em>·</em>Ka</NavLink> 118 <NavLink to="/" className="logo">Job<em>·</em>Ka</NavLink>
73 <GroupeKaBadge dark /> 119 <GroupeKaBadge dark />
74 <nav className="main"> 120 <nav className="main">
75− <NavLink to="/" end className={({ isActive }) => (isActive ? "active" : "")}>Offres</NavLink> 121+ {NAV_LINKS.map((l) => (
76− <NavLink to="/carte" className={({ isActive }) => (isActive ? "active" : "")}>Carte</NavLink> 122+ <NavLink
77− <NavLink to="/sources" className={({ isActive }) => (isActive ? "active" : "")}>Employeurs</NavLink> 123+ key={l.to}
78− <NavLink to="/stats" className={({ isActive }) => (isActive ? "active" : "")}>Stats</NavLink> 124+ to={l.to}
79− <NavLink to="/employeurs" className={({ isActive }) => (isActive ? "active" : "")}>Publier</NavLink> 125+ end={l.end}
80− <NavLink to="/contact" className={({ isActive }) => (isActive ? "active" : "")}>Contact</NavLink> 126+ className={({ isActive }) => (isActive ? "active" : "")}
127+ >
128+ {l.label}
129+ </NavLink>
130+ ))}
81 </nav> 131 </nav>
82 <div className="auth-box"> 132 <div className="auth-box">
83 <KaAuth /> 133 <KaAuth />
84 </div> 134 </div>
135+ <button
136+ type="button"
137+ className="menu-btn"
138+ aria-label={menuOpen ? "Fermer le menu" : "Ouvrir le menu"}
139+ aria-expanded={menuOpen}
140+ onClick={() => setMenuOpen((o) => !o)}
141+ >
142+ <span />
143+ <span />
144+ <span />
145+ </button>
146+ </div>
147+ <div className={menuOpen ? "mobile-menu open" : "mobile-menu"}>
148+ {NAV_LINKS.map((l) => (
149+ <NavLink
150+ key={l.to}
151+ to={l.to}
152+ end={l.end}
153+ className={({ isActive }) => (isActive ? "active" : "")}
154+ onClick={() => setMenuOpen(false)}
155+ tabIndex={menuOpen ? undefined : -1}
156+ >
157+ {l.label}
158+ </NavLink>
159+ ))}
85 </div> 160 </div>
86 </header> 161 </header>
162+ {menuOpen && <div className="mm-backdrop" onClick={() => setMenuOpen(false)} />}
87 <Routes> 163 <Routes>
88 <Route path="/" element={<Home />} /> 164 <Route path="/" element={<Home />} />
89 <Route path="/carte" element={ 165 <Route path="/carte" element={
modified frontend/src/ka/tokens.css +87 −0
@@ -69,7 +69,24 @@
69 69
70 /* Cible tactile minimale */ 70 /* Cible tactile minimale */
71 --touch: 44px; 71 --touch: 44px;
72+
73+ /* ---- Échelle z-index COMMUNE (obligatoire — aucune valeur arbitraire) ----
74+ grain de film body::before = 9999 (pointer-events:none, toujours au-dessus,
75+ purement visuel). Tout composant interactif se place sous 9999 : */
76+ --z-content: 1; /* contenu positionné ordinaire */
77+ --z-sticky: 300; /* éléments sticky de contenu (sous-nav…) */
78+ --z-header: 500; /* header du site */
79+ --z-bottombar: 600; /* tab bar / barre d'action basse */
80+ --z-dropdown: 700; /* menus déroulants (au-dessus des barres) */
81+ --z-overlay: 800; /* voile sous modale/panneau */
82+ --z-modal: 900; /* modales, panneaux de filtres, galeries */
83+ --z-toast: 950; /* notifications */
84+
85+ /* ---- Hauteur de viewport DYNAMIQUE (barre du navigateur mobile qui se
86+ replie au scroll) : toujours var(--vh100), JAMAIS 100vh en dur. ---- */
87+ --vh100: 100vh;
72 } 88 }
89+@supports (height: 100dvh) { :root { --vh100: 100dvh; } }
73 90
74 /* ---- Socle ---- */ 91 /* ---- Socle ---- */
75 * { box-sizing: border-box; } 92 * { box-sizing: border-box; }
@@ -227,3 +244,73 @@ body > * { position: relative; }
227 /* ---- Utilitaires responsive ---- */ 244 /* ---- Utilitaires responsive ---- */
228 .only-mobile { display: initial; } .only-desktop { display: none; } 245 .only-mobile { display: initial; } .only-desktop { display: none; }
229 @media (min-width: 768px) { .only-mobile { display: none; } .only-desktop { display: initial; } } 246 @media (min-width: 768px) { .only-mobile { display: none; } .only-desktop { display: initial; } }
247+
248+/* =============================================================================
249+ SOCLE MOBILE OBLIGATOIRE (2026-08-19) — règles communes aux 13 sites.
250+ RÈGLES pour tout composant futur :
251+ · TAP, jamais :hover, pour ouvrir un menu (le survol n'existe pas au doigt) ;
252+ le :hover ne sert qu'aux effets décoratifs, sous @media (hover: hover).
253+ · var(--vh100) (dvh) et JAMAIS 100vh pour tout élément calé sur le viewport.
254+ · position:fixed exige qu'AUCUN ancêtre n'ait transform/filter/perspective/
255+ will-change/backdrop-filter — sinon monter l'élément à la racine (body).
256+ · Barre basse fixe = .ka-bottombar + classe has-bottombar sur <body>.
257+ · Menu/panneau ouvert = .ka-scroll-lock sur <html> (scroll arrière-plan gelé).
258+ · z-index : uniquement l'échelle --z-* ci-dessus.
259+ · Zones tactiles ≥ var(--touch) (44 px), champs ≥ 16 px (zoom iOS).
260+ ========================================================================== */
261+
262+/* Anti-zoom iOS : au doigt, aucun champ sous 16 px (le focus d'un champ <16 px
263+ déclenche un zoom automatique de page sur Safari iOS). !important assumé :
264+ c'est un filet d'accessibilité, un champ plus petit casse le zoom quoi
265+ qu'il arrive — ne s'applique qu'aux écrans tactiles. */
266+@media (hover: none) and (pointer: coarse) {
267+ input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
268+ select, textarea { font-size: max(16px, 1em) !important; }
269+}
270+
271+/* Zone tactile du badge Groupe KA étendue à ≥44 px au doigt, sans changer
272+ son rendu (débord de la zone cliquable via pseudo-élément). */
273+@media (pointer: coarse) {
274+ .gk-badge { position: relative; }
275+ .gk-badge::after { content: ""; position: absolute; inset: -8px; }
276+}
277+
278+/* Hauteurs viewport dynamiques */
279+.h-viewport { height: 100vh; height: 100dvh; }
280+.min-h-viewport { min-height: 100vh; min-height: 100dvh; }
281+
282+/* Barre fixe basse fiable : safe-area iPhone incluse, immunisée contre les
283+ transforms accidentels, et compensation d'espace via body.has-bottombar. */
284+.ka-bottombar {
285+ position: fixed; left: 0; right: 0; bottom: 0;
286+ z-index: var(--z-bottombar);
287+ padding-bottom: env(safe-area-inset-bottom, 0px);
288+ background: var(--surface);
289+ border-top: 1.5px solid var(--ink);
290+ transform: none !important; filter: none !important;
291+}
292+body.has-bottombar { padding-bottom: calc(var(--bottombar-h, 64px) + env(safe-area-inset-bottom, 0px)); }
293+
294+/* Verrou de scroll d'arrière-plan (menu mobile, modale, panneau de filtres
295+ ouverts) : ajouter/retirer .ka-scroll-lock sur <html>. */
296+html.ka-scroll-lock, html.ka-scroll-lock body { overflow: hidden !important; overscroll-behavior: none; }
297+
298+/* Menu déroulant de référence : au-dessus de tout contenu (cartes Mapbox
299+ incluses), défilement interne avec élan, jamais plus haut que l'écran. */
300+.ka-menu {
301+ position: absolute; z-index: var(--z-dropdown);
302+ min-width: 180px;
303+ max-height: min(60vh, 420px); max-height: min(60dvh, 420px);
304+ overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
305+ background: var(--surface); border: 1.5px solid var(--ink);
306+ border-radius: var(--r-card); box-shadow: var(--shadow-off-mid);
307+}
308+.ka-menu a, .ka-menu button, .ka-menu label, .ka-menu [role="menuitem"], .ka-menu [role="option"] {
309+ display: flex; align-items: center; min-height: var(--touch);
310+ padding: 10px 14px; width: 100%; text-decoration: none;
311+}
312+/* Voile plein écran sous une modale / un panneau */
313+.ka-overlay {
314+ position: fixed; inset: 0; z-index: var(--z-overlay);
315+ background: rgba(20, 24, 20, 0.45);
316+}
modified frontend/src/styles.css +62 −3
@@ -37,13 +37,17 @@ a:hover { text-decoration: underline; }
37 header.site { 37 header.site {
38 background: var(--brand); 38 background: var(--brand);
39 color: #fff; 39 color: #fff;
40− position: sticky; top: 0; z-index: 50; 40+ position: sticky; top: 0; z-index: var(--z-header, 500);
41 } 41 }
42+/* menu mobile ouvert : le header (et son panneau) passe au-dessus du backdrop */
43+header.site.menu-open { z-index: var(--z-modal, 900); }
42 header.site .container { 44 header.site .container {
43 display: flex; align-items: center; flex-wrap: wrap; 45 display: flex; align-items: center; flex-wrap: wrap;
44 gap: 4px 14px; min-height: 58px; padding-top: 8px; padding-bottom: 8px; 46 gap: 4px 14px; min-height: 58px; padding-top: 8px; padding-bottom: 8px;
45 } 47 }
46−.logo { font-family: var(--font-display); font-weight: 700; font-size: 20px; color: #fff; letter-spacing: -0.3px; white-space: nowrap; } 48+.logo { font-family: var(--font-display); font-weight: 700; font-size: 20px; color: #fff; letter-spacing: -0.3px; white-space: nowrap; position: relative; }
49+/* extension tactile : le logo (63×31) atteint >=44px de cible sans changer le rendu */
50+.logo::after { content: ""; position: absolute; inset: -8px; }
47 .logo em { color: var(--accent-soft); font-style: normal; } 51 .logo em { color: var(--accent-soft); font-style: normal; }
48 header.site .gk-badge { flex-shrink: 0; } 52 header.site .gk-badge { flex-shrink: 0; }
49 nav.main { display: flex; gap: 4px; margin-left: auto; } 53 nav.main { display: flex; gap: 4px; margin-left: auto; }
@@ -96,6 +100,51 @@ nav.main a.active { color: var(--accent-soft); background: rgb(255 255 255 / 10%
96 .auth-user span.name { display: none; } 100 .auth-user span.name { display: none; }
97 } 101 }
98 102
103+/* --- menu mobile (hamburger) -------------------------------------------------
104+ Sous 700 px, la nav en flex-wrap (2-3 lignes) est remplacée par un bouton
105+ 44×44 + panneau déroulant sous le header ; backdrop fixe rendu HORS du
106+ header (frère dans App.tsx), verrou de scroll géré côté React. */
107+.menu-btn {
108+ display: none; width: 44px; height: 44px; margin-left: 4px;
109+ flex-direction: column; align-items: center; justify-content: center; gap: 5px;
110+ flex-shrink: 0; background: none; border: 0; border-radius: 8px;
111+ cursor: pointer; padding: 0;
112+}
113+.menu-btn:hover { background: rgb(255 255 255 / 8%); }
114+.menu-btn span {
115+ display: block; width: 22px; height: 2px; background: #f5f3ee; border-radius: 2px;
116+ transition: transform 0.2s ease, opacity 0.15s ease;
117+}
118+.menu-btn[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
119+.menu-btn[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
120+.menu-btn[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
121+.mobile-menu {
122+ display: none; position: absolute; top: 100%; left: 0; right: 0;
123+ background: var(--brand); overflow: hidden;
124+ max-height: 0; opacity: 0;
125+ transition: max-height 0.25s ease, opacity 0.2s ease;
126+}
127+.mobile-menu.open {
128+ max-height: 480px; opacity: 1;
129+ border-bottom: 1.5px solid var(--ink);
130+ padding-bottom: calc(16px + env(safe-area-inset-bottom));
131+}
132+.mobile-menu a {
133+ display: flex; align-items: center; min-height: 44px;
134+ padding: 10px 20px; color: #d7ddda; font-weight: 500;
135+}
136+.mobile-menu a:hover { color: #fff; text-decoration: none; background: rgb(255 255 255 / 8%); }
137+.mobile-menu a.active { color: var(--accent-soft); background: rgb(255 255 255 / 10%); }
138+.mm-backdrop {
139+ position: fixed; inset: 0; background: rgb(16 18 16 / 45%);
140+ z-index: var(--z-overlay, 800);
141+}
142+@media (max-width: 700px) {
143+ nav.main { display: none; }
144+ .menu-btn { display: flex; }
145+ .mobile-menu { display: block; }
146+}
147+
99 /* --- héros ------------------------------------------------------------------ */ 148 /* --- héros ------------------------------------------------------------------ */
100 .hero { background: var(--brand); color: #fff; padding: 40px 0 34px; } 149 .hero { background: var(--brand); color: #fff; padding: 40px 0 34px; }
101 .hero h1 { margin: 0 0 6px; font-size: 30px; font-weight: 700; letter-spacing: -0.5px; color: #fff; } 150 .hero h1 { margin: 0 0 6px; font-size: 30px; font-weight: 700; letter-spacing: -0.5px; color: #fff; }
@@ -115,6 +164,10 @@ nav.main a.active { color: var(--accent-soft); background: rgb(255 255 255 / 10%
115 border: 1px solid var(--border); border-radius: 10px; padding: 0 14px; 164 border: 1px solid var(--border); border-radius: 10px; padding: 0 14px;
116 height: 42px; cursor: pointer; user-select: none; white-space: nowrap; 165 height: 42px; cursor: pointer; user-select: none; white-space: nowrap;
117 } 166 }
167+/* champs >=16px sur mobile : évite le zoom automatique d'iOS au focus */
168+@media (max-width: 700px) {
169+ .filters input[type="search"], .filters select, .filters label.check { font-size: 16px; }
170+}
118 171
119 /* --- cartes d'offres ------------------------------------------------------------ */ 172 /* --- cartes d'offres ------------------------------------------------------------ */
120 .job-list { display: grid; gap: 12px; margin: 22px 0; } 173 .job-list { display: grid; gap: 12px; margin: 22px 0; }
@@ -173,7 +226,13 @@ table.data th { background: var(--surface-2); color: var(--ink-2); font-weight:
173 table.data tr:last-child td { border-bottom: none; } 226 table.data tr:last-child td { border-bottom: none; }
174 227
175 /* --- carte -------------------------------------------------------------------------- */ 228 /* --- carte -------------------------------------------------------------------------- */
176−.map-wrap { height: calc(100vh - 58px); position: relative; } 229+/* hauteur = viewport - hauteur RÉELLE du header (--header-h posée par App.tsx
230+ via ResizeObserver) ; dvh évite le saut quand la barre du navigateur se replie */
231+.map-wrap {
232+ height: calc(100vh - var(--header-h, 58px));
233+ height: calc(100dvh - var(--header-h, 58px));
234+ position: relative;
235+}
177 .map-wrap .maplibregl-map { height: 100%; } 236 .map-wrap .maplibregl-map { height: 100%; }
178 .map-count { 237 .map-count {
179 position: absolute; top: 14px; left: 14px; z-index: 5; 238 position: absolute; top: 14px; left: 14px; z-index: 5;
180 239
Menu

Theme