1 2 3 4<!DOCTYPE html> 5 6 7 8 9 10 11<html lang="fr" data-theme="light"> 12<head> 13 <meta charset="UTF-8"> 14 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 15 <meta name="viewport" content="width=device-width, initial-scale=1"> 16 <title> 17 Politique de confidentialité 18 â L'Or Vert 19</title> 20 21 22 23 24 25 26 27 <meta name="description" content="Politique de confidentialité : données collectées, finalités, durées de conservation, cookies et vos droits RGPD (accès, rectification, effacement)."> 28<meta name="robots" content="index,follow"> 29<link rel="canonical" href="https://lorvert-paris.com/confidentialite"> 30<meta property="og:title" content="Politique de confidentialité"> 31<meta property="og:description" content="Politique de confidentialité : données collectées, finalités, durées de conservation, cookies et vos droits RGPD (accès, rectification, effacement)."> 32<meta property="og:type" content="website"> 33<meta property="og:url" content="https://lorvert-paris.com/confidentialite"> 34<meta name="twitter:card" content="summary_large_image"> 35<meta name="twitter:title" content="Politique de confidentialité"> 36<meta name="twitter:description" content="Politique de confidentialité : données collectées, finalités, durées de conservation, cookies et vos droits RGPD (accès, rectification, effacement)."> 37<meta name="author" content="L'Or Vert"> 38 39 40 41 42
42<script> 43 (function(){ 44 var u=new URLSearchParams(location.search), q=u.get('theme'); 45 var saved=localStorage.getItem('gk-theme'); 46 var siteDefault=null; 47 var prefDark=window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)'); 48 function osTheme(){return (prefDark && prefDark.matches) ? 'dark' : 'light';} 49 // siteDefault wins over saved â brand identity is non-negotiable 50 // for sites that declare a default_theme; user's per-product 51 // preference still applies on sites without a pin. 52 var t=(q==='dark'||q==='light')?q:(siteDefault||saved||osTheme()); 53 document.documentElement.setAttribute('data-theme',t); 54 if (prefDark && prefDark.addEventListener){ 55 prefDark.addEventListener('change',function(){ 56 if (!localStorage.getItem('gk-theme') && !siteDefault){ 57 document.documentElement.setAttribute('data-theme', osTheme()); 58 } 59 }); 60 } 61 var a=u.get('accent'); 62 if(a&&/^#[0-9a-f]{3,8}$/i.test(a)){ 63 var s=document.createElement('style'); 64 s.textContent=':root{--accent:'+a+'!important;--gk-accent:'+a+'!important;--gk-primary:'+a+'!important;--color-accent-blue:'+a+'!important;}'; 65 document.head.appendChild(s); 66 } 67 })(); 68 </script>
68 69 70 71 72 73<meta name="theme-color" content="#1f7a3a" /> 74 75 76 77 78 79<link rel="icon" href="/lorvert-paris-static/img/favicon.png?v=prod-20260926-a" /> 80 81<link rel="manifest" href="/manifest.webmanifest" /> 82 83 84<link rel="apple-touch-icon" href="/lorvert-paris-static/img/apple-touch-icon.png?v=prod-20260926-a" /> 85<meta name="apple-mobile-web-app-capable" content="yes" /> 86<meta name="mobile-web-app-capable" content="yes" /> 87 88 89 90 91<style> 92:root { 93 --role-color-maintainer: #0ea5e9; 94 --role-color-owner: #3b82f6; 95 --role-color-salesrep: #0284c7; 96 --role-color-desk: #16a34a; 97 --role-color-client: #22c55e; 98 --role-color-agent: #f3c84b; 99 --role-color-user: #94a3b8; 100 101} 102 103[data-role="maintainer"], 104.role-pill--maintainer, 105.usr-role.maintainer, 106.gk-member-role--maintainer, 107.feat-role-pill[data-role="maintainer"], 108.pill--role-maintainer { 109 --role-color: #0ea5e9; 110} 111 112[data-role="owner"], 113.role-pill--owner, 114.usr-role.owner, 115.gk-member-role--owner, 116.feat-role-pill[data-role="owner"], 117.pill--role-owner { 118 --role-color: #3b82f6; 119} 120 121[data-role="salesrep"], 122.role-pill--salesrep, 123.usr-role.salesrep, 124.gk-member-role--salesrep, 125.feat-role-pill[data-role="salesrep"], 126.pill--role-salesrep { 127 --role-color: #0284c7; 128} 129 130[data-role="desk"], 131.role-pill--desk, 132.usr-role.desk, 133.gk-member-role--desk, 134.feat-role-pill[data-role="desk"], 135.pill--role-desk { 136 --role-color: #16a34a; 137} 138 139[data-role="client"], 140.role-pill--client, 141.usr-role.client, 142.gk-member-role--client, 143.feat-role-pill[data-role="client"], 144.pill--role-client { 145 --role-color: #22c55e; 146} 147 148[data-role="agent"], 149.role-pill--agent, 150.usr-role.agent, 151.gk-member-role--agent, 152.feat-role-pill[data-role="agent"], 153.pill--role-agent { 154 --role-color: #f3c84b; 155} 156 157[data-role="user"], 158.role-pill--user, 159.usr-role.user, 160.gk-member-role--user, 161.feat-role-pill[data-role="user"], 162.pill--role-user { 163 --role-color: #94a3b8; 164} 165 166</style>
167<script> 168 // Single global â every JS consumer reads from here. 169 // Shape: [{role, color, icon, label, description, assignable, priority}] 170 window.GK_ROLE_CATALOG = [{"assignable": true, "color": "#0ea5e9", "description": "Full admin \u2014 member / config management", "icon": "fa-solid fa-screwdriver-wrench", "label": "Maintainer", "priority": 10, "role": "maintainer", "share": false}, {"assignable": true, "color": "#3b82f6", "description": "Product owner \u2014 top governance", "icon": "fa-solid fa-crown", "label": "Owner", "priority": 20, "role": "owner", "share": false}, {"assignable": true, "color": "#0284c7", "description": "Commercial \u2014 sales representative", "icon": "fa-solid fa-handshake", "label": "Commercial", "priority": 30, "role": "salesrep", "share": false}, {"assignable": true, "color": "#16a34a", "description": "Accueil \u2014 front desk / reception", "icon": "fa-solid fa-desktop", "label": "Accueil", "priority": 40, "role": "desk", "share": false}, {"assignable": true, "color": "#22c55e", "description": "Wholesale buyer", "icon": "fa-solid fa-briefcase", "label": "Client", "priority": 50, "role": "client", "share": true}, {"assignable": true, "color": "#f3c84b", "description": "AI / automation", "icon": "fa-solid fa-robot", "label": "Agent", "priority": 60, "role": "agent", "share": false}, {"assignable": true, "color": "#94a3b8", "description": "Default end user", "icon": "fa-regular fa-user", "label": "User", "priority": 70, "role": "user", "share": false}]; 171 window.GK_ROLE = (function () { 172 var map = {}; 173 window.GK_ROLE_CATALOG.forEach(function (r) { map[r.role] = r; }); 174 return { 175 all: function () { return window.GK_ROLE_CATALOG; }, 176 names: function () { return window.GK_ROLE_CATALOG.filter(function (r) { return r.assignable !== false; }).map(function (r) { return r.role; }); }, 177 color: function (role) { return (map[role] && map[role].color) || '#94a3b8'; }, 178 icon: function (role) { return (map[role] && map[role].icon) || 'fa-regular fa-user'; }, 179 label: function (role) { return (map[role] && map[role].label) || (role || '').charAt(0).toUpperCase() + (role || '').slice(1); }, 180 }; 181 })(); 182</script>
182 183
184<script> 185 (function () { 186 try { 187 // Desktop preference (icon-only mode) is the only state we 188 // restore at first paint. Mobile expand is intentionally NOT 189 // restored â on phones the rail must start collapsed every load 190 // so the content column stays wide. Wipe any stale flag too. 191 // REPLIE PAR DEFAUT (owner 2026-08-19 : « please leave the features 192 // replier by default now »). Le test est `!== '0'` et non `=== '1'` : 193 // absence de preference = replie, seul un depliage EXPLICITE 194 // (le bouton, qui ecrit '0') ouvre la barre. Le meme test vit dans 195 // `_admin_script.html` ; les deux doivent dire la meme chose, sinon 196 // la barre s'ouvre a la premiere peinture puis se replie sous l'oeil. 197 var d = localStorage.getItem('gk-sidebar-collapsed') !== '0'; 198 if (d) document.documentElement.classList.add('feat-sidebar--collapsed'); 199 localStorage.removeItem('gk-sidebar-expanded'); 200 } catch (_) {} 201 })(); 202 // Register the gateka-core service worker for Web Push delivery 203 // (ADR 0007 + 0009). Every product gets push capability via this 204 // single registration â the SW is served by the manifest router on 205 // every fapi (gateka.routers.manifest_routes.service_worker). 206 // Errors are swallowed: push is optional, the page must work even 207 // if SW registration fails (Safari private mode, blocked by CSP, 208 // corporate proxies, etc.). 209 if ('serviceWorker' in navigator) { 210 window.addEventListener('load', function () { 211 navigator.serviceWorker.register('/sw.js', { scope: '/' }) 212 .catch(function (e) { console.warn('SW register failed', e); }); 213 }); 214 } 215</script>
215 216 217 218
219<script> 220 /* Read theme from localStorage before <body> paints â no FOUC. 221 Lookup order: 222 1. localStorage 'gk-theme' (user's explicit click on the theme switch) 223 2. OS preference via prefers-color-scheme (default for first visit) 224 3. 'light' (last-resort fallback when 225 matchMedia isn't available) */ 226 (function () { 227 try { 228 var t = localStorage.getItem('gk-theme'); 229 if (!t) { 230 var mm = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)'); 231 t = (mm && mm.matches) ? 'dark' : 'light'; 232 } 233 document.documentElement.setAttribute('data-theme', t); 234 } catch (e) {} 235 })(); 236</script>
236 237<style> 238 :root { 239 --gk-bg: #f8f9fa; 240 --gk-bg-surface: #ffffff; 241 --gk-text: #1a202c; 242 --gk-text-muted: #6b7280; 243 --gk-border: #e5e7eb; 244 --gk-primary: #2d3748; 245 --gk-primary-hover: #1a202c; 246 --gk-danger: #ef4444; 247 --gk-radius: 10px; 248 --gk-font: system-ui, -apple-system, 'Segoe UI', sans-serif; 249 250 /* Bridge vars consumed by admin_shell.css. Default-aliased to the 251 canonical --gk-* palette; per-product packages.yaml.theme_color 252 overrides --accent via _data_theme.html (included later in 253 gk_base.html). */ 254 --accent: var(--gk-primary); 255 --text: var(--gk-text); 256 --text2: var(--gk-text-muted); 257 --text3: var(--gk-text-muted); 258 --surface: var(--gk-bg-surface); 259 --bg2: var(--gk-bg-surface); 260 --bg3: var(--gk-bg); 261 --border: var(--gk-border); 262 --radius: var(--gk-radius); 263 --overlay-bg: var(--gk-bg-surface); 264 265 /* Legacy `--color-*` vocabulary. ~260 invocations across 266 admin_shell.css and the panel templates name these tokens, and 267 not one of them was ever defined (measured 2026-08-13, zero 268 definitions tree-wide). They survived only as the middle rung of 269 fallback chains â `var(--bg2, var(--color-bg-surface, #fff))` â 270 so every chain skipped straight to its hardcoded literal and the 271 theme looked two levels deep while being one. 39 invocations had 272 no fallback at all, which makes the declaration INVALID and 273 paints nothing (white cards on dark, unpainted hovers). 274 275 Aliased to the bridge vars above rather than to literals: one 276 notion, one source, and dark mode follows with no second block. 277 Aliasing --color-accent-blue to --accent is also what keeps a 278 product's brand colour flowing through â 19 call sites already 279 chained to --accent explicitly, and pinning a literal here would 280 regress every branded product to blue. */ 281 --color-accent-blue: var(--accent, #3b82f6); 282 --color-bg-surface: var(--bg2, #ffffff); 283 --color-bg-elevated: var(--bg3, #f8f9fa); 284 --color-text-primary: var(--text, #111111); 285 --color-text-muted: var(--text3, #888888); 286 --color-border: var(--border, rgba(0, 0, 0, .08)); 287 288 /* These two have no --gk-* counterpart because they are missing 289 notions, not synonyms. --surface-hover is a translucent veil that 290 composes over whatever sits beneath it, so it is the one token 291 that must inverse in dark rather than inherit. --surface-solid is 292 an opaque surface for elements that must not show through 293 (header_auth.css knockout rings, admin_shell.css:282). */ 294 --surface-hover: rgba(0, 0, 0, .05); 295 --surface-solid: var(--bg2, #ffffff); 296 297 /* ââ The non-colour half of the design system (ux-system-epic §D1) ââ 298 Colour was the only notion this block carried. Everything else â 299 spacing, type scale, radius steps, motion â was invented per panel: 300 93 templates, 5221 hardcoded colour values, and ZERO occurrences of 301 `--gk-space` / `--space-` / `--gk-gap` tree-wide when this was 302 written (2026-08-14). `padding: 14px 18px` appears in one panel and 303 `12px 14px` in the next because nothing said which one was right. 304 305 They live HERE, beside the colours, rather than in gateka.css: 306 inline in <head> so they apply before the first paint, present on 307 every gk_base page, and inheriting the html[data-theme] switch for 308 free. gateka.css consumes them with literal fallbacks â it is also 309 served standalone over the CDN to sites that never include this 310 partial. Architect consult 2026-08-14-lot-d1-de-ux-system-epic-571a79 311 (extend_existing). 312 313 â Adding a token here is cheap; adding a SECOND scale is not. If a 314 value you need is one step off the scale, take the nearest step. */ 315 316 /* Spacing â a 4px base, seven steps. Deliberately short: a scale with 317 twelve steps is a scale nobody can hold in their head, and the 318 panels then pick by eye anyway. */ 319 --gk-space-05: 2px; /* the sub-step. Badges and table cells at 11px 320 genuinely need less than 4px of block padding; 321 without a step for it they were writing 2px and 322 3px by eye. One named half-step beats two. */ 323 --gk-space-1: 4px; 324 --gk-space-2: 8px; 325 --gk-space-3: 12px; 326 --gk-space-4: 16px; 327 --gk-space-5: 24px; 328 --gk-space-6: 32px; 329 --gk-space-7: 48px; 330 331 /* Type. --gk-text-base is 13px, not 16px: this is an operational 332 admin surface where density is the point (Stripe Dashboard is the 333 declared reference). Marketing surfaces override in head_extra. */ 334 --gk-text-xs: 11px; 335 --gk-text-sm: 12px; 336 --gk-text-base: 13px; 337 --gk-text-md: 14px; 338 --gk-text-lg: 16px; 339 --gk-text-xl: 18px; 340 /* The DISPLAY steps â a single number a panel shows as its headline, 341 not running text. They stop at 18px until 2026-08-15 because no 342 converted panel had one; §D4 lot 3 converted two that did, and both 343 had reached for a literal. 344 345 Both values are INHERITED, not chosen: 28px is what 346 `.cp-rgpd-card-count` already rendered and 42px is what 347 `.gk-seo-score-big` already rendered. Picking a rounder number 348 would have moved two surfaces for no measured reason â the same 349 reasoning `.gk-card-grid--tight` records for its 140px. */ 350 --gk-text-2xl: 28px; 351 --gk-text-3xl: 42px; 352 --gk-leading-tight: 1.3; 353 --gk-leading: 1.55; 354 --gk-weight-medium: 600; 355 --gk-weight-bold: 700; 356 357 /* Radius â --gk-radius (10px) already existed and stays the middle 358 step, so no existing surface moves. */ 359 --gk-radius-sm: 6px; 360 --gk-radius-lg: 14px; 361 --gk-radius-full: 999px; 362 363 --gk-border-width: 1px; 364 /* The second step exists because §D6 needed a border to carry MEANING 365 rather than separation: `.gk-empty--denied` marks a refusal with a 366 rule on its inline edge, so the state survives greyscale and a 367 `prefers-contrast` palette that flattens the tone. A hairline reads 368 as a divider; this one has to read as a marker. */ 369 --gk-border-width-thick: 2px; 370 /* The THIRD width, and it is a different notion from the two above: 371 a state STRIPE on an element's inline edge, not a rule between 372 things. `.gk-card[data-tone]` has read it as `var(--gk-stripe, 3px)` 373 since §D4 lot 1 and nothing ever declared it, so every caller 374 painted the fallback and a panel that wanted the same stripe had to 375 repeat the literal. Declaring it here costs nothing â a token that 376 equals the fallback every caller already resolved to cannot repaint 377 a screen (§D5 ter) â and it turns the next caller's `3px` into a 378 read. 2026-08-16, §D4. */ 379 --gk-stripe: 3px; 380
381 /* Semantic tones. --gk-danger was already here alone, which is why 382 every panel hardcoded its own green and amber (#16a34a, #fef3c7, 383 #d1fae5 â¦). Surfaces and inks are DERIVED from these four with 384 color-mix() in gateka.css, so a tinted badge composes over whatever 385 is beneath it and follows dark mode with no second declaration â 386 the same reasoning as --surface-hover above. */ 387 --gk-success: #16a34a; 388 --gk-warning: #d97706; 389 --gk-info: #3b82f6; 390 391 /* Motion. Moved here from gateka.css so there is one source; that 392 file now reads them with fallbacks. §C of the epic is binding: 393 nothing above 200ms on an administration gesture, and 394 prefers-reduced-motion is honoured â the reset now lives at the 395 foot of this file, moved out of gateka.css by §D5. Two steps and 396 no more: a duration scale with a long tail is what §D5 removed. */ 397 --gk-ease: cubic-bezier(.4, 0, .2, 1); 398 --gk-ease-out: cubic-bezier(.22, 1, .36, 1); 399 --gk-duration-fast: 120ms; 400 --gk-duration: 180ms; 401 402 /* The stagger STEP â a third axis, not a third duration. §D5 removed a 403 long duration tail and its test still refuses a third --gk-duration-*; 404 this is deliberately not one. It is the offset BETWEEN siblings, and 405 it exists because the alternative is what the language pill shipped on 406 2026-08-15: `calc(30ms + var(--i) * 50ms)`, a rule whose cost grows 407 with the number of children. At three languages that landed the last 408 segment at 250ms â over §C's ceiling, with no number in the rule above 409 200. That is §D5's "a stagger hides its own budget", verbatim. 410 411 A step alone is not enough: a caller MUST also cap it, so the worst 412 case is bounded by the child count it does not control. The pill caps 413 at two steps, which puts its slowest segment at 60 + 120 = 180ms â 414 one --gk-duration for the whole unfold, at two languages as at six. */ 415 --gk-stagger-step: 30ms; 416 417 /* La DURÃE DU RUBAN â un cinquième axe, et le premier qui ne borne 418 aucun geste. `.gk-ribbon` (gateka.css §C) est un ORNEMENT INFINI : 419 il n'annonce aucun changement d'état, donc il ne promet rien qu'une 420 durée puisse trahir. Le plafond de 200 ms de §C lie une CLASSE 421 D'INTERACTION â ce qui répond à un clic, un survol, un focus â pas 422 une classe de surface. 423 424 â IL LUI FAUT SON PROPRE NOM, et ce n'est pas une coquetterie. 425 Le plafond est tenu PAR JETON et non par revue : si le ruban lisait 426 `--gk-duration`, il rentrerait dans l'ensemble plafonné et quelqu'un 427 le ramènerait un jour à 180 ms en toute bonne foi â un ruban à 428 180 ms est un clignotement, et il mourrait sans que personne 429 comprenne pourquoi. Consult 2026-08-16 â¦-45bc25, étape 6. 430 431 40 s pour dix bannières fait ~4 s par bannière traversant l'Åil : 432 assez lent pour ne pas tirer le regard hors du contenu, assez rapide 433 pour que les dix passent pendant qu'on lit la page. */ 434 --gk-ribbon-duration: 40s; 435 436 /* The loading THRESHOLD â a fourth axis, and the only token here that 437 is deliberately ABOVE §C's 200ms ceiling-adjacent numbers without 438 breaking it, because it does not time a gesture: it times the wait 439 BEFORE the screen admits it is waiting. 440 441 Below ~200ms a human reads an appear-then-disappear as a flicker, 442 not as information. An indicator shown for a 40ms request therefore 443 costs more than saying nothing: the screen blinks and the reader 444 learns nothing. So `.gk-empty--loading` holds itself invisible for 445 this long, and the DATA is never delayed â only the admission. 446 447 It is not a --gk-duration-*: nothing moves for 200ms, nothing is 448 animated across it. test_motion_contract.py's refusal of a third 449 duration still stands, and this is not one. */ 450 --gk-loading-delay: 200ms; 451 452 --gk-mono: ui-monospace, SFMono-Regular, 'JetBrains Mono', Menlo, monospace; 453 454 /* Keyboard-first (§D3) needs a ring that is one decision, not one per 455 component. Composed from --accent so a product's brand colour 456 carries into its focus ring. */ 457 --gk-focus-ring: 0 0 0 2px var(--gk-bg-surface, #fff), 458 0 0 0 4px var(--accent, #2d3748); 459 460 --gk-shadow-sm: 0 1px 2px rgba(0, 0, 0, .06); 461 --gk-shadow: 0 4px 12px rgba(0, 0, 0, .10); 462 463 /* The scrim behind a modal surface (§D2's command palette). It obeys 464 the --surface-hover / --gk-shadow rule and not the palette one: it
465 is INK laid over the page, so it stays black in both themes and 466 only its opacity moves. Deriving it from --gk-text with color-mix() 467 would flip it to a WHITE veil in dark mode, which is the one thing 468 a scrim must never do. */ 469 --gk-scrim: rgba(0, 0, 0, .45); 470 471 /* Floating dark chrome, and its ink. The zone editor's pen pill, its 472 context toolbar and its session banner are surfaces laid OVER the 473 document, not part of it â the same category as --gk-scrim, and 474 governed by the same rule: they stay dark in both themes and only 475 the page beneath them changes. Aliasing them to --gk-bg-surface 476 would turn a toolbar that reads as "above the page" into one that 477 reads as part of it, and would make the pill invisible over a dark 478 hero image in light mode. 479 480 --gk-overlay-ink exists so the white is a decision made once, 481 instead of the eleven `#fff` / `#eee` / `rgba(255,255,255,â¦)` 482 literals the drawer carried. Same call gateka.css already makes in 483 .gk-btn--primary: ink on a saturated surface is not a palette 484 entry, it is a contrast requirement. */ 485 --gk-overlay: rgba(26, 26, 46, .94); 486 --gk-overlay-ink: #ffffff; 487 } 488 html[data-theme="dark"] { 489 --gk-bg: #0f141c; 490 --gk-bg-surface: #1a1e27; 491 --gk-text: #f3f6fb; 492 --gk-text-muted: #9ca3af; 493 --gk-border: rgba(128, 128, 128, .3); 494 --gk-primary: #3b82f6; 495 --gk-primary-hover: #2563eb; 496 497 /* The only legacy token needing a dark value: a hover veil made of 498 black ink is invisible on a dark surface, so it inverts to white 499 ink. Every other --color- and --surface- token above aliases a 500 bridge var and inherits this block's --gk-* changes for free. */ 501 --surface-hover: rgba(255, 255, 255, .07); 502 503 /* Same rule as --surface-hover: a shadow is ink composed over what 504 sits beneath it. Black ink on a dark surface is invisible, so the 505 shadow deepens instead of tinting, and the focus ring's knockout 506 has to follow --gk-bg-surface's dark value (it does â it reads the 507 token, not a literal). Spacing, type, radius and motion need no 508 dark values at all: they are not colour. */ 509 --gk-shadow-sm: 0 1px 2px rgba(0, 0, 0, .40); 510 --gk-shadow: 0 4px 14px rgba(0, 0, 0, .55); 511 --gk-scrim: rgba(0, 0, 0, .65); 512 color-scheme: dark; 513 } 514 html[data-theme="dark"] body { 515 background: var(--gk-bg); 516 color: var(--gk-text); 517 } 518</style> 519 520 521<style> 522 @media (prefers-reduced-motion: reduce) { 523 *, *::before, *::after { 524 animation-duration: .01ms !important; 525 animation-iteration-count: 1 !important; 526 transition-duration: .01ms !important; 527 scroll-behavior: auto !important; 528 } 529 } 530</style> 531 532 533 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"> 534 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"> 535 536 537 <link rel="stylesheet" href="/gateka/static/gateka.css?v=prod-20260926-a"> 538 539 540 541 542 543 544 545 546 547<style> 548 :root { 549 --gk-accent: #1f7a3a; 550 --gk-primary: #1f7a3a; 551 --accent: #1f7a3a; 552 --accent-dim: #0f4d24; 553 --accent-hover: #0f4d24; 554 --text: #17231b; 555 --text2: #3b4a40; 556 --text3: #5b6b60; 557 --border: #e3ebe2; 558 --bg2: #ffffff; 559 --bg3: #f3f8f1; 560 --surface: #ffffff; 561 --surface-hover: #eef6ec; 562 --chrome-bg: #ffffff; 563 --chrome-text: #17231b; 564 --chrome-muted: rgba(23,35,27,0.66); 565 --chrome-border: rgba(23,35,27,0.12); 566 --chrome-hover: rgba(31,122,58,0.08); 567 } 568</style> 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587
587<script defer src="/gateka/static/gk.js?v=prod-20260926-a" data-site="lorvert-paris" 588 data-autocapture="0" 589 ></script>
589 590 591</head> 592 593 594<body class="" data-gk-product="Lorvert-Poppies" data-gk-site="lorvert-paris"> 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642
643<script>(function(){try{if(!localStorage.getItem('gk-theme'))document.documentElement.setAttribute('data-theme','light');}catch(e){}})();</script>
643 644 645 646<link rel="stylesheet" href="/gateka/static/gk-nav.css?v=prod-20260926-a"> 647 648<style> 649 /* Tokens â kept in lockstep with header.php :root + html[data-theme="dark"]. */ 650 :root { 651 /* --nav-h was 64px before the search row landed. Search row = 652 ~38px (input 30px + 8px bottom padding), so the topbar is 653 now ~110px tall on desktop. nav_spacer (gk_base.html) and 654 sticky sidebars (catalog #index_app .sidebar) read this var 655 to clear the topbar; under-sizing it clips the top of the 656 page body. */ 657 /* Search row was inlined into nav-row 2026-05-16, freeing ~40px. 658 Topbar now ~75px tall on desktop (nav buttons + upgrade alert). 659 Real height is measured by JS at the bottom of this file and 660 written to --navbar-h, so this fallback only matters before JS 661 runs (FOUC clearance). */ 662 --nav-h: 75px; 663 /* --chrome-bg lets a site override the brand bar without touching 664 --text (which also serves as body ink). Per-site value comes from 665 packages.yaml theme.css_vars (get_theme_tokens, ADR-0036/0045); 666 absent â falls back to the brand navy. Consult 7ce800. */ 667 /* â UN TERMINUS LITTERAL AU BOUT DE LA CHAINE. `--text` n'est emis que 668 par `gk_base.html` : une page AUTONOME qui inclut cette barre sans 669 l'etendre â la vue tuiles du catalogue â n'a donc ni `--chrome-bg` ni 670 `--text`, et `var()` sans repli final rend VIDE. Resultat mesure le 671 2026-08-31 : barre transparente sur lorvert en vue tuiles, navy 672 correcte en vue liste, meme site, meme barre. Le navy de marque ferme 673 la chaine ; les sites qui declarent `--chrome-bg` (sphaigne creme, 674 poppies sombre) n'y arrivent jamais. */ 675 --topbar-bg: var(--chrome-bg, var(--text, #1a202c)); 676 /* L'ENCRE de la barre suit le même contrat que son fond : littérale 677 jusqu'ici, elle empêchait toute barre CLAIRE. Un site au fond crème 678 (sphaigne) héritait bien de `--chrome-bg`, puis écrivait par-dessus en 679 blanc sur blanc. Ces quatre tokens ne changent rien là où 680 `--chrome-*` n'est pas déclaré : les valeurs par défaut sont celles 681 d'avant. Générique, comme `--chrome-bg` â aucune couleur de produit 682 n'entre ici. */ 683 --topbar-text: var(--chrome-text, #f1f5fa); 684 --topbar-muted: var(--chrome-muted, rgba(255,255,255,0.75)); 685 --topbar-border: var(--chrome-border, rgba(255,255,255,0.18)); 686 --topbar-hover-bg: var(--chrome-hover, rgba(255,255,255,0.10)); 687 --topbar-accent: var(--accent); 688 --topbar-on-accent: var(--text); 689 --topbar-shadow: 0 1px 0 rgba(0,0,0,0.20); 690 } 691 html[data-theme="dark"] { 692 /* 2026-05-16: pulled the dark palette toward the brand's navy (var(--text)) 693 instead of the near-black it had before. Topbar gets a slightly 694 deeper navy than body so the chrome still reads as "elevated" 695 above the page (body is #0f1827 across all customer templates). */ 696 --topbar-bg: var(--chrome-bg-dark, #0c1422); 697 --topbar-text: #f3f6fb; 698 --topbar-muted: rgba(255,255,255,0.55); 699 --topbar-border: rgba(255,255,255,0.10); 700 --topbar-hover-bg: rgba(255,255,255,0.06); 701 /* Derive from the per-site accent (persists at :root into dark via 702 _theme.html --accent: var(--gk-primary)) instead of a hardcoded 703 brand-yellow literal â that was a Rule 4 drift baking lorvert's 704 colour into the shared nav. Consult 7ce800. */ 705 --topbar-accent: var(--accent); 706 --topbar-on-accent: #0a0f17; 707 --topbar-shadow: 0 1px 0 rgba(0,0,0,0.50); 708 } 709 /* Lorvert customer-page baseline â restore the body font-size + 710 font-family that lived in the archived header.php (line 88) so 711 untyped text (nav buttons, panel labels, default copy) inherits 712 the same 14px as production. Without this, gk_base falls back 713 to the browser default (16px Times) and per-element px sizes 714 downstream look "smaller" by comparison. */ 715 /* padding-top tracks --navbar-h; since 1a67386 --navbar-h only 716 updates at transitionend (no more per-scroll-frame measure), 717 the recompute on var-change would snap body content up ~60px. 718 Easing padding-top over the same 220ms keeps the whole header 719 squeeze visually coherent on EVERY page (sticky sidebars or not). */ 720 /* â CETTE BARRE PADDE LE BODY ELLE-MEME (regle ci-dessous), donc 721 l'ESPACEUR PARTAGE de gateka-core doit valoir zero, sinon le decalage 722 est COMPTE DEUX FOIS. Mesure du 2026-08-21 sur /backoffice : 723 body padding-top 143px (qui degageait deja exactement le header fixe, 724 bas mesure a 143px) + espaceur 75px = la barre laterale demarrait a 725 234px au lieu de 116px chez les autres produits â ce que l'owner a vu. 726 Sept gabarits lorvert neutralisaient deja le bloc `nav_spacer`, 727 mais `product_admin.html` REDECLARE le bloc et gagne (il est plus 728 derive), donc la neutralisation par bloc ne protege PAS les pages 729 d'administration. On le dit donc en DONNEE, pas en gabarit : le 730 defaut de la variable cote core garde tous les autres produits 731 inchanges (eux n'ont pas de padding sur body â ils ont BESOIN de 732 l'espaceur : sans lui les en-tetes de colonnes kanban passaient sous
733 le header, Guillaume 2026-05-20). */ 734 :root { --gk-nav-spacer-h: 0px; } 735 body { padding-top: var(--navbar-h, 75px); 736 transition: padding-top .22s ease; 737 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-size: 13px; } 738 .topbar { position: fixed; top: 0; left: 0; right: 0; z-index: 1030; background: var(--topbar-bg); color: var(--topbar-text); padding: 0; overflow: visible; border-bottom: 1px solid var(--topbar-border); box-shadow: var(--topbar-shadow); } 739 .topbar-inner { display: flex; align-items: flex-start; gap: 0; padding: 0 12px 0 0; } 740 /* Fixed 200px logo column with a big 200px-tall badge (Guillaume 741 2026-05-23). Height is a STATIC literal â it MUST NOT reference 742 --navbar-h. --navbar-h is set by applyTopOffset to the MEASURED topbar 743 height, which itself includes the logo; binding the logo to it created a 744 feedback loop (logoâtopbarâlogo) that grew the header on every 745 render/resize ("the header keeps growing"). A fixed px keeps the badge 746 large AND stable. max-width + object-fit:contain keep the wide poppies 747 "POPPIES SEEDS" wordmark letterboxed instead of spilling across the 748 header. Shrinks on scroll (see .topbar.is-scrolled below) + on mobile. */ 749 /* â AUCUNE compensation par produit ici. Un `padding-top` calculé sur le 750 déséquilibre de logo7.png (7,4 % de vide en haut, 17,8 % en bas) a été 751 essayé puis RETIRà le 2026-08-18 : cette barre est générique, servie à 752 18 produits, et une constante tirée d'un seul fichier décentrerait les 753 dix-sept autres. Un logo mal cadré se corrige dans l'ASSET, pas dans le 754 gabarit partagé. La barre centre ; elle ne rattrape pas. */ 755 .topbar-logo { flex: 0 0 200px; padding: 0 12px; text-align: center; display: flex; align-items: center; justify-content: center; } 756 /* Single knob: --gk-logo-height. History: 200px badge (2026-05-23, oversized) 757 â 48px â 40px (too small, Guillaume 2026-05-27) â 72px. Fills the fixed 758 200px container better without re-triggering the header-growth loop (still 759 a fixed px, never bound to --navbar-h). One place to dial the resting size. */ 760 :root { --gk-logo-height: 72px; } 761 /* Bigger logo when there's room (wide screens). Square-ish badge stays well 762 under the 200px container/max-width, so no letterboxing. Still fixed px â 763 header-growth loop stays dead. (Guillaume 2026-05-27) */ 764 @media (min-width: 1200px) { :root { --gk-logo-height: 96px; } } 765 .topbar-logo img { height: var(--gk-logo-height, 64px); width: auto; max-width: 200px; object-fit: contain; border-radius: 8px; display: block; } 766 @media (max-width: 640px) { 767 .topbar-logo img { height: 48px; max-width: 160px; } 768 } 769 .topbar-logo a { display: inline-block; line-height: 0; vertical-align: top; } 770 .topbar-content { flex: 1; min-width: 0; padding: 4px 0; } 771 772 /* ââ Header layout model (Guillaume 2026-05-27 rebuild) ââââââââââââââââââ 773 ONE non-wrapping flex row inside .topbar-content (the logo is the fixed
774 200px left zone, outside this row â see .topbar-logo). Three parts: 775 [ nav-links (features) | search (ELASTIC) | nav-actions (login) ] 776 The search is flex:1 â it IS the whitespace. As the viewport narrows the 777 search shrinks first (space closes, no layout change); only once it's out 778 of slack do the breakpoints below compact the buttons (labelsâicons, 779 contactâicon, searchâicon). No justify-content:space-between (that 780 manufactured the voids) and no flex-wrap (that orphaned the actions row). */ 781 .nav-row { display: flex; align-items: center; flex-wrap: nowrap; 782 gap: 12px; min-width: 0; margin-bottom: 12px; } 783 /* `flex: 0 1 auto` + `min-width: 0`, PAS `0 0 auto` : la liste centrale doit 784 CEDER avant les actions. Mesure du 2026-08-31 sur beta.sphaigne-chili.com 785 /catalog en 1280 : avec six entrees de nav, « Se connecter » commencait a 786 x=1251 et « Creer un compte » a x=1382 â hors de l'ecran, et `.nav-row` 787 etant en `overflow-x: visible` le bouton n'etait meme pas atteignable au 788 defilement. Les actions sont deja `flex: 0 0 auto` (l. ~311) : c'est donc 789 ici que la place doit se prendre. La liste defile en interne quand elle 790 deborde. Lorvert/poppies (trois entrees courtes) tiennent toujours dans la 791 ligne, donc `0 1 auto` ne les retrecit jamais â rien ne bouge pour eux. */ 792 .nav-links { display: flex; gap: 4px; list-style: none; margin: 0; padding: 0; 793 align-items: center; flex: 0 1 auto; min-width: 0; 794 overflow-x: auto; scrollbar-width: none; } 795 .nav-links::-webkit-scrollbar { display: none; } 796 /* Mobile: the header stays a single one-line row (Guillaume 2026-05-23, 797 superseding the 2026-05-22 "wrap onto 2-3 lines" rule). The canonical 798 mobile layout lives in the `@media (max-width: 767px)` block below â 799 nowrap row, icon-only, with horizontal scroll as the overflow valve so 800 it never breaks onto multiple lines. */ 801 /* Center nav buttons â Le Monde style (Guillaume 2026-05-18, mirrors 802 gateka-core's .gk-nav-link contract). Icon stacked above a small 803 label, like gateka.fr/features sidebar buttons. At â¥900px switches 804 to row layout with a readable 14px label, matching the canonical 805 wide-viewport pattern. */ 806 .nav-links li a, 807 .nav-links li button.nav-btn-newcmd { 808 position: relative; 809 display: inline-flex; 810 flex-direction: column; 811 align-items: center; justify-content: center; 812 gap: 3px; 813 min-width: 44px; min-height: 36px; 814 padding: 4px 8px; 815 color: var(--topbar-muted); background: transparent; 816 font-size: 10px; font-weight: 600; 817 letter-spacing: .2px; line-height: 1.1; 818 text-align: center; 819 text-decoration: none; 820 border: none; border-radius: 8px; 821 cursor: pointer; font-family: inherit; 822 transition: color .15s, background .15s, filter .15s; 823 } 824 .nav-links li a > .fa-solid, 825 .nav-links li button.nav-btn-newcmd > .fa-solid { font-size: 18px !important; line-height: 1; } 826 @media (min-width: 900px) { 827 .nav-links li a, 828 .nav-links li button.nav-btn-newcmd { 829 flex-direction: row; 830 gap: 8px; 831 padding: 6px 12px; 832 font-size: 14px; 833 min-height: 44px; 834 } 835 .nav-links li a > .fa-solid, 836 .nav-links li button.nav-btn-newcmd > .fa-solid { font-size: 16px !important; } 837 } 838 .nav-links li a:hover, 839 .nav-links li button.nav-btn-newcmd:hover { color: var(--topbar-text); background: var(--topbar-hover-bg); } 840 /* Active state â minimal (Guillaume 2026-05-18). No background, 841 no underline. Just accent text + icon color and slight weight 842 bump. The hover-tint pill (inherited from `.nav-links li a:hover`) 843 is the only background that ever paints; the active page is 844 called out purely by color difference. Smallest possible 845 selection zone. */ 846 .nav-links li.active a { 847 color: var(--topbar-accent); 848 font-weight: 600; 849 } 850 .nav-links li.active a:hover { color: var(--topbar-accent); } 851 .nav-links li.active a > .fa-solid { color: var(--topbar-accent); } 852 .nav-links li button.nav-btn-newcmd { background: var(--topbar-accent); color: var(--topbar-on-accent); font-weight: 600; } 853 .nav-links li button.nav-btn-newcmd:hover { background: var(--topbar-accent); filter: brightness(1.10); } 854 855 .nav-actions { display: flex; gap: 6px; align-items: center; flex-wrap: nowrap; flex: 0 0 auto; } 856 .nav-accordion-btn { display: none; } 857 /* Sans la recherche (sites ou `nav_search` est faux ou reserve aux connectes), 858 la rangee n'a plus d'element elastique : les actions se collaient aux liens 859 et laissaient 305 px de vide a droite (mesure sphaigne, 1500 px, 2026-08-31). 860 `margin-left: auto` reprend ce role â uniquement dans ce cas. */ 861 .nav-row.is-nav-nosearch .nav-actions { margin-left: auto; } 862 863 /* Anon login/register CTAs (shown instead of the account menu when logged 864 out â unify, 2026-05-27). Labels collapse to icons on the narrow strip. */ 865 .nav-cta { display: inline-flex; align-items: center; gap: 6px; padding: 7px 12px; 866 border-radius: 8px; font: inherit; font-size: 13px; font-weight: 600; 867 text-decoration: none; white-space: nowrap; flex: 0 0 auto; cursor: pointer; } 868 .nav-cta--primary { background: var(--topbar-accent); color: var(--topbar-on-accent); } 869 .nav-cta--primary:hover { filter: brightness(1.08); } 870 .nav-cta--ghost { background: transparent; color: var(--topbar-text); border: 1px solid var(--topbar-border); } 871 .nav-cta--ghost:hover { background: var(--topbar-hover-bg); } 872 /* Variante discrete : ni aplat ni contour, le poids retombe a 500 et la 873 couleur d'accent n'apparait qu'au survol. Les deux boutons gardent leur 874 zone de clic (padding inchange) â on baisse le contraste, pas la cible. */ 875 .is-auth-quiet .nav-cta { font-weight: 500; } 876 .is-auth-quiet .nav-cta--primary { background: transparent; color: var(--topbar-text); } 877 .is-auth-quiet .nav-cta--primary:hover { filter: none; } 878 .is-auth-quiet .nav-cta--ghost { border-color: transparent; } 879 .is-auth-quiet .nav-cta:hover { background: var(--topbar-hover-bg); color: var(--topbar-accent); } 880 /* Icones SEULES, a toute largeur â demande de l'owner 2026-08-31. 881 Le libelle tombe (`.nav-text`), le bouton devient une pastille ronde de 882 34px : meme diametre que la pastille de langue et que le couple 883 `.gk-cta-signin`/`.gk-cta-signup` de la barre du noyau, pour que les deux 884 barres se ressemblent. Le `title`/`aria-label` portent deja le texte, 885 donc rien n'est perdu pour un lecteur d'ecran ni au survol. */ 886 .is-auth-quiet .nav-cta .nav-text { display: none; } 887 .is-auth-quiet .nav-cta { 888 width: 34px; height: 34px; padding: 0; gap: 0; 889 border-radius: 999px; justify-content: center; 890 border: 1px solid var(--topbar-border); 891 } 892 .is-auth-quiet .nav-cta i { font-size: 14px; } 893 /* Verrou optique du bloc de marque â voir gk-nav.css. L'icone est centree 894 sur le lockup entier, donc 6px sous le centre du mot-marque ; on la 895 remonte pour qu'elle s'aligne sur la ligne que l'oeil lit. */ 896 .gk-nav-brand > svg { 897 /* â ON ALIGNE L'ENCRE, PAS LA BOITE â et on donne au signe sa taille. 898 Mesure au grossissement x4 : dans une boite de 30px, le dessin n'occupe 899 que 20.3px (haut 5.9px de marge, bas 3.8px), il est donc DECENTRE dans 900 sa propre boite de ~1px vers le bas. Centrer les boites â ce que fait 901 `align-items: center` â laissait donc l'encre a 50.1 quand le bloc de 902 marque est a 49. 903 Et surtout : 20px d'encre contre un bloc de 34px, le signe ne tenait pas 904 son rang a cote de deux lignes de texte ; sa masse (les feuilles) se 905 lisait en haut et sa tige pendait le long de « PAR L'OR VERT ». 906 38px de boite portent ~26px d'encre, et le decalage compense l'asymetrie 907 du trace. Trois iterations a l'oeil avant de mesurer l'encre : c'etait 908 la mesure qui manquait, pas la patience. */ 909 /* MISE A JOUR 2026-09-01 â le signe a change (feuille pleine de Font 910 Awesome, owner). Le `-1.3px` compensait l'asymetrie du BRIN d'avant ; 911 applique au nouveau trace il laissait l'encre 1,3px trop haut et son 912 sommet debordait la barre. Le decalage est desormais porte par le
913 dessin (son `viewBox` centre l'encre dans sa propre boite, cote 914 packages.yaml) : changer de logo ne demande plus de toucher ici. */ 915 position: relative; top: 0; 916 width: 38px; height: 38px; flex-shrink: 0; 917} 918/* top: 0 â le CENTRE, mesure. L'ancre est en `align-items: center`, donc 919 sans decalage l'icone tombe a 34-64 et son milieu a 49, exactement 920 celui du bloc de marque. Les valeurs essayees avant : -6px la faisait 921 SORTIR du bloc par le haut (28 contre 32), -2px la laissait a fleur du 922 bord (haut 32, milieu 47) â alignee sur le mot-marque, mais lue comme 923 collee en haut. L'owner a demande l'alignement vertical : c'est le 924 milieu du bloc, pas celui de la premiere ligne. */ 925/* -2px, pas -6px : a -6 l'icone sortait du bloc de marque par le HAUT 926 (haut de l'icone 28, haut du bloc 32) et se lisait comme decollee de 927 la barre. -2 la garde DANS le bloc tout en la remontant vers le 928 centre optique du mot-marque, entre le centre geometrique du lockup 929 (49) et celui du mot-marque (42.8). Owner 2026-08-31. */ 930 @media (max-width: 719px) { 931 .nav-cta .nav-text { display: none; } 932 .nav-cta { padding: 7px 9px; } 933 } 934 935 /* Service Client PRO contact block â sits in nav-actions so it's 936 always visible (logged-in customer chrome). Two stacked lines 937 (phone + email) keep the height in line with the connect-pill; 938 gold email + neutral phone match the legacy header.php styling. */ 939 .gk-support-pill { 940 display: inline-flex; flex-direction: column; gap: 1px; 941 padding: 4px 10px; border: 1px solid var(--topbar-border); 942 border-radius: 6px; background: rgba(0,0,0,0.15); 943 line-height: 1.15; font-size: 11px; 944 } 945 .gk-support-pill .gk-support-label { 946 font-size: 9px; font-weight: 700; letter-spacing: 0.06em; 947 text-transform: uppercase; color: var(--topbar-muted); 948 } 949 .gk-support-pill .gk-support-lines { display: flex; flex-direction: column; gap: 1px; } 950 .gk-support-pill a { text-decoration: none; display: inline-flex; align-items: center; gap: 5px; font-variant-numeric: tabular-nums; } 951 .gk-support-pill .gk-support-phone { color: var(--topbar-text); font-weight: 600; } 952 .gk-support-pill .gk-support-phone:hover { color: var(--topbar-accent); } 953 .gk-support-pill .gk-support-mail { color: var(--topbar-accent); font-weight: 500; } 954 .gk-support-pill .gk-support-mail:hover { filter: brightness(1.15); } 955 .gk-support-pill i { font-size: 9px; opacity: 0.8; } 956 /* Below 900px the email is the chattiest line â drop it; phone stays. */ 957 @media (max-width: 900px) { 958 .gk-support-pill .gk-support-mail-text { display: none; } 959 .gk-support-pill .gk-support-mail i { font-size: 11px; } 960 } 961 /* Below 600px collapse to icon-only phone link (one icon, taps to dial). */ 962 @media (max-width: 600px) { 963 .gk-support-pill { padding: 6px 8px; } 964 .gk-support-pill .gk-support-label, 965 .gk-support-pill .gk-support-phone-text, 966 .gk-support-pill .gk-support-mail-text { display: none; } 967 .gk-support-pill i { font-size: 13px; opacity: 1; } 968 } 969 .nav-btn-ghost { padding: 6px 10px; font-size: 13px; background: transparent; border: 1px solid var(--topbar-border); border-radius: 6px; color: var(--topbar-muted); cursor: pointer; display: inline-flex; align-items: center; text-decoration: none; transition: color .15s, border-color .15s, background .15s; } 970 .nav-btn-ghost:hover { color: var(--topbar-text); border-color: var(--topbar-text); background: var(--topbar-hover-bg); } 971 #admin_btn { padding: 6px 8px; border: none; background: transparent; } 972 #admin_btn:hover { background: transparent; border-color: transparent; } 973 /* Canonical Features & bricks trigger (_features_menu.html) â the 974 upstream .gk-nav-btn class isn't in Lorvert's fork so the button 975 was rendering as a browser-default 17x19 grey rectangle around a 976 tiny gear. Style the fm-trigger to match the retired #admin_btn 977 look: no chrome, 18px gear, hover-spin animation. Scoped so it 978 doesn't fight #admin_btn if that ever comes back. */ 979 [data-gk-feat-menu] .gk-nav-btn.fm-trigger { 980 padding: 6px 8px; border: none; background: transparent; cursor: pointer; 981 color: var(--topbar-muted); display: inline-flex; align-items: center; 982 justify-content: center; line-height: 1; 983 } 984 [data-gk-feat-menu] .gk-nav-btn.fm-trigger:hover { background: transparent; } 985 [data-gk-feat-menu] .fm-trigger .fa-gear { transition: color .15s; font-size: 18px !important; } 986 [data-gk-feat-menu] .fm-trigger:hover .fa-gear, 987 [data-gk-feat-menu].is-open .fm-trigger .fa-gear { 988 color: var(--topbar-text); 989 animation: lorv-crank-spin 2s linear infinite; 990 } 991 /* Crank â continuous spin while hovered, same shape as the canonical 992 gateka-core gk-nav-btn .fa-gear contract (gk-nav.css). Was a single 993 45° tick before; aligned 2026-05-17 so Lorvert-Poppies matches 994 every other Gateka site's chrome. */ 995 #admin_btn .fa-gear { transition: color .15s; font-size: 18px !important; } 996 #admin_btn:hover .fa-gear { 997 color: var(--topbar-text); 998 animation: lorv-crank-spin 2s linear infinite; 999 } 1000 @keyframes lorv-crank-spin { 1001 from { transform: rotate(0deg); } 1002 to { transform: rotate(360deg); } 1003 } 1004 1005 /* Connect pill â avatar + stacked firstname hint */ 1006 .connect-pill { position: relative; width: 30px; height: 30px; padding: 0; border-radius: 50%; background: transparent; border: none; color: var(--topbar-text); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; transition: filter .15s; } 1007 .connect-pill:hover { filter: brightness(1.10); } 1008 .connect-pill-avatar { width: 26px;
1008 height: 26px; border-radius: 50%; background: var(--topbar-accent); color: var(--topbar-on-accent); font-weight: 700; font-size: 11px; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; } 1009 /* â L'image REMPLIT la pastille sans la deformer : `cover` recadre, il ne 1010 comprime pas. Sans `overflow:hidden` le carre deborderait du cercle. */ 1011 .connect-pill-avatar, .connect-pill-head-avatar { overflow: hidden; } 1012 .connect-pill-avatar img, .connect-pill-head-avatar img { 1013 width: 100%; height: 100%; object-fit: cover; display: block; } 1014 /* Firstname hint under the profile pill â hidden per Guillaume 1015 2026-05-18. Avatar initials inside the pill already identify the 1016 user; the "GUYDEGN..." truncated label was visual noise. The full 1017 name + email still surface in the dropdown header on click. */ 1018 .connect-pill-name { display: none; } 1019 .connect-pill-menu { display: none; position: absolute; top: calc(100% + 6px); right: 0; background: #fff; color: #1f2937; border: 1px solid #e5e7eb; border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,.18); min-width: 240px; z-index: 2000; overflow: hidden; } 1020 #connect_pill_wrap.is-open .connect-pill-menu { display: block; } 1021 .connect-pill-head { display: flex; align-items: center; gap: 10px; padding: 14px; background: #f8fafc; border-bottom: 1px solid #e5e7eb; } 1022 .connect-pill-head-avatar { width: 36px; height: 36px; border-radius: 50%; background: var(--topbar-accent); color: var(--topbar-on-accent); font-weight: 700; font-size: 13px; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; } 1023 /* â La vignette du menu passe de 36 a 52 px (owner 2026-08-30). Le menu 1024 est l'endroit ou l'on VERIFIE son identite avant d'agir â une vignette 1025 de 36 px n'y sert a rien. La pastille de la barre reste petite : elle, 1026 doit tenir dans la hauteur de nav. */ 1027 .connect-pill-head-avatar { width: 52px !important; height: 52px !important; 1028 flex: 0 0 52px; } 1029 .connect-pill-head-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px; } 1030 .cph-tag { font-size: .68rem; font-weight: 600; letter-spacing: .03em; 1031 padding: 2px 8px; border-radius: 999px; font-variant-numeric: tabular-nums; 1032 background: var(--gk-accent, #f5c518); color: #3a2c00; } 1033 .cph-tag--soft { background: rgba(0,0,0,.07); color: inherit; opacity: .75; } 1034 html[data-theme="dark"] .cph-tag--soft { background: rgba(255,255,255,.12); } 1035 .connect-pill-head-text { min-width: 0; flex: 1; } 1036 .connect-pill-head-name { font-weight: 600; font-size: 13px; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 1037 .connect-pill-head-email { font-size: 11px; color: #6b7280; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 1038 .connect-pill-item { display: flex; align-items: center; gap: 10px; padding: 9px 14px; color: #374151; text-decoration: none; font-size: 13px; cursor: pointer; transition: background .12s; border: none; background: none; width: 100%; text-align: left; font-family: inherit; } 1039 .connect-pill-item:hover { background: #f9fafb; color: #111; } 1040 .connect-pill-item--active { background: #f0f9ff; color: #0ea5e9; font-weight: 600; } 1041 .connect-pill-item--active:hover { background: #e0f2fe; color: #0284c7; } 1042 .connect-pill-item--danger { color: #dc2626; } 1043 .connect-pill-item--danger:hover { background: #fef2f2; color: #b91c1c; } 1044 .connect-pill-sep { height: 1px; background: #e5e7eb; margin: 4px 0; } 1045 1046 /* B2C-mode toggle (replaces the old theme toggle 2026-06-04). Off: 1047 subtle outline like the lang toggle. On: filled amber so the operator 1048 can't miss it AND the body gets `lorvert-b2c-mode` so the whole 1049 topbar tints warm â visible confirmation that current display is 1050 public retail prices, not the operator's B2B view. */ 1051 .gk-b2c-toggle { position: relative; display: inline-flex; align-items: center; gap: 5px; height: 30px; padding: 0 10px; background: transparent; border: 1px solid var(--topbar-border); border-radius: 6px; cursor: pointer; color: var(--topbar-muted); transition: background .15s, border-color .15s, color .15s; font: 600 11px 'Inter','Helvetica Neue',sans-serif; } 1052 .gk-b2c-toggle:hover { background: var(--topbar-hover-bg); border-color: var(--topbar-text); color: var(--topbar-text); } 1053 .gk-b2c-toggle .fa-solid { font-size: 12px; } 1054 .gk-b2c-toggle .gk-b2c-label { letter-spacing: .04em; } 1055 /* Active state â orange amber, applies to button + body wrapper. */ 1056 body.lorvert-b2c-mode .gk-b2c-toggle { background: #f59e0b; border-color: #b45309; color: #1f2937; } 1057 body.lorvert-b2c-mode .gk-b2c-toggle:hover { background: #d97706; color: #fff; } 1058 body.lorvert-b2c-mode .topbar { box-shadow: inset 0 -2px 0 #f59e0b; } 1059 body.lorvert-b2c-mode .topbar::before { content: ''; position: absolute; inset: 0; pointer-events: none; background: linear-gradient(to right, rgba(245,158,11,0.06), rgba(245,158,11,0) 30%, rgba(245,158,11,0) 70%, rgba(245,158,11,0.06)); } 1060 /* B2C simulation banner â full-width strip, only visible when the 1061 body has `lorvert-b2c-mode`. Loud amber + diagonal stripe so the 1062 operator literally cannot miss they're in client-facing mode. */ 1063 .lorvert-b2c-banner { display: none; align-items: center; gap: 12px; 1064 padding: 8px 16px; font: 600 13px 'Inter','Helvetica Neue',sans-serif; 1065 color: #1f2937; background: repeating-linear-gradient(135deg, #f59e0b 0 24px, #fbbf24 24px 48px); 1066 border-bottom: 2px solid #b45309; position: sticky; top: 0; z-index: 990; } 1067 body.lorvert-b2c-mode .lorvert-b2c-banner { display: flex; } 1068 .lorvert-b2c-banner-tag { flex: 0 0 auto; background: #1f2937; color: #fbbf24; 1069 padding: 3px 8px; border-radius: 4px; font-size: 11px; letter-spacing: .06em; font-weight: 800; } 1070 .lorvert-b2c-banner-text { flex: 1 1 auto; } 1071 .lorvert-b2c-banner-text strong { color: #7c2d12; } 1072 .lorvert-b2c-banner-exit { flex: 0 0 auto; cursor: pointer; background: #1f2937; color: #fbbf24; 1073 border: 0; padding: 4px 10px; border-radius: 4px; font: inherit; font-size: 11px; 1074 text-transform: uppercase; letter-spacing: .04em; } 1075 .lorvert-b2c-banner-exit:hover { background: #111827; } 1076 @media (max-width: 560px) { 1077 .lorvert-b2c-banner { font-size: 11px; padding: 6px 10px; gap: 6px; } 1078 .lorvert-b2c-banner-tag { font-size: 9.5px; padding: 2px 5px; } 1079 } 1080 /* Subtle page background tint while in B2C mode â secondary signal 1081 for when the banner scrolls off the viewport. */ 1082 body.lorvert-b2c-mode { background: linear-gradient(180deg, rgba(245,158,11,0.04) 0 600px, transparent 600px); } 1083 1084 /* Le selecteur de langue N'A PLUS DE REGLES ICI â 2026-08-16.
1085 `.nav-lang-toggle` / `.nav-lang-opt` / `.nav-lang-slider` etaient la 1086 quatrieme declaration de la meme notion dans la flotte. La pilule 1087 partagee (`_lang_switch.html`) porte les siennes dans `gk-nav.css`, 1088 liee en tete de ce fichier. Ne les reintroduis pas : une regle locale 1089 posee apres ce <link> gagnerait en silence et la pilule divergerait a 1090 nouveau, ce qui est precisement ce que la migration retire. 1091 1092 Ce qui reste ci-dessous, c'est `.rg-lang-*` dans `landing.html` â le 1093 champ de langue du FORMULAIRE d'inscription, sur fond clair, qui 1094 soumet une valeur. Un champ de formulaire, pas un commutateur de 1095 chrome : meme mot, autre notion. */ 1096 1097 /* Connect-pill dropdown adapts to dark mode (mirrors theme_toggle.php). */ 1098 html[data-theme="dark"] .connect-pill-menu { background: #1a1e27; color: #f3f6fb; border-color: rgba(255,255,255,0.10); box-shadow: 0 8px 24px rgba(0,0,0,.5); } 1099 html[data-theme="dark"] .connect-pill-head { background: rgba(255,255,255,0.04); border-bottom-color: rgba(255,255,255,0.10); } 1100 html[data-theme="dark"] .connect-pill-head-name { color: #f3f6fb; } 1101 html[data-theme="dark"] .connect-pill-head-email { color: rgba(255,255,255,0.55); } 1102 html[data-theme="dark"] .connect-pill-item { color: #f3f6fb; } 1103 html[data-theme="dark"] .connect-pill-item:hover { background: rgba(255,255,255,0.06); color: #fff; } 1104 html[data-theme="dark"] .connect-pill-item--active { background: rgba(59,130,246,0.15); color: #93c5fd; } 1105 html[data-theme="dark"] .connect-pill-item--active:hover { background: rgba(59,130,246,0.25); color: #bfdbfe; } 1106 html[data-theme="dark"] .connect-pill-item--danger { color: #f87171; } 1107 html[data-theme="dark"] .connect-pill-item--danger:hover { background: rgba(220,38,38,0.15); color: #fca5a5; } 1108 html[data-theme="dark"] .connect-pill-sep { background: rgba(255,255,255,0.10); } 1109 1110 /* Upgrade-in-progress alert â ported from legacy header.php so customers 1111 see the same notice across the migration. Gold-tinted, sits below the 1112 nav-row inside the topbar. */ 1113 /* Data-driven site notice (get_site_notice â packages.yaml.notices). 1114 `.upgrade-alert` is the base; `--<type>` sets the palette. `info` 1115 keeps the original gold tint; alert=red, warning=amber, success= 1116 green. Default (no modifier) = info. */ 1117 .upgrade-alert { margin: 2px 0 6px 0; padding: 6px 10px; border-radius: 6px; 1118 font-size: 12px; text-align: center; 1119 max-height: 40px; overflow: hidden; 1120 background: color-mix(in srgb, var(--topbar-accent) 15%, transparent); 1121 border: 1px solid color-mix(in srgb, var(--topbar-accent) 40%, transparent); 1122 color: var(--topbar-accent); 1123 /* max-height transition only animates visibly while the 1124 ceiling is BELOW the natural content height â keep the 1125 ceiling tight (40px â content) so frame-1 of the collapse 1126 is immediately visible. 80px used to leave the first 1127 ~60% of the 220ms doing nothing. */ 1128 transition: max-height .22s ease, padding .22s ease, 1129 margin .22s ease, opacity .22s ease, 1130 border-width .22s ease; } 1131 .upgrade-alert i { margin-right: 4px; } 1132 .upgrade-alert-cta { display: inline-flex; align-items: center; gap: 4px; margin-left: 8px; font-weight: 700; text-decoration: underline; color: inherit; white-space: nowrap; } 1133 .upgrade-alert-cta:hover { filter: brightness(1.15); } 1134 .upgrade-alert-cta i { margin: 0; font-size: 10px; } 1135 .upgrade-alert--alert { 1136 background: rgba(220,38,38,0.14); border-color: rgba(220,38,38,0.45); color: #fca5a5; 1137 } 1138 .upgrade-alert--warning { 1139 background: rgba(245,158,11,0.14); border-color: rgba(245,158,11,0.45); color: #fcd34d; 1140 } 1141 .upgrade-alert--success { 1142 background: rgba(34,197,94,0.14); border-color: rgba(34,197,94,0.45); color: #86efac; 1143 } 1144 /* Squeeze mode (Guillaume 2026-05-18) â collapse the upgrade notice
1145 to zero height on scroll. Visitors land on the page, see it once, 1146 then it folds away as they scroll into the catalog so the screen 1147 fills with product content. Tied to the same .topbar.is-scrolled 1148 state that shrinks the logo (see the shrink-on-scroll block below). */ 1149 .topbar.is-scrolled .upgrade-alert { 1150 max-height: 0; 1151 padding-top: 0; padding-bottom: 0; 1152 margin: 0; 1153 opacity: 0; 1154 border-width: 0; 1155 } 1156 1157 @media (max-width: 767px) { 1158 .upgrade-alert { display: none; } 1159 .topbar-inner { flex-wrap: nowrap; align-items: center; padding: 4px 8px; gap: 8px; } 1160 /* `flex: 0 0 auto` = « ne retrecis JAMAIS ». C'est la colonne du logo qui 1161 figeait la rangee : bord droit mesure a 405 en 390 px, 405 en 360 et 1162 405 en 320 â identique, donc la rangee ignorait l'ecran et tout le 1163 manque sortait par la droite. `0 1 auto` + `min-width: 0` (sans lui un 1164 enfant flex ne descend jamais sous la largeur de son contenu) la rend 1165 compressible ; elle cede la premiere parce qu'elle est la plus large et 1166 la seule qu'on puisse tronquer sans perdre une fonction. */ 1167 .topbar-logo { flex: 0 1 auto; min-width: 0; overflow: hidden; padding: 2px 4px; 1168 /* `0 1 auto` seul l'a fait passer de 210 a 171 px en 320 â puis il s'est 1169 arrete, et la pastille de langue sortait encore de 33 px. Un plafond 1170 RELATIF a l'ecran le force a continuer de ceder tant qu'il le faut, 1171 a toutes les largeurs, au lieu d'un seuil fixe a re-mesurer au 1172 prochain telephone. */ 1173 max-width: 46vw; } 1174 /* Reduction mode: shrink the badge to ~line height so it sits inline with 1175 the icon row instead of dominating the strip (Guillaume 2026-05-27). */ 1176 .topbar-logo { --gk-logo-height: 24px; } 1177 .topbar-logo img { max-width: 90px; } 1178 .topbar.is-scrolled .topbar-logo { --gk-logo-height: 22px; } 1179 .topbar-content { flex: 1 1 auto; min-width: 0; padding: 0; } 1180 .nav-collapsible { min-width: 0; } 1181 /* ââ Single one-line header (Guillaume 2026-05-23) ââ 1182 Everything on ONE row. nowrap is forced (!important) to beat the base 1183 `.nav-row { flex-wrap: wrap }` + the old per-element wrap rules. If the 1184 cluster is wider than the viewport on a very narrow phone it SCROLLS 1185 horizontally rather than wrapping â the scrollbar is hidden so it reads 1186 as a tidy single bar that the thumb can nudge. */ 1187 .nav-row { 1188 flex-wrap: nowrap !important; 1189 justify-content: flex-start; /* pack icons left â no mid-strip void */ 1190 margin: 0; gap: 6px; 1191 min-width: 0; 1192 /* Le defilement descend sur `.nav-links` (regle juste dessous) au lieu 1193 de porter sur toute la rangee. La rangee reste UNE ligne, decision 1194 de Guillaume du 2026-05-23 inchangee ; ce qui change est QUI defile. 1195 Avec la rangee entiere en `overflow-x: auto`, les six entrees de 1196 sphaigne repoussaient « Se connecter »/« Creer un compte » a x=535 et 1197 x=570 dans un ecran de 390 : presents, mais atteignables seulement 1198 par un glissement lateral sans affordance. Les actions restent 1199 maintenant visibles, et seules les entrees de nav defilent. */ 1200 -webkit-overflow-scrolling: touch; 1201 } 1202 .nav-row::-webkit-scrollbar { display: none; } /* WebKit/Blink */ 1203 /* ââ Menu accordeon, rangee chargee (>=5 entrees) ââââââââââââââââ 1204 Le defilement horizontal des entrees (regle ci-dessous) supposait 1205 que le pouce trouve une bande qui ne dit pas qu'elle defile. Mesure 1206 du 2026-08-31 sur sphaigne en 390 px : des six entrees, une seule 1207 etait visible, coupee en deux par la pastille de connexion â les 1208 cinq autres n'existaient pas pour le visiteur. Un ticket proposait 1209 « mettre un scroll horizontal » : c'est ce qui etait deja en place 1210 et qui echouait. 1211 On replie donc les entrees derriere un bouton, en case a cocher 1212 PURE CSS â meme mecanique que la nav du noyau (gk-nav.css, « pure 1213 CSS, no JS ») : la case precede `.nav-links`, `:checked ~` ouvre le 1214 panneau. Naviguer recharge la page, donc la case se decoche seule. 1215 Uniquement sur `.is-nav-dense` : lorvert et poppies n'ont pas 1216 d'entree publique, leur barre n'a rien a replier. */ 1217 .is-nav-dense .nav-accordion-btn { 1218 display: inline-flex; align-items: center; justify-content: center; 1219 flex: 0 0 auto; width: 38px; height: 34px; 1220 border: 1px solid var(--topbar-border); border-radius: 8px; 1221 background: rgba(0,0,0,0.15); color: var(--topbar-text); 1222 font-size: 16px; cursor: pointer; 1223 } 1224 .is-nav-dense .nav-links { display: none; } 1225 .is-nav-dense .nav-accordion-toggle:checked ~ .nav-links { 1226 display: flex; flex-direction: column; align-items: stretch; 1227 position: absolute; top: 100%; left: 0; right: 0; 1228 width: auto !important; max-width: none; 1229 max-height: calc(100vh - 120px); overflow-y: auto; overflow-x: hidden; 1230 background: var(--topbar-bg); color: var(--topbar-text); 1231 border-bottom: 1px solid var(--topbar-border); 1232 box-shadow: 0 10px 26px rgba(0,0,0,.22); 1233 padding: 8px; gap: 4px; z-index: 1031; 1234 } 1235 .is-nav-dense .nav-accordion-toggle:checked ~ .nav-links li { width: 100%; } 1236 .is-nav-dense .nav-accordion-toggle:checked ~ .nav-links li a,
1237 .is-nav-dense .nav-accordion-toggle:checked ~ .nav-links li button.nav-btn-newcmd { 1238 display: flex; flex-direction: row; align-items: center; 1239 justify-content: flex-start; text-align: left; 1240 box-sizing: border-box; 1241 gap: 14px; width: 100%; min-height: 52px; 1242 padding: 12px 14px; border-radius: 8px; font-size: 15px; 1243 } 1244 .is-nav-dense .nav-accordion-toggle:checked ~ .nav-links li a .nav-text, 1245 .is-nav-dense .nav-accordion-toggle:checked ~ .nav-links li button.nav-btn-newcmd .nav-text { 1246 display: inline; 1247 } 1248 .is-nav-dense .nav-accordion-toggle:checked ~ .nav-links li a > .fa-solid, 1249 .is-nav-dense .nav-accordion-toggle:checked ~ .nav-links li button.nav-btn-newcmd > .fa-solid { 1250 font-size: 18px !important; min-width: 26px; text-align: center; 1251 } 1252 /* Center nav must not hog width (the old width:100% pushed search + 1253 actions onto new lines). Size to content, never wrap internally. */ 1254 .nav-links { 1255 flex: 0 1 auto; 1256 min-width: 0; 1257 width: auto !important; 1258 flex-wrap: nowrap !important; 1259 flex-direction: row; 1260 gap: 2px; 1261 overflow-x: auto; 1262 -webkit-overflow-scrolling: touch; 1263 scrollbar-width: none; 1264 } 1265 .nav-links::-webkit-scrollbar { display: none; } 1266 .nav-links li a, 1267 .nav-links li button.nav-btn-newcmd { padding: 5px 6px; min-width: 36px; min-height: 34px; font-size: 18px; } 1268 .nav-links li a > .fa-solid, 1269 .nav-links li button.nav-btn-newcmd > .fa-solid { font-size: 20px !important; } 1270 .nav-links li a .nav-text, 1271 .nav-links li button.nav-btn-newcmd .nav-text { display: none; } 1272 /* Actions sit right after the search â no margin-left:auto, so there's 1273 no gap between the search and the controls (Guillaume 2026-05-27). */ 1274 .nav-actions { flex: 0 0 auto; flex-wrap: nowrap !important; gap: 4px; margin: 0; } 1275 .gk-search-trigger { flex: 0 0 auto; margin-right: 0; } 1276 /* Contact info (Service Client PRO) is hidden on the mobile strip â 1277 it's on the landing + footer, and the phone is a tap-to-call link 1278 there. Frees the row (Guillaume 2026-05-27). */ 1279 .gk-support-pill { display: none; } 1280 .connect-pill-name { display: none; } 1281 /* ââ Les 40 px qui manquent au telephone ââââââââââââââââââââââââââââ 1282 Mesure du 2026-09-01 en 390 px sur beta.sphaigne-chili.com/catalog : 1283 le bord droit de la pastille de langue tombe a 411 â elle est COUPEE. 1284 Connecte c'est pire : la capture de l'operateur montre l'engrenage et 1285 la pastille de compte tranches au bord droit, parce que la rangee 1286 porte en plus la recherche, le compte et la bascule B2C. 1287 1288 On COMPACTE, on ne supprime pas : `gk-b2c-btn` est une BASCULE (elle 1289 change l'affichage des prix), pas un badge â la cacher retirerait une 1290 fonction. Le pictogramme suffit a l'identifier, comme le bloc contact 1291 plus haut se replie deja sur son telephone. */ 1292 .gk-b2c-label { display: none; } 1293 .gk-langpill-globe { display: none; } 1294 .nav-actions { gap: 3px; } 1295 /* Mesure APRES ces deux compactages : le bord droit tombait de 411 a 409 1296 pour un ecran de 390 â le globe et l'etiquette B2C ne suffisaient pas 1297 (la bascule B2C est masquee pour un anonyme, elle ne rendait rien). 1298 Les deux pastilles d'authentification portent 7px de padding vertical 1299 et 9 lateral ; les resserrer rend les ~20 px qui manquent, sans rien 1300 retirer de l'ecran. */ 1301 .nav-cta { padding: 6px 7px; } 1302 .nav-row { gap: 4px; } 1303 .topbar-inner { padding-left: 6px; padding-right: 6px; } 1304 /* â LA VRAIE CAUSE. Mesure du 2026-09-01 : le bord droit valait 405 en 1305 390 px, 405 en 360 ET 405 en 320 â IDENTIQUE. Une rangee qui ne bouge 1306 pas quand l'ecran retrecit ne « deborde » pas un peu : elle ignore 1307 l'ecran. Rien dedans ne pouvait ceder, donc chaque pixel manquant 1308 sortait par la droite, et compacter les pastilles une a une ne rendait 1309 que quelques pixels a chaque fois. 1310 On rend la marque COMPRESSIBLE : `flex: 0 1 auto` + `min-width: 0` 1311 (sans lui, un enfant flex ne descend jamais sous son contenu) et une 1312 coupure a l'ellipse. Elle cede la premiere parce qu'elle est la plus 1313 large et la seule qu'on puisse tronquer sans perdre une fonction : un 1314 nom de site raccourci reste lisible, un bouton hors ecran ne se clique 1315 pas. */ 1316 .gk-nav-brand { flex: 0 1 auto; min-width: 0; overflow: hidden; } 1317 .gk-nav-brand > span { min-width: 0; overflow: hidden; } 1318 /* Pour qu'une ellipse s'AFFICHE, c'est l'element porteur du texte qui 1319 doit etre contraint â sinon le parent le rogne et la coupure tombe au 1320 milieu d'une lettre, sans point de suspension. On propage donc la 1321 contrainte : la marque ne depasse pas sa colonne, et chaque ligne de 1322 texte est un bloc compressible. */ 1323 .topbar-logo > .gk-nav-brand { max-width: 100%; } 1324 .gk-nav-brand > span { flex: 0 1 auto; } 1325 .gk-nav-brand > span > span { display: block; white-space: nowrap; 1326 overflow: hidden; text-overflow: ellipsis; 1327 /* `max-width` EST la piece qui manquait. Mesure du 2026-09-01 en 1328 390 px : la ligne de texte faisait 166 px dans un parent de 99 â 1329 `brand_html` pose `align-items: flex-start` en ligne, donc les 1330 enfants se dimensionnent sur leur CONTENU et non sur le conteneur. 1331 Sans contrainte, `text-overflow` n'a rien a tronquer : le parent 1332 rognait, et la coupure tombait au milieu d'une lettre. */ 1333 max-width: 100%; } 1334 .connect-pill { padding: 3px; } 1335 } 1336 1337 /* Palier le plus etroit. En 46vw la rangee tient a 412, 390 et 360 px avec 1338 le nom du site ENTIER ; a 320 elle depasse de 18. Ce bloc DOIT suivre 1339 celui de 767 : meme specificite, donc c'est l'ordre qui tranche. */ 1340 @media (max-width: 359px) { 1341 .topbar-logo { max-width: 38vw; } 1342 } 1343
1344 /* Header search row (option C â ported from _archive/header.php). 1345 Appears below the nav-row inside the topbar; visible on every page 1346 that includes _gk_nav (catalog, orders, brands, cart). Each input 1347 drives the catalog Vue via window.headerSearch when present, or 1348 navigates to /catalog?searchField=X&searchTerm=Y on non-catalog 1349 pages (catalog.html's mount reads those params). Autocomplete is 1350 fed by /api/lorvert/autocomplete. */ 1351 .search-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; padding: 0 4px 8px; } 1352 .search-wrap { position: relative; } 1353 .search-wrap input { width: 100%; padding: 5px 28px 5px 10px; font-size: 13px; border: 1px solid rgba(255,255,255,0.25); border-radius: 6px; height: 30px; background: #fff; color: #1f2937; transition: border-color .15s, box-shadow .15s, background .15s; } 1354 .search-wrap input::placeholder { color: #6b7280; } 1355 .search-wrap input:focus { outline: none; border-color: var(--topbar-accent); background: #fff; box-shadow: 0 0 0 2px color-mix(in srgb, var(--topbar-accent) 30%, transparent); } 1356 .search-arrow { position: absolute; right: 1px; top: 1px; bottom: 1px; width: 24px; background: #f3f4f6; border: none; border-left: 1px solid #e5e7eb; border-radius: 0 5px 5px 0; cursor: pointer; display: flex; align-items: center; justify-content: center; color: #4b5563; font-size: 10px; } 1357 .search-arrow:hover { background: #e5e7eb; color: #111; } 1358 .ac-list { position: absolute; top: 100%; left: 0; right: 0; z-index: 1100; background: #fff; border: 1px solid #d1d5db; border-top: none; border-radius: 0 0 6px 6px; max-height: 300px; overflow-y: auto; box-shadow: 0 4px 12px rgba(0,0,0,0.16);
1358 margin-top: 1px; } 1359 .ac-item { padding: 6px 10px; font-size: 13px; cursor: pointer; color: #333; } 1360 .ac-item:hover, .ac-item.ac-active { background: #fff7d6; color: #111; } 1361 html[data-theme="dark"] .ac-list { background: #1a1e27; border-color: rgba(255,255,255,0.10); box-shadow: 0 4px 12px rgba(0,0,0,0.6); } 1362 html[data-theme="dark"] .ac-item { color: rgba(255,255,255,0.85); } 1363 html[data-theme="dark"] .ac-item:hover, html[data-theme="dark"] .ac-item.ac-active { background: rgba(252,211,77,0.18); color: #fff; } 1364 @media (max-width: 900px) { .search-row { grid-template-columns: repeat(2, 1fr); } } 1365 @media (max-width: 480px) { .search-row { grid-template-columns: 1fr; } } 1366 1367 /* Hortitec-style unified search â trigger + modal. The trigger 1368 looks like a full-width search input but is a button: click anywhere 1369 to open the modal. */ 1370 /* Search trigger â fixed-width pill (no more typewriter / max-width 1371 dance). Bluish translucent surface sits naturally against the navy 1372 topbar; the constant gold blinking cursor at the end signals 1373 "click to type" without taking up extra space. */ 1374 .gk-search-trigger { 1375 display: inline-flex; align-items: center; gap: 10px; 1376 flex: 1 1 auto; /* the elastic middle â absorbs/returns slack */ 1377 width: auto; min-width: 0; max-width: 720px; height: 38px; 1378 padding: 4px 16px; margin: 0; 1379 /* ââ La recherche prend la couleur de SA barre ââââââââââââââââââââ 1380 Ces trois valeurs etaient un bleu indigo en dur, taille pour la barre 1381 navy de lorvert. Sur sphaigne, dont la barre est creme, le champ 1382 ressortait en periwinkle sur fond ivoire â « the research colors are 1383 ugly », owner 2026-08-31, capture a l'appui. Les jetons `--topbar-*` 1384 sont deja resolus par site (`_data_theme.html`), donc le champ suit 1385 desormais la barre au lieu de supposer laquelle il habite. */ 1386 border: 1px solid var(--topbar-border, rgba(147,197,253,0.30)); 1387 border-radius: 8px; 1388 background: var(--topbar-hover-bg, rgba(30,58,138,0.28)); 1389 color: var(--topbar-muted, rgba(226,232,240,0.78)); 1390 font: inherit; font-size: 14px; text-align: left; cursor: pointer; 1391 transition: background .15s, border-color .15s, box-shadow .15s; 1392 } 1393 .gk-search-trigger:hover, .gk-search-trigger:focus { 1394 background: color-mix(in srgb, var(--topbar-accent, #1e3a8a) 10%, var(--topbar-hover-bg, rgba(30,58,138,0.42))); 1395 border-color: var(--topbar-accent, rgba(147,197,253,0.55)); 1396 box-shadow: 0 0 0 2px color-mix(in srgb, var(--topbar-accent) 25%, transparent); 1397 color: var(--topbar-text, #f1f5f9); 1398 } 1399 .gk-search-trigger i { color: var(--topbar-muted, rgba(191,219,254,0.80)); font-size: 14px; } 1400 /* flex:0 1 auto (not 1) so the text sizes to its content and the gold 1401 ::after caret hugs the end of the placeholder instead of being pushed 1402 to the right edge; min-width:0 keeps ellipsis truncation when tight. */ 1403 .gk-search-trigger-text { flex: 0 1 auto; min-width: 0; display: inline-block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 1404 /* Permanent gold-blinking cursor at the right edge of the trigger â 1405 replaces the per-char typewriter caret. Tells the user "press here 1406 to start typing" without any width animation. */ 1407 .gk-search-trigger::after { 1408 content: 'â'; flex: 0 0 auto; 1409 color: var(--topbar-accent); 1410 font-weight: 700; 1411 animation: gkCaretBlink 1s steps(2, end) infinite; 1412 } 1413 @keyframes gkCaretBlink { 50% { opacity: 0; } } 1414 1415 /* ââ Responsive search trigger (Guillaume 2026-05-18) âââââââââââââ 1416 Three smooth steps as the viewport narrows: 1417 - â¥1100px : full 280px, placeholder + caret (default rules above) 1418 - 900-1099 : 200px, caret hidden (less twitch in tight rows) 1419 - 720-899 : 140px, placeholder still visible but truncated 1420 - â¤719px : icon-only square button (collapses to ~36px) â the 1421 placeholder + caret hide, click still opens the 1422 same modal. UX-equivalent to a search-icon-button. 1423 Width + padding transition together so the collapse reads as a 1424 smooth fold rather than a snap. */ 1425 .gk-search-trigger { transition: width .25s ease, padding .25s ease, 1426 background .15s, border-color .15s, 1427 box-shadow .15s; } 1428 @media (max-width: 1099px) { 1429 /* search stays elastic (base flex:1) â just drop the blinking caret */ 1430 .gk-search-trigger::after { display: none; } 1431 } 1432 @media (max-width: 899px) { 1433 .gk-search-trigger { gap: 8px; padding-left: 10px; padding-right: 10px; } 1434 } 1435 @media (max-width: 719px) { 1436 .gk-search-trigger { 1437 flex: 0 0 auto; /* stop the elastic grow â fixed icon on the strip */ 1438 width: 36px; height: 36px; 1439 padding: 0; 1440 gap: 0; 1441 justify-content: center; 1442 margin-right: 6px; 1443 } 1444 .gk-search-trigger .gk-search-trigger-text { display: none; } 1445 .gk-search-trigger i { font-size: 14px; color: var(--topbar-text, rgba(226,232,240,0.92)); } 1446 } 1447 1448 /* Search modal â themed via the site CSS variables emitted by 1449 _data_theme.html so the panel feels native to lorvert/poppies 1450 instead of a generic gray sheet. Overlay tints with the brand 1451 navy; panel has a thin accent stripe along the top. */ 1452 .gk-search-modal { display: none; position: fixed; inset: 0; z-index: 3000; background: color-mix(in srgb, var(--text, #15233e) 70%, transparent); align-items: flex-start; justify-content: center; padding: 60px 20px 20px; overflow-y: auto; } 1453 .gk-search-modal.is-open { display: flex; animation: gkSearchFade .18s ease-out; } 1454 @keyframes gkSearchFade { from { opacity: 0; } to { opacity: 1; } } 1455 .gk-search-panel { width: 100%; max-width: 720px; background: var(--surface, #fff); border-radius: 14px; box-shadow: 0 20px 60px rgba(15,23,42,0.35); overflow: hidden; animation: gkSearchSlide .22s cubic-bezier(.16,1,.3,1); border-top: 3px solid var(--accent, #f3c84b); } 1456 @keyframes gkSearchSlide { from { transform: translateY(-12px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } 1457 .gk-search-head { display: flex; align-items: center; gap: 10px; padding: 14px 18px; border-bottom: 1px solid var(--border, #e5e7eb); } 1458 .gk-search-head input { flex: 1; min-width: 0; border: none; outline: none; font: inherit; font-size: 16px; padding: 6px 4px; color: #1f2937; background: transparent; } 1459 /* Suppress browser-native <input type="search"> clear button. Without 1460 this rule Chromium/Safari render their own X next to the custom 1461 `.gk-search-clear` + `.gk-search-close` â three X icons in a row. */ 1462 .gk-search-head input::-webkit-search-cancel-button, 1463 .gk-search-head input::-webkit-search-decoration { -webkit-appearance: none; appearance: none; } 1464 .gk-search-head input[type="search"] { -moz-appearance: textfield; appearance: textfield; } 1465 .gk-search-head .gk-search-clear { width: 28px; height: 28px; border: none; background: transparent; color: #9ca3af; font-size: 14px; cursor: pointer; border-radius: 6px; } 1466 .gk-search-head .gk-search-clear:hover { background: #f3f4f6; color: #1f2937; } 1467 .gk-search-head .gk-search-close { width: 32px; height: 32px; border: none; background: transparent; color: #6b7280; font-size: 16px; cursor: pointer; border-radius: 8px; } 1468 .gk-search-head .gk-search-close:hover { background: #f3f4f6; color: #1f2937; } 1469 .gk-search-head .gk-search-icon { color: #6b7280; font-size: 18px; padding: 0 4px; } 1470 .gk-search-body { max-height: calc(100vh - 180px); overflow-y: auto; } 1471 .gk-search-empty { padding: 48px 24px; text-align: center; color: #9ca3af; font-size: 13px; } 1472 /* Quick-action shortcuts (Nouveautés / Promotions / Mes favoris). 1473 One-liner inline pills â all three share a single row on desktop, 1474 wrap to two/one on narrow viewports. Semantic icon colour is 1475 inline (blue=new, red=promo, pink=favoris) matching the catalog 1476 sidebar's `.sb-item` vocabulary so the customer recognises the 1477 destination instantly. Adding a 4th action drops in markup-only; 1478 the row stays flex-1 so the new chip resizes the others. */ 1479 .gk-search-quick { display: flex; flex-wrap: wrap; gap: 6px; padding: 10px 16px; border-bottom: 1px solid var(--border, #e5e7eb); } 1480 .gk-search-quick-btn { 1481 flex: 1 1 0; min-width: 110px; 1482 display: inline-flex; align-items: center; justify-content: center; gap: 6px; 1483 padding: 5px 10px; 1484 font-size: 12px; font-weight: 600; 1485 color: var(--text, #1f2937); 1486 background: var(--surface, #fff); 1487 border: 1px solid var(--border, #e5e7eb); 1488 border-radius: 999px; 1489 text-decoration: none; cursor: pointer; 1490 transition: border-color .15s, background .15s, color .15s; 1491 } 1492 .gk-search-quick-btn:hover { 1493 border-color: var(--accent, #f3c84b); 1494 background: color-mix(in srgb, var(--accent, #f3c84b) 8%, var(--surface, #fff)); 1495 } 1496 .gk-search-quick-icon { font-size: 13px; flex-shrink: 0; } 1497 .gk-search-section { padding: 14px 22px 4px; } 1498 .gk-search-section + .gk-search-section { border-top: 1px solid var(--border, #f3f4f6); padding-top: 14px; } 1499 .gk-search-section-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; font-size: 11px; font-weight: 700; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.08em; } 1500 .gk-search-section-head a { color: var(--topbar-accent); text-decoration: none; font-weight: 600; font-size: 11px; text-transform: none; letter-spacing: 0; } 1501 .gk-search-section-head a:hover { text-decoration: underline; } 1502 .gk-search-product { display: flex; align-items: center; gap: 14px; padding: 10px 4px; border-bottom: 1px solid #f3f4f6; color: inherit; text-decoration: none; cursor: pointer; } 1503 .gk-search-product:last-child { border-bottom: none; } 1504 .gk-search-product:hover { background: #fafafa; } 1505 .gk-search-product .gk-sp-img { flex: 0 0 56px; width: 56px; height: 56px; border-radius: 6px; background: #f9fafb; display: flex; align-items: center; justify-content: center; overflow: hidden; } 1506 .gk-search-product .gk-sp-img img { max-width: 100%; max-height: 100%; object-fit: contain; } 1507 .gk-search-product .gk-sp-img i { color: #d1d5db; font-size: 20px; } 1508 .gk-search-product .gk-sp-body { flex: 1 1 auto; min-width: 0; } 1509 .gk-search-product .gk-sp-brand { font-size: 11px; font-weight: 700; color: #16a34a; text-transform: uppercase; letter-spacing: 0.04em; } 1510 .gk-search-product .gk-sp-name { font-size: 14px; color: #1f2937; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 1511 .gk-search-product .gk-sp-ref { font-size: 11px; color: #9ca3af; margin-top: 2px; } 1512 .gk-search-product .gk-sp-fam { font-size: 11px; font-weight: 600; color: #6b7280; margin-top: 3px; display: flex; align-items: center; gap: 5px; } 1513 .gk-search-product .gk-sp-fam .gk-sp-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; } 1514 .gk-search-product .gk-sp-price { flex: 0 0 auto; text-align: right; } 1515 .gk-search-product .gk-sp-price-lbl { font-size: 10px; color: #9ca3af; } 1516 .gk-search-product .gk-sp-price-val { font-size: 14px; font-weight: 700; color: #1f2937; } 1517 .gk-search-chips { display: flex; flex-wrap: wrap; gap: 6px; padding-bottom: 4px; } 1518 .gk-search-chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px 4px 6px; font-size: 12px; color: #374151; background: #f3f4f6; border: none; border-radius: 999px; cursor: pointer; font-weight: 500; text-decoration: none; line-height: 1.2; } 1519 .gk-search-chip:hover { background: #e5e7eb; } 1520 .gk-search-chip.is-on { background: var(--topbar-accent); color: var(--topbar-on-accent); font-weight: 600; } 1521 .gk-search-chip--brand { padding-left: 4px; }
1522 .gk-search-chip-logo { width: 20px; height: 20px; object-fit: contain; background: #fff; border-radius: 50%; padding: 1px; flex: 0 0 auto; } 1523 .gk-search-hl { background: #fef08a; padding: 0 1px; border-radius: 2px; color: inherit; } 1524 1525 /* ââ Dark mode (2026-05-21) â the search modal had hardcoded light 1526 colors that stayed white-on-dark. Mirror the landing's dark palette 1527 (#1a1e27 panel / #f3f6fb text) so the modal matches the page. ââ */ 1528 html[data-theme="dark"] .gk-search-panel { background: #1a1e27; box-shadow: 0 20px 60px rgba(0,0,0,0.55); } 1529 html[data-theme="dark"] .gk-search-head { border-bottom-color: rgba(255,255,255,0.10); } 1530 html[data-theme="dark"] .gk-search-head input { color: #f3f6fb; } 1531 html[data-theme="dark"] .gk-search-head input::placeholder { color: rgba(255,255,255,0.45); } 1532 html[data-theme="dark"] .gk-search-head .gk-search-icon, 1533 html[data-theme="dark"] .gk-search-head .gk-search-close { color: rgba(255,255,255,0.65); } 1534 html[data-theme="dark"] .gk-search-head .gk-search-clear { color: rgba(255,255,255,0.50); } 1535 html[data-theme="dark"] .gk-search-head .gk-search-clear:hover, 1536 html[data-theme="dark"] .gk-search-head .gk-search-close:hover { background: rgba(255,255,255,0.10); color: #fff; } 1537 html[data-theme="dark"] .gk-search-quick { border-bottom-color: rgba(255,255,255,0.10); } 1538 html[data-theme="dark"] .gk-search-quick-btn { background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.14); color: #e5e7eb; } 1539 html[data-theme="dark"] .gk-search-quick-btn:hover { background: color-mix(in srgb, var(--accent, #f3c84b) 14%, #1a1e27); border-color: var(--accent, #f3c84b); } 1540 html[data-theme="dark"] .gk-search-section + .gk-search-section { border-top-color: rgba(255,255,255,0.08); } 1541 html[data-theme="dark"] .gk-search-section-head { color: rgba(255,255,255,0.50); } 1542 html[data-theme="dark"] .gk-search-product { border-bottom-color: rgba(255,255,255,0.07); } 1543 html[data-theme="dark"] .gk-search-product:hover { background: rgba(255,255,255,0.04); } 1544 html[data-theme="dark"] .gk-search-product .gk-sp-img { background: rgba(255,255,255,0.05); } 1545 html[data-theme="dark"] .gk-search-product .gk-sp-name, 1546 html[data-theme="dark"] .gk-search-product .gk-sp-price-val { color: #f3f6fb; } 1547 html[data-theme="dark"] .gk-search-product .gk-sp-ref, 1548 html[data-theme="dark"] .gk-search-product .gk-sp-price-lbl { color: rgba(255,255,255,0.45); } 1549 html[data-theme="dark"] .gk-search-empty { color: rgba(255,255,255,0.45); } 1550 html[data-theme="dark"] .gk-search-chip { background: rgba(255,255,255,0.08); color: #e5e7eb; } 1551 html[data-theme="dark"] .gk-search-chip:hover { background: rgba(255,255,255,0.14); } 1552 html[data-theme="dark"] .gk-search-hl { background: rgba(243,200,75,0.30); color: #fff; } 1553 1554 /* ââ Premium confirm modal (replaces window.confirm) âââââââââââââââ */ 1555 .gk-confirm-ov { position: fixed; inset: 0; z-index: 4000; display: flex; align-items: center; justify-content: center; padding: 20px; background: color-mix(in srgb, #0b1220 55%, transparent); backdrop-filter: blur(3px); opacity: 0; transition: opacity .18s ease; } 1556 .gk-confirm-ov.is-open { opacity: 1; } 1557 .gk-confirm { width: 100%; max-width: 400px; background: var(--surface, #fff); color: var(--text, #1f2937); border-radius: 16px; padding: 28px 26px 22px; text-align: center; box-shadow: 0 24px 70px rgba(0,0,0,0.35); border-top: 3px solid var(--accent, #f3c84b); transform: translateY(10px) scale(.97); transition: transform .22s cubic-bezier(.16,1,.3,1); } 1558 .gk-confirm-ov.is-open .gk-confirm { transform: translateY(0) scale(1); } 1559 .gk-confirm-icon { width: 54px; height: 54px; margin: 0 auto 14px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: color-mix(in srgb, var(--accent, #f3c84b) 18%, transparent); color: var(--accent, #f3c84b); font-size: 22px; } 1560 .gk-confirm-title { margin: 0 0 8px; font-size: 17px; font-weight: 700; line-height: 1.3; } 1561 .gk-confirm-msg { margin: 0 0 22px; font-size: 14px; line-height: 1.55; color: color-mix(in srgb, var(--text, #1f2937) 72%, transparent); } 1562 .gk-confirm-msg strong { color: var(--text, #1f2937); font-weight: 700; } 1563 .gk-confirm-actions { display: flex; gap: 10px; justify-content: center; } 1564 .gk-confirm-actions button { flex: 1; padding: 11px 16px; font-size: 14px; font-weight: 600; border-radius: 10px; cursor: pointer; border: 1px solid transparent; transition: filter .15s, background .15s, border-color .15s; font-family: inherit; } 1565 .gk-confirm-cancel { background: transparent; border-color: var(--border, #e5e7eb); color: color-mix(in srgb, var(--text, #1f2937) 70%, transparent); } 1566 .gk-confirm-cancel:hover { background: color-mix(in srgb, var(--text, #1f2937) 6%, transparent); } 1567 .gk-confirm-ok { background: var(--accent, #f3c84b); color: var(--topbar-on-accent, #1A2C42); } 1568 .gk-confirm-ok:hover { filter: brightness(1.07); box-shadow: 0 4px 14px color-mix(in srgb, var(--accent, #f3c84b) 45%, transparent); } 1569 html[data-theme="dark"] .gk-confirm { background: #1a1e27; color: #f3f6fb; box-shadow: 0 24px 70px rgba(0,0,0,0.6); } 1570 html[data-theme="dark"] .gk-confirm-msg { color: rgba(255,255,255,0.72); } 1571 html[data-theme="dark"] .gk-confirm-msg strong { color: #f3f6fb; } 1572 html[data-theme="dark"] .gk-confirm-cancel { border-color: rgba(255,255,255,0.18); color: rgba(255,255,255,0.78); } 1573 html[data-theme="dark"] .gk-confirm-cancel:hover { background: rgba(255,255,255,0.07); } 1574 @keyframes gkConfirmIn { from { opacity: 0; } to { opacity: 1; } } 1575 1576 @media (max-width: 480px) { 1577 .gk-search-modal { padding: 12px; } 1578 .gk-search-product .gk-sp-img { flex: 0 0 44px; width: 44px; height: 44px; } 1579 } 1580 1581 /* ââ Shrink-on-scroll (Guillaume 2026-05-18) ââââââââââââââââââââââ 1582 Mirrors the canonical .gk-nav.is-scrolled pattern shipped in 1583 gateka-core/static/gk-nav.css, but targets THIS file's .topbar 1584 element (lorvert/poppies are still on the bespoke .topbar chrome, 1585 not the canonical .gk-nav). Logo height drives the topbar height; 1586 shrinking the logo therefore shrinks the whole nav. The logo 1587 container has `flex: 0 0 200px` so its width is fixed regardless 1588 of inner img size â horizontal positions of the right cluster stay 1589 stable (Guillaume's hard rule from the elen iteration). JS toggle 1590 lives at the bottom of this file. */ 1591 .topbar { transition: box-shadow .22s ease; } 1592 .topbar.is-scrolled { box-shadow: 0 2px 12px rgba(0,0,0,.08); } 1593 .topbar-logo img { transition: height .22s ease; } 1594 .topbar-logo { transition: padding .22s ease; } 1595 /* Compacted (scrolled): shrink the logo to ~line height so the whole bar
1596 collapses to a single tight row (Guillaume 2026-05-27). Was var-8px 1597 (~64â88px), barely smaller than resting. */ 1598 /* Le compactage règle la VARIABLE, il n'écrase plus `height` : c'est ce 1599 qui garde le centrage optique valable dans tous les états. Ãcrasé en 1600 dur, le logo rétrécissait pendant que le calage au-dessus restait 1601 calculé sur la taille au repos (Guillaume : « when compacted, not 1602 centered anymore »). Le fichier annonçait déjà « Single knob: 1603 --gk-logo-height » â deux règles le contredisaient. */ 1604 .topbar.is-scrolled { --gk-logo-height: 40px; } 1605 @media (max-width: 640px) { 1606 .topbar.is-scrolled { --gk-logo-height: 36px; } 1607 } 1608 /* Extra vertical squeeze (Guillaume 2026-05-18) â beyond the logo 1609 shrink + alert collapse, also tighten the inner padding + 1610 button geometry so the topbar's effective height drops by another 1611 ~20px when scrolled. Every reclaimed pixel goes to the catalog 1612 below. All transitions match the 220ms canonical so the whole 1613 fold reads as one motion, not a cascade. */ 1614 .topbar-logo, .topbar-content, .nav-row, 1615 .nav-links li a, .nav-links li button.nav-btn-newcmd { 1616 transition: padding .22s ease, margin .22s ease, 1617 min-height .22s ease, gap .22s ease; 1618 } 1619 .topbar.is-scrolled .topbar-logo { padding-top: 0; padding-bottom: 0; } 1620 .topbar.is-scrolled .topbar-content { padding: 0; } 1621 .topbar.is-scrolled .nav-row { margin-bottom: 0; } 1622 .topbar.is-scrolled .nav-links li a, 1623 .topbar.is-scrolled .nav-links li button.nav-btn-newcmd { 1624 padding: 2px 8px; 1625 min-height: 30px; 1626 gap: 1px; 1627 } 1628 @media (min-width: 900px) { 1629 .topbar.is-scrolled .nav-links li a, 1630 .topbar.is-scrolled .nav-links li button.nav-btn-newcmd { 1631 padding: 4px 12px; 1632 min-height: 36px; 1633 } 1634 } 1635 1636 /* ââ Contact compaction step (part of the layout ladder above) ââââââââââ 1637 Full Service Client PRO pill (label + phone + email) â¥1200px where the 1638 elastic row has room; folds to a single phone icon in the tight band 1639 768â1199; hidden entirely on the â¤767 mobile strip (see that block). 1640 The base nav-row model (search-elastic, nowrap) already keeps it on one 1641 line at every width â this only swaps the pill's *content* density. */ 1642 /* Rangee chargee (>=5 entrees publiques) : le palier « icone seule » 1643 court jusqu'a 1439. Il montait a 1599, ce qui repliait le bloc contact 1644 en une simple icone de telephone sur un ecran de 1440 â l'adresse de 1645 courriel, la seule information que le visiteur ne peut pas deviner, 1646 disparaissait alors qu'il restait de la place. Mesure du 2026-08-31 sur 1647 sphaigne (six entrees, pastille complete forcee) : la rangee demande 1648 1085px pour 1190 disponibles a 1440, et 1085 pour 1116 des 1366 â aucun 1649 debordement. On garde 1440 comme seuil et non 1366 : la nav connectee 1650 ajoute « Catalogue » et la recherche, et c'est la marge qui les absorbe. 1651 Meme declaration que le bloc 768-1199 ci-dessous, portee par .is-nav-dense. */ 1652 @media (min-width: 1200px) and (max-width: 1439px) { 1653 .is-nav-dense .gk-support-pill { flex-direction: row; align-items: center; 1654 padding: 0; gap: 0; border: none; background: transparent; } 1655 .is-nav-dense .gk-support-pill .gk-support-label, 1656 .is-nav-dense .gk-support-pill .gk-support-phone-text, 1657 .is-nav-dense .gk-support-pill .gk-support-mail { display: none; } 1658 .is-nav-dense .gk-support-pill .gk-support-phone { 1659 display: inline-flex; align-items: center; justify-content: center; 1660 width: 38px; height: 38px; border: 1px solid var(--topbar-border); 1661 border-radius: 8px; background: rgba(0,0,0,0.15); 1662 } 1663 .is-nav-dense .gk-support-pill .gk-support-phone i { font-size: 15px; opacity: 1; } 1664 /* Dernier cran : en 1280 il manquait ~40px pour « Boutique » apres la 1665 compaction de la pastille. On resserre l'entre-deux et les flancs des 1666 entrees plutot que de rogner la taille du texte â mesure du 1667 2026-08-31 : six entrees passent de 697px a ~640px, la liste tient. */ 1668 .is-nav-dense .nav-links { gap: 1px; } 1669 .is-nav-dense .nav-links li a { padding-left: 6px; padding-right: 6px; } 1670 } 1671 @media (min-width: 768px) and (max-width: 1199px) { 1672 .gk-support-pill { flex-direction: row; align-items: center; 1673 padding: 0; gap: 0; border: none; background: transparent; } 1674 .gk-support-pill .gk-support-label, 1675 .gk-support-pill .gk-support-phone-text, 1676 .gk-support-pill .gk-support-mail { display: none; } 1677 .gk-support-pill .gk-support-phone { 1678 display: inline-flex; align-items: center; justify-content: center; 1679 width: 38px; height: 38px; border: 1px solid var(--topbar-border); 1680 border-radius: 8px; background: rgba(0,0,0,0.15); 1681 } 1682 .gk-support-pill .gk-support-phone i { font-size: 15px; opacity: 1; } 1683 } 1684</style> 1685 1686 1687 1688<div class="topbar" id="topbar" data-brand="L'Or Vert"> 1689 <div class="topbar-inner"> 1690 <div class="topbar-logo"> 1691 1692 1693 1694 <a href="/" class="gk-nav-brand" title="L'Or Vert â Paris" style="display:inline-flex;align-items:center;gap:10px;text-decoration:none"> 1695 <img src="/lorvert-paris-static/img/logo.png" alt="L'Or Vert" style="height:40px;width:auto"> 1696 <span style="font-weight:800;font-size:1.15rem;letter-spacing:-.01em;color:var(--accent-dim,#0f4d24)">L'Or Vert</span> 1697</a> 1698 1699 </div> 1700 <div class="topbar-content"> 1701 <div class="nav-collapsible"> 1702 <div class="nav-row is-nav-nosearch"> 1703 1704 1705 1706 <ul class="nav-links"> 1707 1708 </ul> 1709 1710 1711 1712 <div class="nav-actions"> 1713 1714 1715 1716 1717 1718 1719 <a href="/#signin" class="nav-cta nav-cta--primary"> 1720 <i class="fa-solid fa-right-to-bracket"></i>
1721 <span class="nav-text"> Se connecter</span> 1722 </a> 1723 <a href="/#register" class="nav-cta nav-cta--ghost"> 1724 <i class="fa-solid fa-user-plus"></i> 1725 <span class="nav-text"> Créer un compte</span> 1726 </a> 1727 1728 1729 <button class="gk-b2c-toggle" id="gk-b2c-btn" type="button" 1730 onclick="toggleB2cMode()" title="Affichage prix B2C â panier reste en B2B" 1731 aria-label="Toggle B2C price display" 1732 style="display:none;"> 1733 <i class="fa-solid fa-tag"></i> 1734 <span class="gk-b2c-label">B2C</span> 1735 </button> 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745
1746<script src="/gateka/static/gk-nav.js?v=prod-20260926-a" defer></script>
1746 1747 1748 1749 1750<noscript><style> 1751 /* Sans JS, rien ne repose jamais `aria-expanded` : les segments doivent 1752 redevenir lisibles et cliquables, exactement comme avant ce lot. 1753 `!important` parce que cette feuille est en ligne dans le CORPS â même 1754 spécificité que `gk-nav.css`, et je ne veux pas dépendre de l'ordre 1755 source pour une règle d'accessibilité. */ 1756 /* Le garde de collision a raison de les signaler : ce sont bien deux 1757 ecritures de la classe `gk-langpill`. Elle est DELIBEREE et bornee : ces 1758 regles ne font QUE neutraliser les deux replis de `gk-nav.css`, sous 1759 `<noscript>`, donc elles ne peuvent pas s'appliquer en meme temps que 1760 lui. Consolider n'est pas possible : une feuille externe ne sait pas 1761 si JS est actif, c'est precisement ce que `<noscript>` repond. */ 1762 .gk-langpill:has(> .gk-langpill-trigger[aria-expanded="false"]) .gk-langpill-opts { /* noqa: class-collision */ 1763 grid-template-columns: 1fr !important; padding-left: 2px !important; 1764 } 1765 .gk-langpill:has(> .gk-langpill-trigger[aria-expanded="false"]) .gk-langpill-seg { /* noqa: class-collision */ 1766 opacity: 1 !important; transform: none !important; 1767 } 1768</style></noscript> 1769<details class="gk-langpill" id="gk-lang-pill" open> 1770 <summary class="gk-langpill-trigger" 1771 aria-expanded="false" 1772 aria-label="Changer de langue â Français" 1773 title="Changer de langue â Français"> 1774 <i class="fa-solid fa-globe gk-langpill-globe" aria-hidden="true"></i> 1775 1776 <span class="gk-langpill-code"><span class="gk-langpill-roll"><span>FR</span></span></span> 1777 </summary> 1778 <div class="gk-langpill-opts"> 1779 1780 1781 <div class="gk-langpill-track gk-stagger"> 1782 1783 <a class="gk-langpill-seg gk-stagger-item" href="?lang=en" style="--i: 0" 1784 title="Passer en English" aria-label="Passer en English">EN</a> 1785 1786 <a class="gk-langpill-seg gk-stagger-item" href="?lang=es" style="--i: 1" 1787 title="Passer en Español" aria-label="Passer en Español">ES</a> 1788 1789 </div> 1790 </div> 1791</details> 1792 1793 1794 1795 1796 1797 </div> 1798 </div> 1799 1800 1801 1802 1803 <div class="upgrade-alert upgrade-alert--warning"> 1804 <i class="fa-solid fa-triangle-exclamation"></i> 1805 Boutique de vente au détail en ligne en cours de rénovation. 1806 1807 </div> 1808 1809 </div> 1810 </div> 1811 </div> 1812</div> 1813 1814 1815<div class="lorvert-b2c-banner" role="status" aria-live="polite"> 1816 <span class="lorvert-b2c-banner-tag">ð· MODE B2C</span> 1817 <span class="lorvert-b2c-banner-text"> 1818 Vous affichez les prix <strong>publics TTC</strong> pour un client particulier â le panier et la commande restent en tarif pro B2B. 1819 </span> 1820 <button type="button" class="lorvert-b2c-banner-exit" onclick="if(window.toggleB2cMode)window.toggleB2cMode();" title="Quitter le mode B2C"> 1821 Quitter 1822 </button> 1823</div> 1824 1825 1826<div class="gk-search-modal" id="gk_search_modal" role="dialog" aria-modal="true" aria-label="Recherche"> 1827 <div class="gk-search-panel"> 1828 <div class="gk-search-head"> 1829 <i class="fa-solid fa-magnifying-glass gk-search-icon"></i> 1830 <input type="search" id="gk_search_input" autocomplete="off" 1831 placeholder="Produit, marque, catégorie, réfâ¦"> 1832 <button type="button" class="gk-search-clear" id="gk_search_clear" 1833 aria-label="Effacer" hidden> 1834 <i class="fa-solid fa-xmark"></i> 1835 </button> 1836 <button type="button" class="gk-search-close" id="gk_search_close" 1837 aria-label="Fermer"> 1838 <i class="fa-solid fa-xmark"></i> 1839 </button> 1840 </div> 1841 <div class="gk-search-body" id="gk_search_body"> 1842 1843 <div class="gk-search-quick" id="gk_search_quick"> 1844 <a class="gk-search-quick-btn" href="/catalog?state=new"> 1845 <i class="gk-search-quick-icon fa-solid fa-bolt" style="color:#3b82f6;"></i> 1846 <span class="gk-search-quick-label">Nouveautés</span> 1847 </a> 1848 <a class="gk-search-quick-btn" href="/catalog?state=promos"> 1849 <i class="gk-search-quick-icon fa-solid fa-percent" style="color:#ef4444;"></i>
1850 <span class="gk-search-quick-label">Promotions</span> 1851 </a> 1852 <a class="gk-search-quick-btn" href="/catalog?state=favorites"> 1853 <i class="gk-search-quick-icon fa-solid fa-heart" style="color:#ec4899;"></i> 1854 <span class="gk-search-quick-label">Mes favoris</span> 1855 </a> 1856 </div> 1857 1858 <div class="gk-search-section gk-search-recent" id="gk_search_recent" hidden> 1859 <div class="gk-search-section-head"> 1860 <span>Dernières recherches</span> 1861 <a href="#" id="gk_search_recent_clear">Effacer</a> 1862 </div> 1863 <div class="gk-search-chips" id="gk_search_recent_chips"></div> 1864 </div> 1865 1866 <div class="gk-search-footer" id="gk_search_footer"></div> 1867 1868 <div class="gk-search-results" id="gk_search_results"> 1869 <div class="gk-search-empty"> 1870 Commencez à taper pour rechercher. 1871 </div> 1872 </div> 1873 </div> 1874 </div> 1875</div>
1876<script> 1877/* Server-rendered copy for the JS below. The search-results builder 1878 and the archive-confirm dialog assemble HTML in JS, so their 1879 labels cannot be Jinja expressions inline â they arrive as 1880 constants instead of being hardcoded French, which is what they 1881 were. ADR-0038 forbids a client-side catalogue; this is the 1882 server rendering one language, which is the rule it asks for. */ 1883var T_CATEGORIES = "Cat\u00e9gories"; 1884var T_REF = "Ref"; 1885var T_ITEMS = "articles"; 1886var T_ARCHIVE_TITLE = "Archiver cette commande ?"; 1887var T_ARCHIVE_BODY = "La commande {order} sera archiv\u00e9e et une nouvelle d\u00e9marr\u00e9e. Vous la retrouverez dans votre historique."; 1888var T_ARCHIVE_OK = "Archiver"; 1889var T_CANCEL = "Annuler"; 1890/* The page language, for the JS that legitimately needs it (the 1891 decimal separator). Copy goes through the constants above, never 1892 through a branch on this. */ 1893window.LORVERT_LANG = "fr"; 1894(function () { 1895 /* Active-page highlight â flip `.active` on the matching <li> based 1896 on location.pathname so the CSS `.nav-links li.active a` rule 1897 (defined above) actually fires. Without this the topbar buttons 1898 all rendered base-styled regardless of which page was on screen â 1899 the only "highlighted" one was Nouvelle commande, and only 1900 because it carries `nav-btn-primary` (a CTA-gold class, not an 1901 active-page indicator). Lookup uses startsWith so `/orders/13310` 1902 still matches `/orders`, etc. */ 1903 /* Expose the actual topbar height to the rest of the page (and to 1904 the beta banner's body padding-top calc) so content never hides 1905 behind the topbar. The beta banner sets 1906 body.gk-env-beta { padding-top: calc(banner-h + var(--navbar-h, 0)) } 1907 and without this the topbar (~110px) covered the top of the page. */ 1908 var setNavH = function () { 1909 var tb = document.getElementById('topbar'); 1910 if (!tb) return; 1911 var h = tb.offsetHeight || 0; 1912 document.body.style.setProperty('--navbar-h', h + 'px'); 1913 document.documentElement.style.setProperty('--navbar-h', h + 'px'); 1914 }; 1915 setNavH(); 1916 window.addEventListener('resize', setNavH); 1917 window.addEventListener('load', setNavH); 1918 // Track --navbar-h ONLY at transitionend, not during scroll 1919 // (Guillaume 2026-06-22 bug report: catalog page transition not 1920 // smooth). The previous on-scroll rAF measure read offsetHeight 1921 // every animation frame â a forced sync layout each time â which 1922 // on the Vue-rendered /catalog (sticky sidebar reading 1923 // `top: calc(var(--navbar-h, 76px) + â¦)`) cascaded into a relayout 1924 // of thousands of product rows per scroll frame. That's the 1925 // staircase. The squeeze itself is pure CSS (220ms ease); only the 1926 // FINAL --navbar-h needs to land. Listen on the topbar's animating 1927 // children so we catch the actual end-of-transition. 1928 var _topbarEl = document.getElementById('topbar'); 1929 if (_topbarEl) { 1930 _topbarEl.addEventListener('transitionend', setNavH); 1931 document.querySelectorAll('#topbar img, #topbar .upgrade-alert,' 1932 + ' #topbar .nav-row, #topbar .nav-links a') 1933 .forEach(function (el) { el.addEventListener('transitionend', setNavH); }); 1934 } 1935 1936 /* Match current path against each center-nav <a href> instead of a 1937 hardcoded id map â the entries are now data-driven from 1938 packages.yaml.Lorvert-Poppies.nav_pages (architect consult 1939 2026-05-16-move-lorvert-poppies-center-nav). 1940 1941 Parse each href so links that target a pane via /features?focus=1#⦠1942 (e.g. Marques â /features?focus=1#pane-feat-brands per ADR-0035) 1943 get their pathname compared to location.pathname AND their hash 1944 compared to location.hash â otherwise the literal full-href compare 1945 failed (location.pathname is just "/features") and the Marques tab 1946 never picked up `.active`. */ 1947 function updateActiveNav() { 1948 var path = location.pathname || '/'; 1949 var hash = location.hash || ''; 1950 var links = document.querySelectorAll('.nav-links li a[href]'); 1951 var bestLi = null, bestScore = -1; 1952 for (var i = 0; i < links.length; i++) { 1953 var href = links[i].getAttribute('href'); 1954 if (!href || href === '/' || href.charAt(0) !== '/') continue; 1955 var hrefUrl;
1956 try { hrefUrl = new URL(href, location.origin); } 1957 catch (_e) { continue; } 1958 var lp = hrefUrl.pathname, lh = hrefUrl.hash; 1959 var pathOk = path === lp 1960 || path.indexOf(lp + '/') === 0 1961 || path.indexOf(lp + '?') === 0; 1962 if (!pathOk) continue; 1963 // Hash-bearing links (pane targets) require an exact hash match so 1964 // /features (no hash) doesn't randomly pick up the first hashed 1965 // sibling. Pathname-only links win on pathname alone. 1966 var score; 1967 if (lh) score = (hash === lh) ? 2 : -1; 1968 else score = 1; 1969 if (score > bestScore) { 1970 bestScore = score; 1971 bestLi = links[i].closest('li'); 1972 } 1973 } 1974 // Drop any prior `.active` first â pane navigation (hashchange) 1975 // re-runs this without a full reload, so old highlights would stick. 1976 document.querySelectorAll('.nav-links li.active') 1977 .forEach(function (el) { el.classList.remove('active'); }); 1978 if (bestLi) bestLi.classList.add('active'); 1979 } 1980 updateActiveNav(); 1981 // Re-evaluate on pane navigation inside /features (hash-routed) so the 1982 // topbar highlight follows the visible pane without a full reload. 1983 window.addEventListener('hashchange', updateActiveNav); 1984 1985 /* "Archiver et démarrer une nouvelle commande" â archives every 1986 status=1 draft of the caller, then lands on /catalog where the 1987 handler mints a fresh draft via `_active_or_create_order`. Called 1988 from the catalog sidebar and the cart-page summary panel (UX 1989 consult 2026-05-16 moved this off the topbar into the order- 1990 summary surfaces where it semantically belongs). 1991 1992 summary = optional {orderid, articles, totalTtc, lang} so the 1993 confirm prompt reads concretely (e.g. "Archiver la commande 1994 N° 13332 (8 articles, 224,62 ⬠TTC) ?"). Pass null/undefined to 1995 skip the confirm (used by the migration banner's CTA). 1996 1997 Idempotent on error: any 4xx/5xx still navigates so the operator 1998 isn't trapped on a stuck button. */ 1999 /* Premium custom confirm â replaces the native window.confirm. Returns 2000 a Promise<bool>. opts: { title, message (may contain <strong>), 2001 confirmLabel, cancelLabel, icon }. Theme-aware, ESC=cancel, Enter=ok, 2002 backdrop-tap=cancel, focus lands on the confirm button. */ 2003 window._gkConfirm = function (opts) { 2004 opts = opts || {}; 2005 return new Promise(function (resolve) { 2006 var ov = document.createElement('div'); 2007 ov.className = 'gk-confirm-ov'; 2008 ov.innerHTML = 2009 '<div class="gk-confirm" role="dialog" aria-modal="true">' 2010 + '<div class="gk-confirm-icon"><i class="' + (opts.icon || 'fa-solid fa-circle-question') + '"></i></div>' 2011 + (opts.title ? '<h3 class="gk-confirm-title">' + opts.title + '</h3>' : '') 2012 + '<p class="gk-confirm-msg">' + (opts.message || '') + '</p>' 2013 + '<div class="gk-confirm-actions">' 2014 + '<button type="button" class="gk-confirm-cancel">' + (opts.cancelLabel || 'Annuler') + '</button>' 2015 + '<button type="button" class="gk-confirm-ok">' + (opts.confirmLabel || 'Confirmer') + '</button>' 2016 + '</div></div>'; 2017 document.body.appendChild(ov); 2018 requestAnimationFrame(function () { ov.classList.add('is-open'); }); 2019 var done = false; 2020 function close(val) { 2021 if (done) return; done = true; 2022 document.removeEventListener('keydown', onKey); 2023 ov.classList.remove('is-open'); 2024 setTimeout(function () { if (ov.parentNode) ov.parentNode.removeChild(ov); }, 200); 2025 resolve(val); 2026 } 2027 function onKey(e) { if (e.key === 'Escape') close(false); else if (e.key === 'Enter') close(true); } 2028 ov.querySelector('.gk-confirm-ok').addEventListener('click', function () { close(true); }); 2029 ov.querySelector('.gk-confirm-cancel').addEventListener('click', function () { close(false); }); 2030 ov.addEventListener('click', function (e) { if (e.target === ov) close(false); }); 2031 document.addEventListener('keydown', onKey); 2032 setTimeout(function () { var b = ov.querySelector('.gk-confirm-ok'); if (b) b.focus(); }, 60); 2033 }); 2034 }; 2035
2036 window.gkArchiveAndNewOrder = function (btn, summary) { 2037 var lang = (summary && summary.lang) || (document.documentElement.lang || 'fr'); 2038 var scope = window.LORVERT_SCOPE 2039 || (location.host.indexOf('poppies') >= 0 ? 'poppies' : 'lorvert'); 2040 var doArchive = function () { 2041 if (btn) { btn.disabled = true; btn.style.opacity = '0.6'; } 2042 var go = function () { location.href = '/catalog?newcmd=1'; }; 2043 fetch('/api/lorvert/orders/archive', { 2044 method: 'POST', 2045 credentials: 'same-origin', 2046 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 2047 body: 'scope=' + encodeURIComponent(scope), 2048 }).then(go, go); 2049 }; 2050 if (summary && summary.orderid) { 2051 var fmt = function (n) { 2052 if (n == null || isNaN(+n)) return ''; 2053 return (+n).toFixed(2).replace('.', lang === 'fr' ? ',' : '.'); 2054 }; 2055 var bits = []; 2056 if (summary.articles != null) bits.push(summary.articles + ' ' + T_ITEMS); 2057 if (summary.totalTtc != null) bits.push(fmt(summary.totalTtc) + ' ⬠TTC'); 2058 var detail = bits.length ? ' · ' + bits.join(' · ') : ''; 2059 window._gkConfirm({ 2060 icon: 'fa-solid fa-cart-plus', 2061 title: T_ARCHIVE_TITLE, 2062 message: T_ARCHIVE_BODY 2063 .replace('{order}', '<strong>N° ' + summary.orderid + '</strong>' + detail), 2064 confirmLabel: T_ARCHIVE_OK, 2065 cancelLabel: T_CANCEL, 2066 }).then(function (ok) { if (ok) doArchive(); }); 2067 } else { 2068 doArchive(); 2069 } 2070 }; 2071 2072 /* Connect-pill open/close (clone of header.php's toggleConnectPill) */ 2073 window.toggleConnectPill = function () { 2074 var wrap = document.getElementById('connect_pill_wrap'); 2075 var btn = document.getElementById('connect_pill_btn'); 2076 var panel = document.getElementById('connect_pill_menu'); 2077 if (!wrap || !btn) return; 2078 var open = !wrap.classList.contains('is-open'); 2079 wrap.classList.toggle('is-open', open); 2080 btn.setAttribute('aria-expanded', open ? 'true' : 'false'); 2081 if (panel) panel.setAttribute('aria-hidden', open ? 'false' : 'true'); 2082 if (open) { 2083 // Anchor the menu to the viewport (position:fixed), computed from the 2084 // button rect. This frees it from the mobile .nav-row { overflow-x:auto } 2085 // scroll-valve, which otherwise clips the dropdown (Guillaume 2026-05-27). 2086 if (panel) { 2087 var r = btn.getBoundingClientRect(); 2088 panel.style.position = 'fixed'; 2089 panel.style.top = (r.bottom + 6) + 'px'; 2090 panel.style.right = Math.max(8, window.innerWidth - r.right) + 'px'; 2091 panel.style.left = 'auto'; 2092 } 2093 setTimeout(function () { 2094 document.addEventListener('click', function close(e) { 2095 if (!wrap.contains(e.target)) { 2096 wrap.classList.remove('is-open'); 2097 btn.setAttribute('aria-expanded', 'false'); 2098 if (panel) panel.setAttribute('aria-hidden', 'true'); 2099 document.removeEventListener('click', close); 2100 } 2101 }); 2102 document.addEventListener('keydown', function esc(e) { 2103 if (e.key === 'Escape') { 2104 wrap.classList.remove('is-open'); 2105 btn.setAttribute('aria-expanded', 'false'); 2106 if (panel) panel.setAttribute('aria-hidden', 'true'); 2107 document.removeEventListener('keydown', esc); 2108 } 2109 }); 2110 }, 0); 2111 } 2112 }; 2113 2114 /* Le commutateur de langue n'a plus d'ecouteur ici â 2026-08-16. 2115 Il posait `document.cookie = 'lang=â¦'` puis rechargeait. C'etait un 2116 SECOND chemin d'ecriture du cookie de langue, a cote de 2117 `gateka.cookies.LANG` qu'ADR-0030 designe comme le seul â et les deux 2118 ne posaient pas la meme portee selon l'hote, donc ils pouvaient se 2119 masquer l'un l'autre. 2120 2121 La pilule partagee rend des liens `?lang=xx` ; c'est 2122 `GatekaMiddleware._persist_lang` qui valide et ecrit, cote serveur. 2123 Son comportement d'ouverture/fermeture (clic dehors, Echap, rouleau 2124 du code) vient de `gk-nav.js`, que `_lang_switch.html` emporte 2125 lui-meme avec sa propre garde de double-liaison. */ 2126 2127 /* "Vue rôle" switcher handler removed 2026-05-21 with its buttons 2128 (operator request). The /api/auth/role-impersonate endpoint stays in 2129 gateka-core for the impersonate bar + any other product surface. */ 2130 2131 /* Hortitec-style unified search modal. One backend round-trip 2132 (/api/lorvert/search?q=X) returns products + brands + categories; 2133 clicking any result hops to /catalog filtered to the chosen value 2134 (catalog.html reads searchField/searchTerm at mount). On the 2135 catalog page we use window.headerSearch (same Vue method as 2136 before) to avoid a full reload. */ 2137 document.addEventListener('DOMContentLoaded', function () { 2138 var trigger = document.getElementById('gk_search_trigger'); 2139 var modal = document.getElementById('gk_search_modal'); 2140 var input = document.getElementById('gk_search_input'); 2141 var body = document.getElementById('gk_search_body'); 2142 /* DOM-stable results zone â the quick-action shortcuts (Nouveautés / 2143 Promotions) live as a sibling above it, so render() can rebuild 2144 the results without clobbering the shortcuts on empty-state / 2145 clear-input. Falls back to body for legacy/test harness paths. */ 2146 var results = document.getElementById('gk_search_results') || body; 2147 var closeBtn = document.getElementById('gk_search_close'); 2148 var clearBtn = document.getElementById('gk_search_clear'); 2149 if (!trigger || !modal || !input || !body) return; 2150 2151 /* Rotating typewriter placeholder â cycles through the messages 2152 declared on data-messages (JSON array) with per-char reveal + 2153 per-char erase. ~10s per message: 40ms/char type + 7s hold + 2154 25ms/char erase + 300ms gap. Cleared automatically when the 2155 modal opens (input.focus â no need for a competing animation 2156 on the trigger). */ 2157 (function rotateTypewriter() { 2158 var el = document.getElementById('gk_search_trigger_text'); 2159 if (!el) return; 2160 var messages = [];
2161 try { messages = JSON.parse(el.getAttribute('data-messages') || '[]'); } 2162 catch (e) { messages = []; } 2163 if (!messages.length) return; 2164 if (messages.length === 1) { el.textContent = messages[0]; return; } 2165 2166 var idx = 0, charIdx = messages[0].length; 2167 el.textContent = messages[0]; // server already painted msg 0 2168 var phase = 'hold'; // start by holding the rendered msg 2169 function tick() { 2170 // Pause the animation while the modal is open (we don't want 2171 // to fight the input focus / autocomplete UX). 2172 if (modal && modal.classList.contains('is-open')) { 2173 return setTimeout(tick, 800); 2174 } 2175 var msg = messages[idx]; 2176 if (phase === 'type') { 2177 charIdx++; 2178 el.textContent = msg.slice(0, charIdx); 2179 if (charIdx >= msg.length) { phase = 'hold'; return setTimeout(tick, 7000); } 2180 return setTimeout(tick, 40); 2181 } 2182 if (phase === 'hold') { 2183 phase = 'erase'; 2184 return setTimeout(tick, 25); 2185 } 2186 // erase 2187 charIdx--; 2188 el.textContent = msg.slice(0, Math.max(0, charIdx)); 2189 if (charIdx <= 0) { 2190 idx = (idx + 1) % messages.length; 2191 charIdx = 0; 2192 phase = 'type'; 2193 return setTimeout(tick, 300); 2194 } 2195 return setTimeout(tick, 25); 2196 } 2197 // Kick off after the initial 7s hold on the first message. 2198 setTimeout(tick, 7000); 2199 })(); 2200 2201 var scope = window.LORVERT_SCOPE || (location.host.indexOf('poppies') >= 0 ? 'poppies' : 'lorvert'); 2202 var onCatalog = /\/catalog\b/.test(location.pathname); 2203 var debounce = null; 2204 var lastQ = ''; 2205 var RECENT_KEY = 'gk_search_recent_' + scope; 2206 var RECENT_MAX = 10; 2207 var recentNode = document.getElementById('gk_search_recent'); 2208 var recentChips = document.getElementById('gk_search_recent_chips'); 2209 var recentClear = document.getElementById('gk_search_recent_clear'); 2210 2211 function recentRead() { 2212 try { 2213 var raw = localStorage.getItem(RECENT_KEY); 2214 var arr = raw ? JSON.parse(raw) : []; 2215 return Array.isArray(arr) ? arr.filter(function (s) { return typeof s === 'string' && s.length; }) : []; 2216 } catch (e) { return []; } 2217 } 2218 function recentWrite(arr) { 2219 try { localStorage.setItem(RECENT_KEY, JSON.stringify(arr.slice(0, RECENT_MAX))); } catch (e) {} 2220 } 2221 function recentPush(q) { 2222 q = String(q || '').trim(); 2223 if (!q) return; 2224 var arr = recentRead(); 2225 // Dedupe (case-insensitive) â move existing to front. 2226 var lc = q.toLowerCase(); 2227 arr = arr.filter(function (s) { return s.toLowerCase() !== lc; }); 2228 arr.unshift(q); 2229 recentWrite(arr); 2230 } 2231 function renderRecent() { 2232 if (!recentNode || !recentChips) return; 2233 var arr = recentRead(); 2234 if (!arr.length) { recentNode.hidden = true; return; } 2235 var html = ''; 2236 arr.forEach(function (s) { 2237 html += '<button class="gk-search-chip" data-kind="recent" data-val="' 2238 + escapeHtml(s) + '">' + escapeHtml(s) + '</button>'; 2239 }); 2240 recentChips.innerHTML = html; 2241 recentNode.hidden = false; 2242 } 2243 function recentHide() { if (recentNode) recentNode.hidden = true; } 2244 2245 /* Placeholder reveal â progressively types out the input's own 2246 placeholder ("Produit, marque, catégorie, réfâ¦"), echoing the 2247 global search trigger's typewriter but ONE-SHOT: it runs once each 2248 time the modal OPENS (operator rule 2026-05-21), never rotates or 2249 erases. Only animates on an empty input (otherwise the placeholder 2250 is hidden behind the typed value anyway). _phFull is captured once 2251 from the server-rendered attribute so re-opens replay from full. */ 2252 var _phFull = input ? (input.getAttribute('placeholder') || '') : ''; 2253 var _phTimer = null; 2254 function revealPlaceholder() { 2255 if (!input || !_phFull) return; 2256 clearTimeout(_phTimer); 2257 if (input.value) { input.placeholder = _phFull; return; } 2258 var i = 0; 2259 input.placeholder = ''; 2260 (function tick() { 2261 i++; 2262 input.placeholder = _phFull.slice(0, i); 2263 if (i < _phFull.length) _phTimer = setTimeout(tick, 45); 2264 })(); 2265 } 2266 function open() { 2267 modal.classList.add('is-open'); 2268 document.body.style.overflow = 'hidden'; 2269 renderRecent(); 2270 seedFooter(); 2271 revealPlaceholder(); 2272 setTimeout(function () { input.focus(); input.select(); }, 0); 2273 } 2274 function close() { 2275 modal.classList.remove('is-open'); 2276 document.body.style.overflow = ''; 2277 } 2278 function escapeHtml(s) { 2279 return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { 2280 return ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]; 2281 }); 2282 } 2283 function highlight(text, q) { 2284 var safe = escapeHtml(text); 2285 if (!q) return safe; 2286 var qq = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 2287 return safe.replace(new RegExp('(' + qq + ')', 'ig'), 2288 '<mark class="gk-search-hl">$1</mark>'); 2289 } 2290 // 2026-07-03 â prefer alias over raw imported name in the search dropdown. 2291 function displayName(p) { 2292 var a = p && p.alias; 2293 var n = (!a || String(a).toLowerCase() === 'null') ? ((p && p.name) || '') : a;
2294 try { return decodeURIComponent(escape(String(n || ''))); } catch (_) { return String(n || ''); } 2295 } 2296 function fmtPrice(p) { 2297 if (p == null || isNaN(+p)) return ''; 2298 return (+p).toFixed(2).replace('.', ',') + ' â¬'; 2299 } 2300 function imgSrc(p) { 2301 if (p.image_url_local) return p.image_url_local; 2302 if (p.image_url_source) return p.image_url_source; 2303 return ''; 2304 } 2305 function commit(field, value, exact) { 2306 // exact defaults to true â a brand/category/reference chip click is a 2307 // precise navigation. Free-text Enter passes exact=false so it runs the 2308 // same partial "show all" query as the "Montrer tout" link (an exact 2309 // match on a partial term like "CAN" returns nothing â the reported bug). 2310 if (exact === undefined) exact = true; 2311 // Free-text keyword searches feed the Dernières recherches history. 2312 // Brand / category / reference clicks are navigation primitives. 2313 if ((field === 'name' || field === 'search') && value) recentPush(value); 2314 if (onCatalog && typeof window.headerSearch === 'function') { 2315 window.headerSearch(field, value, exact); 2316 close(); 2317 } else { 2318 var qs = '?searchField=' + encodeURIComponent(field) + 2319 '&searchTerm=' + encodeURIComponent(value) + (exact ? '&exact=1' : ''); 2320 location.href = '/catalog' + qs; 2321 } 2322 } 2323 2324 var footer = document.getElementById('gk_search_footer'); 2325 2326 function renderProducts(products, q) { 2327 if (!products.length) { 2328 results.innerHTML = '<div class="gk-search-empty">' 2329 + (q ? 'Aucun résultat pour « ' + escapeHtml(q) + ' ».' 2330 : 'Commencez à taper pour rechercher.') 2331 + '</div>'; 2332 return; 2333 } 2334 // B2B price visibility â same skill as the catalog (view-b2b-prices). 2335 // Public / non-B2B viewers don't see prices in search either. 2336 var _grid = (window.LORVERT_SKILLS && window.LORVERT_SKILLS.catalog) || null; 2337 var showPrice = !(_grid && _grid['view-b2b-prices'] === false); 2338 // Seed-family colour code â same palette/logic as the catalog + cart + orders. 2339 function _seedColor(p) { 2340 var s = ((p.category || '') + ' ' + (p.subcategory || '')).toUpperCase(); 2341 if (/CB[DG]/.test(s)) return '#1E88E5'; 2342 if (/FAST/.test(s)) return '#E91E63'; 2343 if (/AUTO/.test(s)) return '#F39C12'; 2344 if (/FEMINIZ/.test(s)) return '#E6007E'; 2345 if (/REGULAR/.test(s)) return '#3AAA35'; 2346 return ''; 2347 } 2348 function _seedFamily(p) { 2349 var s = ((p.category || '') + ' ' + (p.subcategory || '')).toUpperCase(); 2350 if (/CB[DG]/.test(s)) { if (/AUTO/.test(s)) return 'CBD, Autoflowering'; if (/REGULAR/.test(s)) return 'CBD, Regular'; return 'CBD, Feminized'; } 2351 if (/FAST/.test(s)) return 'Fast Version, Feminized'; 2352 if (/AUTO/.test(s)) return 'Autofloraison'; 2353 if (/FEMINIZ/.test(s)) return 'Féminisée'; 2354 if (/REGULAR/.test(s)) return 'Régulière'; 2355 return ''; 2356 } 2357 var html = '<div class="gk-search-section">' 2358 + '<div class="gk-search-section-head">' 2359 + '<span>Produits</span>' 2360 + '<a href="/catalog?searchField=search&searchTerm=' + encodeURIComponent(q) + '">Montrer tout ›</a>' 2361 + '</div>'; 2362 products.forEach(function (p) { 2363 var src = imgSrc(p); 2364 var fam = _seedFamily(p), famCol = _seedColor(p); 2365 var famHtml = fam ? '<div class="gk-sp-fam"><span class="gk-sp-dot" style="background:' + famCol + ';"></span>' + escapeHtml(fam) + '</div>' : ''; 2366 html += '<a class="gk-search-product" data-ref="' + escapeHtml(p.reference) + '">' 2367 + '<div class="gk-sp-img">' 2368 + (src ? '<img src="' + escapeHtml(src) + '" alt="" loading="lazy">' 2369 : '<i class="fa-solid fa-image"></i>') 2370 + '</div>' 2371 + '<div class="gk-sp-body">' 2372 + '<div class="gk-sp-brand">' + escapeHtml(p.brand || '') + '</div>' 2373 + '<div class="gk-sp-name">' + highlight(displayName(p), q) + '</div>' 2374 + '<div class="gk-sp-ref">' + T_REF + ' ' + escapeHtml(p.reference || '') + '</div>' 2375 + famHtml 2376 + '</div>' 2377 + (showPrice 2378 ? (function () { 2379 // 2026-06-30 â match the catalog's B2C-mode behavior: 2380 // when body.lorvert-b2c-mode is on, render p.public_price 2381 // (DB: p.price_b2c_ttc) instead of the B2B p.price. 2382 // Falls through to B2B when public_price is null/0 so a 2383 // product missing a B2C price still shows something. 2384 var b2c = document.body && document.body.classList.contains('lorvert-b2c-mode'); 2385 var b2cVal = (b2c && p.public_price) ? p.public_price : null; 2386 var display = b2cVal || p.promo_price || p.price; 2387 var lbl = b2cVal ? 'Prix B2C' : (p.promo_price ? 'Promo' : 'Prix'); 2388 return '<div class="gk-sp-price">' 2389 + '<div class="gk-sp-price-lbl">' + lbl + '</div>' 2390 + '<div class="gk-sp-price-val">' + fmtPrice(display) + '</div>' 2391 + '</div>'; 2392 }()) 2393 : '') 2394 + '</a>'; 2395 }); 2396 html += '</div>'; 2397 results.innerHTML = html; 2398 } 2399 2400 function renderFooter(brands, categories, q) { 2401 if (!footer) return; 2402 var html = ''; 2403 if (brands.length) { 2404 html += '<div class="gk-search-section"><div class="gk-search-section-head"><span>Marques</span></div>' 2405 + '<div class="gk-search-chips">'; 2406 brands.forEach(function (b) { 2407 // API returns either a plain string (legacy) or { name, logo_url }. 2408 var name = (b && typeof b === 'object') ? (b.name || '') : b; 2409 var logo = (b && typeof b === 'object') ? b.logo_url : null; 2410 var logoHtml = logo 2411 ? '<img src="' + escapeHtml('/' + String(logo).replace(/^\/+/,'')) + '" alt="" loading="lazy" class="gk-search-chip-logo">' 2412 : ''; 2413 html += '<button class="gk-search-chip gk-search-chip--brand" data-kind="brand" data-val="' + escapeHtml(name) + '">' 2414 + logoHtml + '<span>' + highlight(name, q) + '</span></button>'; 2415 }); 2416 html += '</div></div>'; 2417 } 2418 if (categories.length) { 2419 html += '<div class="gk-search-section"><div class="gk-search-section-head"><span>' + T_CATEGORIES + '</span></div>' 2420 + '<div class="gk-search-chips">';
2421 categories.forEach(function (c) { 2422 html += '<button class="gk-search-chip" data-kind="category" data-val="' + escapeHtml(c) + '">' 2423 + highlight(c, q) + '</button>'; 2424 }); 2425 html += '</div></div>'; 2426 } 2427 footer.innerHTML = html; 2428 footer.hidden = !html; 2429 } 2430 2431 function render(data, q) { 2432 var products = (data && data.products) || []; 2433 var brands = (data && data.brands) || []; 2434 var categories = (data && data.categories) || []; 2435 renderProducts(products, q); 2436 renderFooter(brands, categories, q); 2437 } 2438 2439 // Full brands + categories cache. Loaded once per session on the 2440 // first modal open, then re-used by restoreFooter() whenever the 2441 // input is cleared so the customer always lands back on the full 2442 // list (not the last filtered subset). 2443 var allBrands = null; 2444 var allCategories = null; 2445 2446 function seedFooter() { 2447 if (seedFooter._done) return; 2448 seedFooter._done = true; 2449 var url = '/api/lorvert/search?scope=' + encodeURIComponent(scope) + '&q=&stream=0'; 2450 fetch(url, { credentials: 'same-origin' }) 2451 .then(function (r) { return r.json(); }) 2452 .then(function (j) { 2453 var d = (j && j.res) || {}; 2454 allBrands = d.brands || []; 2455 allCategories = d.categories || []; 2456 renderFooter(allBrands, allCategories, ''); 2457 }) 2458 .catch(function () { /* footer stays empty â non-fatal */ }); 2459 } 2460 function restoreFooter() { 2461 if (allBrands != null && allCategories != null) { 2462 renderFooter(allBrands, allCategories, ''); 2463 } 2464 } 2465 2466 function runSearch(q) { 2467 if (q === lastQ) return; 2468 lastQ = q; 2469 if (!q) { 2470 results.innerHTML = '<div class="gk-search-empty">Commencez à taper pour rechercher.</div>'; 2471 return; 2472 } 2473 // 2026-06-30 â feed Top Searches: post the settled query as an 2474 // analytics event so admin_routes top_searches() picks it up. 2475 // Debounced (200ms by caller) so we don't flood per keystroke. 2476 // Uses the short-key payload shape /api/track expects (s/p/event_type 2477 // /target â see gk.js basePayload + public_routes.track). 2478 // Best-effort: any error swallowed, never blocks the actual search. 2479 try { 2480 var trackBody = JSON.stringify({ 2481 s: scope, p: location.pathname, 2482 event_type: 'search', target: q.slice(0, 200), 2483 }); 2484 if (navigator.sendBeacon) { 2485 navigator.sendBeacon('/api/track', 2486 new Blob([trackBody], { type: 'application/json' })); 2487 } else { 2488 fetch('/api/track', { 2489 method: 'POST', credentials: 'same-origin', 2490 headers: { 'content-type': 'application/json' }, 2491 body: trackBody, keepalive: true, 2492 }).catch(function () {}); 2493 } 2494 } catch (_) { /* never block search on tracker error */ } 2495 // ?stream=0 â request the legacy single-JSON envelope. The default 2496 // stream=1 returns NDJSON (one JSON line per section: products, 2497 // brands, categories) which `r.json()` cant parse â caught here as 2498 // "Erreur réseau". Switch to non-streaming until the modal teaches 2499 // itself to read the stream incrementally. 2500 var url = '/api/lorvert/search?scope=' + encodeURIComponent(scope) 2501 + '&q=' + encodeURIComponent(q) + '&stream=0'; 2502 fetch(url, { credentials: 'same-origin' }) 2503 .then(function (r) { return r.json(); }) 2504 .then(function (j) { render((j && j.res) || {}, q); }) 2505 .catch(function () { 2506 results.innerHTML = '<div class="gk-search-empty">Erreur réseau.</div>'; 2507 }); 2508 } 2509 2510 trigger.addEventListener('click', open); 2511 closeBtn && closeBtn.addEventListener('click', close); 2512 modal.addEventListener('click', function (e) { 2513 if (e.target === modal) close(); 2514 }); 2515 document.addEventListener('keydown', function (e) { 2516 if (e.key === 'Escape' && modal.classList.contains('is-open')) close(); 2517 }); 2518 2519 input.addEventListener('input', function () { 2520 var v = input.value.trim(); 2521 clearBtn.hidden = !v; 2522 clearTimeout(debounce); 2523 if (v.length >= 2) { 2524 recentHide(); 2525 debounce = setTimeout(function () { runSearch(v); }, 200); 2526 } else if (!v) { 2527 lastQ = ''; 2528 results.innerHTML = '<div class="gk-search-empty">Commencez à taper pour rechercher.</div>'; 2529 renderRecent(); 2530 restoreFooter(); 2531 } 2532 }); 2533 input.addEventListener('keydown', function (e) { 2534 if (e.key === 'Enter') { 2535 e.preventDefault(); 2536 var v = input.value.trim(); 2537 // Partial multi-field search (field=search, exact=false) â same query 2538 // as the "Montrer tout" link, so Enter on a partial term returns the 2539 // full result list instead of an empty exact-match. 2540 if (v) commit('search', v, false); 2541 } 2542 }); 2543 clearBtn && clearBtn.addEventListener('click', function () { 2544 input.value = ''; clearBtn.hidden = true; lastQ = ''; 2545 results.innerHTML = '<div class="gk-search-empty">Commencez à taper pour rechercher.</div>'; 2546 renderRecent(); 2547 restoreFooter(); 2548 input.focus(); 2549 }); 2550 2551 // "Effacer" link in the Dernières recherches header â wipe history. 2552 recentClear && recentClear.addEventListener('click', function (e) { 2553 e.preventDefault();
2554 try { localStorage.removeItem(RECENT_KEY); } catch (err) {} 2555 renderRecent(); 2556 input.focus(); 2557 }); 2558 2559 body.addEventListener('click', function (e) { 2560 var prod = e.target.closest('.gk-search-product'); 2561 if (prod) { e.preventDefault(); commit('reference', prod.dataset.ref || ''); return; } 2562 var chip = e.target.closest('.gk-search-chip'); 2563 if (chip) { 2564 e.preventDefault(); 2565 // Recent-history chips: re-populate the input and run the 2566 // search inline (no /catalog redirect) so the customer can 2567 // refine. Brand / category chips commit and navigate. 2568 if (chip.dataset.kind === 'recent') { 2569 var v = chip.dataset.val || ''; 2570 input.value = v; 2571 clearBtn.hidden = !v; 2572 recentHide(); 2573 lastQ = ''; 2574 runSearch(v); 2575 input.focus(); 2576 return; 2577 } 2578 commit(chip.dataset.kind, chip.dataset.val || ''); 2579 } 2580 }); 2581 }); 2582})(); 2583 2584// Shrink-on-scroll for .topbar (lorvert/poppies bespoke nav, Guillaume 2585// 2026-05-18). Mirrors the canonical .gk-nav scroll listener in 2586// gateka-core/templates/_gk_nav.html â same threshold (30px), same 2587// rAF throttle. Toggles .is-scrolled on the bespoke .topbar element. 2588// CSS for the shrunken state lives in this file's <style> block above. 2589(function () { 2590 var bar = document.querySelector('.topbar'); 2591 if (!bar) return; 2592 var pending = false; 2593 function update() { 2594 pending = false; 2595 var y = window.scrollY || document.documentElement.scrollTop || 0; 2596 bar.classList.toggle('is-scrolled', y > 30); 2597 } 2598 window.addEventListener('scroll', function () { 2599 if (pending) return; 2600 pending = true; 2601 requestAnimationFrame(update); 2602 }, { passive: true }); 2603 update(); 2604})(); 2605 2606// ââ B2C-mode toggle â Lorvert Wholesale 2026-06-04 âââââââââââââââ 2607// Reveals the toggle for users with the catalog.view-b2b-prices skill 2608// (logged-in pros). Persists state in localStorage `gk-b2c-mode`, 2609// applies body class for the topbar tint, and dispatches `gk:b2cmode` 2610// so the catalog Vue app recomputes _unitPrice / _total from 2611// p.public_price (b2c_ttc in wholedb.products) without changing the 2612// cart contents. Idempotent (multiple bootstraps on Vue hot-reload 2613// won't double-fire). 2614(function () { 2615 // Visibility gated by the dedicated catalog skill `view-b2c-prices` 2616 // (declared in pane.skills; default-granted to the `desk` role). The 2617 // skill IS the permission â users without it can't flip the display, 2618 // not just by lacking the button, because the catalog Vue also reads 2619 // the same skill to decide whether to honour b2cMode. Set via the 2620 // Pages et accès matrix. 2621 function revealB2c() { 2622 var btn = document.getElementById('gk-b2c-btn'); 2623 if (!btn) return; 2624 var grid = (window.LORVERT_SKILLS && window.LORVERT_SKILLS.catalog) || null; 2625 if (grid && grid['view-b2c-prices']) { 2626 btn.style.display = ''; 2627 } 2628 } 2629 // `window.LORVERT_SKILLS` is assigned by the catalog page's own inline 2630 // script, which runs AFTER this shared-nav script. Running the reveal 2631 // now would always read an undefined grid (the bug that hid the button); 2632 // defer to DOMContentLoaded so the grid is populated. Run immediately 2633 // too in case the document is already parsed. 2634 if (document.readyState === 'loading') { 2635 document.addEventListener('DOMContentLoaded', revealB2c); 2636 } else { 2637 revealB2c(); 2638 } 2639 var stored = '0'; 2640 try { stored = localStorage.getItem('gk-b2c-mode') || '0'; } catch (e) {} 2641 if (stored === '1') document.body.classList.add('lorvert-b2c-mode'); 2642})(); 2643 2644window.toggleB2cMode = function () { 2645 var on = !document.body.classList.contains('lorvert-b2c-mode'); 2646 document.body.classList.toggle('lorvert-b2c-mode', on); 2647 try { localStorage.setItem('gk-b2c-mode', on ? '1' : '0'); } catch (e) {} 2648 window.dispatchEvent(new CustomEvent('gk:b2cmode', { detail: { active: on } })); 2649}; 2650</script>
2650 2651 2652 2653 2654 2655
2655<script> 2656 function toggleLang() { 2657 var cur = (document.cookie.match(/(?:^|;\s*)lang=([^;]+)/) || [])[1] || 'fr'; 2658 var nxt = cur === 'fr' ? 'en' : 'fr'; 2659 document.cookie = 'lang=' + nxt + ';path=/;max-age=31536000;SameSite=Lax'; 2660 try { localStorage.setItem('gk-lang', nxt); } catch (e) {} 2661 // Optimistic label flip + <html lang> update so the click feels 2662 // responsive in the ~150 ms before the reload completes. 2663 var l = document.getElementById('gk-lang-label'); 2664 if (l) l.textContent = (nxt === 'fr' ? 'EN' : 'FR'); 2665 document.documentElement.setAttribute('lang', nxt); 2666 location.reload(); 2667 } 2668 // ââ Sélecteur de langue âââââââââââââââââââââââââââââââââââââââââââ 2669 // Le clic dehors, l'Ãchap et la moitié « sortie » de l'animation 2670 // vivaient ICI. Ils sont partis dans `gateka/static/gk-nav.js`, que 2671 // `_lang_switch.html` charge lui-même â comme il charge déjà sa 2672 // feuille, et pour la même raison. 2673 // 2674 // Mesuré le 2026-08-15 en navigateur, page authentifiée : les trois 2675 // étaient morts sur `beta.artsiderz.fr`, qui inclut le partiel sans 2676 // hériter de ce gabarit. Un comportement déclaré dans la base ne 2677 // couvre que les héritiers de la base ; le partiel, lui, couvre ses 2678 // inclusions. Consultation â¦-0e645b. 2679 2680 function toggleTheme() { 2681 var c = document.documentElement.getAttribute('data-theme') || 'light'; 2682 var n = c === 'light' ? 'dark' : 'light'; 2683 document.documentElement.setAttribute('data-theme', n); 2684 var i = document.getElementById('gk-theme-icon'); 2685 if (i) i.className = (n === 'dark' ? 'fa-solid fa-sun' : 'fa-solid fa-moon');
2686 try { localStorage.setItem('gk-theme', n); } catch (e) {} 2687 } 2688 // Pre-paint sync â cookie is the source of truth for lang. Mirror 2689 // into localStorage + DOM attrs so the label paints correctly on 2690 // first frame (no flash) without the JS having to wait for a 2691 // postMessage round-trip. 2692 (function () { 2693 try { 2694 var t = localStorage.getItem('gk-theme'); 2695 var i = document.getElementById('gk-theme-icon'); 2696 if (i && t === 'dark') i.className = 'fa-solid fa-sun'; 2697 } catch (e) {} 2698 try { 2699 var cookieLang = (document.cookie.match(/(?:^|;\s*)lang=([^;]+)/) || [])[1]; 2700 var lang = cookieLang || localStorage.getItem('gk-lang') || 'fr'; 2701 if (cookieLang) localStorage.setItem('gk-lang', cookieLang); 2702 document.documentElement.setAttribute('lang', lang); 2703 var lbl = document.getElementById('gk-lang-label'); 2704 if (lbl) lbl.textContent = (lang === 'fr' ? 'EN' : 'FR'); 2705 } catch (e) {} 2706 })(); 2707 </script>
2707 2708 2709 2710 2711 2712 <div style="padding-top: var(--gk-nav-spacer-h, var(--nav-h, 68px));" aria-hidden="true"></div> 2713 2714 2715 2716 2717<article class="gk-legal"> 2718 2719 2720 <nav class="gk-legal-nav"> 2721 <a href="/mentions-legales" class="">Mentions légales</a> 2722 <a href="/cgu" class="">CGU</a> 2723 <a href="/confidentialite" class="is-active">Confidentialité</a> 2724 </nav> 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 <h1>Politique de confidentialité</h1> 2737 <p class="gk-legal-intro"> 2738 La présente politique explique les traitements de données à caractère 2739 personnel effectués par L'Or Vert sur le 2740 site <strong>https://lorvert-paris.com</strong>, dans le respect du Règlement 2741 Général sur la Protection des Données (RGPD, UE 2016/679) et de la loi 2742 Informatique et Libertés. 2743 </p> 2744 2745 <h2>Responsable du traitement</h2> 2746 <p> 2747 L'Or Vert, 17 Rue Berthollet 75005 Paris, France. 2748 Contact : <a href="mailto:[email protected]">[email protected]</a>. 2749 </p> 2750 2751 <h2>Données collectées</h2> 2752 <ul> 2753 <li><b>Compte utilisateur :</b> nom, prénom, adresse courriel, mot de passe (haché), langue, avatar.</li> 2754 <li><b>Navigation :</b> pages consultées, horodatage, type d'appareil, 2755 pays estimé (via l'adresse IP tronquée), référent. Les adresses IP ne 2756 sont jamais conservées en clair.</li> 2757 <li><b>Formulaires :</b> toute information volontairement communiquée 2758 via un formulaire de contact ou de devis.</li> 2759 <li><b>Fichiers téléversés :</b> lorsqu'ils sont associés à une tâche 2760 ou une fonctionnalité spécifique, ils restent accessibles aux seuls 2761 membres du produit concerné.</li> 2762 </ul> 2763 2764 <h2>Finalités et bases légales</h2> 2765 <table class="gk-legal-table"> 2766 <thead><tr><th>Finalité</th><th>Base légale</th><th>Durée de conservation</th></tr></thead> 2767 <tbody> 2768 <tr><td>Gestion du compte et authentification</td><td>Exécution du contrat</td><td>Durée du compte + 1 an</td></tr> 2769 <tr><td>Communication avec les utilisateurs</td><td>Intérêt légitime / consentement</td><td>3 ans après dernier échange</td></tr> 2770 <tr><td>Mesure d'audience anonymisée</td><td>Intérêt légitime (sans cookie traceur)</td><td>13 mois</td></tr> 2771 <tr><td>Sécurité du site, détection de fraude</td><td>Intérêt légitime</td><td>1 an</td></tr> 2772 <tr><td>Obligations comptables et légales</td><td>Obligation légale</td><td>10 ans</td></tr> 2773 </tbody> 2774 </table> 2775 2776 <h2>Destinataires</h2> 2777 <p> 2778 Vos données sont destinées aux équipes habilitées de 2779 L'Or Vert. Elles ne sont ni vendues ni 2780 cédées à des tiers à des fins commerciales. Elles peuvent être traitées 2781 par les sous-traitants techniques suivants, encadrés par contrat et 2782 soumis au RGPD : 2783 </p> 2784 <ul> 2785 <li><b>Hébergement :</b> OVHcloud (France).</li> 2786 <li><b>Base de données managée :</b> OVHcloud (Union européenne).</li> 2787 <li><b>Sauvegardes chiffrées :</b> OVHcloud Object Storage (région GRA, France) â données chiffrées avant envoi (age).</li> 2788 2789 </ul> 2790 2791 <h2>Transferts hors Union européenne</h2> 2792 <p> 2793 Lorsque des données transitent par un prestataire situé hors UE (par 2794 exemple pour les API d'IA génératives), le transfert s'appuie sur les 2795 Clauses Contractuelles Types de la Commission européenne et sur les 2796 engagements de conformité RGPD du sous-traitant. Les données 2797 d'identification restent en UE. 2798 </p> 2799 2800 <h2>Vos droits</h2> 2801 <p>Conformément au RGPD et à la loi Informatique et Libertés, vous disposez des droits suivants :</p> 2802 <ul> 2803 <li>droit d'accès, de rectification et d'effacement ;</li> 2804 <li>droit à la limitation et à la portabilité des données ;</li> 2805 <li>droit d'opposition au traitement fondé sur l'intérêt légitime ;</li> 2806 <li>droit de définir des directives post-mortem ;</li> 2807 <li>droit d'introduire une réclamation auprès de la CNIL 2808 (<a href="https://www.cnil.fr/fr/plaintes" rel="noopener">cnil.fr/plaintes</a>).</li> 2809 </ul> 2810 <p> 2811 Pour exercer vos droits, contactez 2812 <a href="mailto:[email protected]">[email protected]</a>. 2813 Une réponse vous sera apportée dans un délai d'un mois.
2814 </p> 2815 2816 <h2>Cookies</h2> 2817 <p> 2818 Le site utilise exclusivement des cookies strictement nécessaires à son 2819 fonctionnement (session d'authentification, préférence de thème, préférence 2820 de langue). La mesure d'audience est effectuée sans cookie traceur, à partir 2821 d'un identifiant de session anonyme recalculé à chaque visite â aucun 2822 consentement préalable n'est donc requis. Les préférences éventuelles 2823 utilisateur peuvent être révoquées à tout moment en vidant le stockage du 2824 navigateur. 2825 </p> 2826 2827 <h2>Sécurité</h2> 2828 <p> 2829 Les mots de passe sont hachés (bcrypt). Les communications sont chiffrées 2830 en TLS. Les sauvegardes quotidiennes sont chiffrées avec une clé publique 2831 (age) dont la clé privée est conservée hors du serveur de 2832 production. Les accès aux données sont restreints par rôle et par produit. 2833 </p> 2834 2835 <h2>Modifications</h2> 2836 <p> 2837 La présente politique peut être mise à jour pour refléter les évolutions 2838 légales ou techniques. La date de dernière mise à jour est indiquée en pied 2839 de page. Toute modification substantielle fera l'objet d'une notification 2840 aux utilisateurs connectés. 2841 </p> 2842 2843 2844 <p class="gk-legal-updated"> 2845 Dernière mise à jour : 2026-02-27. 2846 </p> 2847</article> 2848 2849<style> 2850.gk-legal{max-width:820px;margin:40px auto 60px;padding:0 24px;line-height:1.65;color:var(--text,#1f2937)} 2851.gk-legal h1{font-size:clamp(26px,4vw,34px);margin:0 0 20px;font-weight:800;letter-spacing:-.5px} 2852.gk-legal h2{font-size:18px;margin:30px 0 10px;font-weight:700;color:var(--text,#1f2937)} 2853.gk-legal p{margin:10px 0} 2854.gk-legal-intro{color:var(--text2,#374151);font-size:15px} 2855.gk-legal-list{list-style:none;padding:0;margin:10px 0} 2856.gk-legal-list li{padding:4px 0;border-bottom:1px solid var(--border,rgba(128,128,128,.12))} 2857.gk-legal-list li b{color:var(--text3,#6b7280);font-weight:600;margin-right:6px} 2858.gk-legal-table{width:100%;border-collapse:collapse;font-size:13px;margin:10px 0} 2859.gk-legal-table th,.gk-legal-table td{padding:8px 10px;border-bottom:1px solid var(--border,rgba(128,128,128,.12));text-align:left;vertical-align:top} 2860.gk-legal-table th{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--text3,#6b7280)} 2861.gk-legal-nav{display:flex;flex-wrap:wrap;gap:14px;margin-bottom:24px;padding-bottom:14px;border-bottom:1px solid var(--border,rgba(128,128,128,.18))} 2862.gk-legal-nav a{color:var(--text3,#6b7280);font-size:13px;text-decoration:none;font-weight:600} 2863.gk-legal-nav a:hover{color:var(--accent,#3b82f6)} 2864.gk-legal-nav a.is-active{color:var(--accent,#3b82f6)} 2865.gk-legal-warn{padding:10px 14px;margin-bottom:18px;background:rgba(180,83,9,.1);color:#b45309;border-left:3px solid #b45309;font-size:13px;border-radius:4px} 2866.gk-legal-updated{margin-top:30px;font-size:12px;color:var(--text3,#6b7280);font-style:italic} 2867.gk-legal a{color:var(--accent,#3b82f6)} 2868.gk-legal a:hover{text-decoration:underline} 2869/* Dark theme: --text isn't defined dark-aware on these shared pages, so the 2870 var() fallbacks (#1f2937â¦) render dark-on-dark. Force readable light text. */ 2871html[data-theme="dark"] .gk-legal{color:#e5e7eb} 2872html[data-theme="dark"] .gk-legal h1,html[data-theme="dark"] .gk-legal h2{color:#f3f4f6} 2873html[data-theme="dark"] .gk-legal-intro{color:#cbd5e1} 2874html[data-theme="dark"] .gk-legal-list li b, 2875html[data-theme="dark"] .gk-legal-table th, 2876html[data-theme="dark"] .gk-legal-nav a, 2877html[data-theme="dark"] .gk-legal-updated{color:#9ca3af} 2878</style> 2879 2880 2881 2882 2883<footer class="gk-legal-footer"> 2884 <div class="gk-legal-footer-inner"> 2885 2886 <span class="gk-legal-footer-copy"> 2887 © 2026 Lorvert-Poppies 2888 · <a href="https://gateka.fr" class="gk-legal-footer-sig" target="_blank" rel="noopener">gateka.fr</a> 2889 </span> 2890 2891 <nav class="gk-legal-footer-links" aria-label="Pages légales"> 2892 <a href="/mentions-legales">Mentions légales</a> 2893 <a href="/cgu">CGU</a> 2894 <a href="/confidentialite">Confidentialité</a> 2895 2896 <a class="gk-legal-footer-mail" href="mailto:[email protected]">[email protected]</a> 2897 </nav> 2898 </div> 2899</footer> 2900<style> 2901.gk-legal-footer{background:var(--bg);border-top:1px solid var(--border,rgba(128,128,128,.15));padding:18px 24px;margin-top:40px;font-size:12px;color:var(--text3,#6b7280)} 2902.gk-legal-footer-inner{max-width:var(--max-w,1200px);margin:0 auto;display:flex;flex-wrap:wrap;gap:14px;align-items:center;justify-content:space-between} 2903.gk-legal-footer-links{display:flex;gap:14px;flex-wrap:wrap} 2904.gk-legal-footer-links a{color:var(--text3,#6b7280);text-decoration:none} 2905.gk-legal-footer-links a:hover{color:var(--accent,#3b82f6);text-decoration:underline} 2906/* Pas de règle de couleur pour `.gk-legal-footer-mail` : l'adresse est un 2907 lien de pied de page comme les trois autres et se peint avec eux. Une 2908 première version en déclarait une â morte, `.gk-legal-footer-links a` 2909 la bat d'un point de spécificité, et une règle qui annonce une intention 2910 qu'elle n'obtient pas est pire que pas de règle. La classe reste comme 2911 prise pour un produit qui voudra la distinguer. */ 2912.gk-legal-footer-sig{color:var(--accent,#3b82f6);text-decoration:none;font-weight:600} 2913.gk-legal-footer-sig:hover{text-decoration:underline} 2914.gk-legal-footer-version{opacity:.65;font-variant-numeric:tabular-nums;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px} 2915</style> 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927<div id="signin" class="gk-sc-modal" role="dialog" aria-modal="true" aria-labelledby="gk-sc-title" data-entrance="fade"> 2928 <a href="#" class="gk-sc-backdrop" aria-label="Fermer" tabindex="-1"></a> 2929 <div class="gk-sc-panel"> 2930 <a href="#" class="gk-sc-close" aria-label="Fermer">×</a> 2931 2932 <h3 id="gk-sc-title" class="gk-sc-title"><i class="fa-solid fa-right-to-bracket"></i> Se connecter</h3> 2933 2934 <form id="gk-sc-form" method="post" action="#" autocomplete="on" onsubmit="return gkScSubmit(event);"> 2935 2936 2937 2938 <div id="gk-sc-google-wrap" class="gk-sc-google-wrap" hidden>
2939 <div id="gk-sc-google-btn-light" class="gk-sc-google-btn gk-sc-google-btn--light gk-sc-google-btn--iconclip"></div> 2940 <div id="gk-sc-google-btn-dark" class="gk-sc-google-btn gk-sc-google-btn--dark gk-sc-google-btn--iconclip"></div> 2941 <button type="button" id="gk-sc-microsoft-btn" class="gk-sc-ms-btn" hidden 2942 aria-label="Microsoft" title="Microsoft"> 2943 <svg width="18" height="18" viewBox="0 0 23 23" aria-hidden="true"> 2944 <path fill="#f35325" d="M1 1h10v10H1z"/><path fill="#81bc06" d="M12 1h10v10H12z"/> 2945 <path fill="#05a6f0" d="M1 12h10v10H1z"/><path fill="#ffba08" d="M12 12h10v10H12z"/> 2946 </svg> 2947 </button> 2948 <div class="gk-sc-divider"><span>ou</span></div> 2949 </div> 2950 2951 2952 <label for="gk-sc-email" class="gk-sc-label">Email</label> 2953 <input id="gk-sc-email" name="email" type="email" required autocomplete="username" 2954 value="" placeholder="Votre email" class="gk-sc-input"> 2955 2956 2957 <button type="button" id="gk-sc-magic-btn" class="gk-sc-submit gk-sc-submit-magic"> 2958 <i class="fa-solid fa-wand-magic-sparkles"></i> 2959 <span>Recevoir mon lien de connexion</span> 2960 </button> 2961 <div id="gk-sc-magic-sent" class="gk-sc-notice gk-sc-ok" hidden> 2962 <i class="fa-solid fa-circle-check"></i> Lien envoyé â vérifiez votre boîte. 2963 </div> 2964 <div id="gk-sc-err" class="gk-sc-notice gk-sc-err" hidden></div> 2965 2966 2967 <label for="gk-sc-pwd" class="gk-sc-label">Mot de passe</label> 2968 <div class="gk-sc-pwd-inner"> 2969 <input id="gk-sc-pwd" name="password" type="password" autocomplete="current-password" 2970 placeholder="Mot de passe" class="gk-sc-input"> 2971 <button type="button" class="gk-sc-eye" aria-label="Afficher le mot de passe" 2972 onclick="(function(b){var i=document.getElementById('gk-sc-pwd');if(!i)return;var on=i.type==='password';i.type=on?'text':'password';b.setAttribute('aria-label',on?'Masquer le mot de passe':'Afficher le mot de passe');b.firstElementChild.className='fa-solid '+(on?'fa-eye-slash':'fa-eye');})(this)"> 2973 <i class="fa-solid fa-eye"></i> 2974 </button> 2975 </div> 2976 2977 2978 <div class="gk-sc-login-row"> 2979 <button type="submit" class="gk-sc-submit gk-sc-submit-pwd"> 2980 <i class="fa-solid fa-arrow-right-to-bracket"></i> Se connecter 2981 </button> 2982 2983 </div> 2984 </form> 2985 2986 2987 </div> 2988</div> 2989 2990<style> 2991 /* Shape C â self-contained, CSS-variable-driven per-site theming. 2992 Composer can pass EITHER single palette values (used in both light 2993 + dark, backward-compat) OR dual variants (surface_light/surface_dark, 2994 ink_light/ink_dark, â¦). Dual variants + html[data-theme=â¦] + 2995 prefers-color-scheme let the modal adapt at runtime when the site 2996 toggles theme. */ 2997 #signin { 2998 /* Universal (theme-agnostic) â brand colours + status. */ 2999 --gk-sc-accent: #f3c84b; 3000 --gk-sc-accent-dim: #e8bc3a; 3001 --gk-sc-error: #dc2626; 3002 --gk-sc-ok: #059669; 3003 /* Light-mode palette (default). */ 3004 --gk-sc-surface: #ffffff; 3005 --gk-sc-ink: #1A2C42; 3006 --gk-sc-muted: #6b7280; 3007 --gk-sc-backdrop: rgba(15,20,28,0.7); 3008 --gk-sc-border: rgba(0,0,0,0.08); 3009 } 3010 /* Dark-mode overrides â fired when the site opts into data-theme="dark" 3011 OR when the visitor's OS prefers dark AND the site hasn't pinned light. 3012 Composers that don't declare dark variants stay on the single value 3013 (rendered above) â byte-invariant fallback. */ 3014 /* Single source of truth: the modal's OWN data-theme attribute 3015 (synced from <html>
3015 at load by JS below). Reading the modal's 3016 attr â not <html>'s â survives every site-specific theme scheme: 3017 data-theme on html/body, class-based "dark-mode", prefers-color- 3018 scheme, whatever. If a site flips theme after load, the JS 3019 mutation observer re-syncs and every rule below picks it up. */ 3020 #signin[data-theme="dark"] { 3021 --gk-sc-surface: #ffffff; 3022 --gk-sc-ink: #1A2C42; 3023 --gk-sc-muted: #6b7280; 3024 --gk-sc-backdrop: rgba(15,20,28,0.7); 3025 --gk-sc-border: rgba(0,0,0,0.08); 3026 } 3027 .gk-sc-modal { 3028 display: none; 3029 position: fixed; inset: 0; 3030 z-index: 1500; 3031 background: var(--gk-sc-backdrop); 3032 backdrop-filter: blur(2px); 3033 align-items: safe center; 3034 justify-content: center; 3035 padding: 24px; 3036 overflow-y: auto; 3037 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; 3038 } 3039 .gk-sc-modal:target { display: flex; } 3040 .gk-sc-backdrop { 3041 position: absolute; inset: 0; z-index: 0; cursor: default; 3042 } 3043 .gk-sc-panel { 3044 position: relative; z-index: 1; 3045 width: min(440px, 100%); 3046 background: var(--gk-sc-surface); 3047 color: var(--gk-sc-ink); 3048 border-radius: 14px; 3049 box-shadow: 0 20px 60px rgba(0,0,0,0.4); 3050 padding: 30px 32px 26px; 3051 transform-origin: 50% 30%; 3052 /* §D5: was 0.24s. Measured in the browser on /features, not read 3053 off this file â the sign-in panel renders inside the admin 3054 shell, so its entrance is an administration gesture and §C's 3055 200ms ceiling applies to it. */ 3056 animation: gkScFadeIn var(--gk-duration, 180ms) var(--gk-ease-out, ease-out); 3057 } 3058 #signin[data-entrance="none"] .gk-sc-panel { animation: none; } 3059 @keyframes gkScFadeIn { 3060 from { opacity: 0; transform: scale(0.94) translateY(-6px); } 3061 to { opacity: 1; transform: scale(1) translateY(0); } 3062 } 3063 .gk-sc-close { 3064 position: absolute; top: 10px; right: 14px; 3065 font-size: 26px; line-height: 1; 3066 color: var(--gk-sc-muted); text-decoration: none; font-weight: 300; 3067 } 3068 .gk-sc-close:hover { color: var(--gk-sc-ink); } 3069 /* Masthead */ 3070 .gk-sc-mast { 3071 display: flex; flex-direction: column; align-items: center; 3072 margin: 2px 0 14px 0; 3073 } 3074 .gk-sc-logo { 3075 /* Height imposée, largeur au ratio (Guillaume 2026-07-12 « the 3076 Elen logo is tiny ») : un wordmark 4:1 dans une boîte 64Ã64 3077 se dessinait en 64Ã16. Icône carrée : inchangée (64Ã64). 3078 2026-06-30 (Bogue: "Tu peux en profiter pour grossir le logo") â 3079 bumped 64 â 96 (1.5Ã) so the site brand carries more visual 3080 weight in the signin surface. max-width proportionally raised. */ 3081 height: 96px; width: auto; max-width: 300px; object-fit: contain; 3082 filter: drop-shadow(0 2px 8px rgba(0,0,0,0.08)); 3083 } 3084 /* Theme-aware logo toggling â composer can pass logo_light + logo_dark 3085 for surfaces where the mark is a dark shape on a light card vs a 3086 light shape on a dark card. Default (no data-theme, no prefers- 3087 color-scheme match) shows the light-mode variant; dark-mode surfaces 3088 switch to the dark variant. Sites that pass only logo_url render 3089 one <img class="gk-sc-logo"> with neither modifier â the rules 3090 below don't apply. */ 3091 .gk-sc-logo--dark { display: none; } 3092 .gk-sc-logo--light { display: block; } 3093 3094 /* Same single-source-of-truth pattern as palette + Google button. */ 3095 #signin[data-theme="dark"] .gk-sc-logo--light { display: none; } 3096 #signin[data-theme="dark"] .gk-sc-logo--dark { display: block; } 3097 .gk-sc-wordmark { 3098 font-size: 10px; font-weight: 700; 3099 letter-spacing: 0.42em; text-transform: uppercase; 3100 color: var(--gk-sc-muted); 3101 margin-top: 8px; padding-left: 0.42em; 3102 } 3103 .gk-sc-rule { 3104 position: relative; height: 4px; width: 60%; 3105 margin-top: 10px; overflow: hidden; 3106 } 3107 .gk-sc-rule::before, 3108 .gk-sc-rule::after { 3109 content: ""; position: absolute; left: 0; right: 0; height: 1px; 3110 background: var(--gk-sc-accent); opacity: 0.55; 3111 } 3112 .gk-sc-rule::before { top: 0; } 3113 .gk-sc-rule::after { bottom: 0; } 3114 /* Title */ 3115 .gk-sc-title { 3116 font-size: 18px; font-weight: 700; 3117 color: var(--gk-sc-ink); 3118 margin: 0 0 18px 0; 3119 letter-spacing: 0.04em; 3120 } 3121 /* Form controls */ 3122 .gk-sc-label { 3123 display: block; font-size: 12px; font-weight: 600; 3124 color: var(--gk-sc-muted); 3125 margin: 6px 0 4px 0; 3126 } 3127 .gk-sc-input { 3128 width: 100%; padding: 10px 12px; 3129 font-size: 14px; 3130 border: 1px solid var(--gk-sc-border); 3131 border-radius: 8px; 3132 background: var(--gk-sc-surface); 3133 color: var(--gk-sc-ink); 3134 outline: none; box-sizing: border-box; 3135 } 3136 .gk-sc-input:focus { 3137 border-color: var(--gk-sc-accent); 3138 box-shadow: 0 0 0 2px color-mix(in srgb, var(--gk-sc-accent) 20%, transparent); 3139 } 3140 /* Primary submit (password login = the top CTA). */ 3141 .gk-sc-submit { 3142 width: 100%; padding: 12px; 3143 font: inherit; font-size: 15px; font-weight: 700; 3144 color: var(--gk-sc-ink); 3145 background: var(--gk-sc-accent); 3146 border: none; border-radius: 8px; 3147 cursor: pointer; 3148 display: flex; align-items: center; justify-content: center; gap: 8px; 3149 margin-top: 12px; 3150 transition: background 0.2s; 3151 } 3152 .gk-sc-submit:hover:not(:disabled) { background: var(--gk-sc-accent-dim); } 3153 .gk-sc-submit:disabled { opacity: 0.6;
3153 cursor: not-allowed; } 3154 /* 2026-06-30 â Login row: primary submit on the left, forgot link on 3155 the right so a user who can't remember their password reaches 3156 recovery without leaving the visible surface. */ 3157 .gk-sc-login-row { 3158 display: flex; align-items: center; gap: 12px; 3159 margin-top: 12px; 3160 } 3161 .gk-sc-login-row .gk-sc-submit { margin-top: 0; flex: 1; } 3162 .gk-sc-link-forgot { 3163 font-size: 12px; white-space: nowrap; 3164 color: var(--gk-sc-muted); text-decoration: underline; 3165 } 3166 /* 2026-06-30 (Bogue: "i still want to promote the recevoir mon lien 3167 de connexion rather than se connecter") â primary/secondary 3168 treatments swapped. The base .gk-sc-submit (accent-filled) now 3169 applies to the MAGIC button which sits above the password fields; 3170 the .gk-sc-submit-pwd modifier below downgrades password login to 3171 outlined-secondary alongside the forgot link. */ 3172 .gk-sc-submit-pwd { 3173 background: transparent; 3174 color: var(--gk-sc-muted); 3175 border: 1px solid var(--gk-sc-border); 3176 font-weight: 600; 3177 } 3178 .gk-sc-submit-pwd:hover:not(:disabled) { 3179 background: color-mix(in srgb, var(--gk-sc-accent) 8%, transparent); 3180 color: var(--gk-sc-ink); 3181 border-color: var(--gk-sc-accent); 3182 } 3183 /* Notices */ 3184 .gk-sc-notice { 3185 font-size: 13px; font-weight: 600; 3186 margin-top: 8px; text-align: center; 3187 } 3188 .gk-sc-ok { color: var(--gk-sc-ok); } 3189 .gk-sc-err { color: var(--gk-sc-error); } 3190 /* OU divider + Google */ 3191 /* Centre by text-align, NOT flex: the wrap also carries the "OU" 3192 separator, and a flex wrap would pull it inline with the buttons 3193 (see the iconclip note below). font-size:0 kills the whitespace gap 3194 between inline-flex children; each child re-declares its own size. */ 3195 /* 2026-06-30 (Bogue: "copy the fucking connexion button shape of 3196 lorvert") â Google + MS render as small circle icons SIDE BY 3197 SIDE, matching Lorvert's inline signin. Wrap is centered with 3198 text-align (NOT flex, so the OU divider stays a wrap child 3199 without getting inline-flexed into the row). font-size:0 kills 3200 the whitespace gap between inline-flex children; each child 3201 re-declares its own size. */ 3202 .gk-sc-google-wrap { margin-top: 14px; text-align: center; font-size: 0; } 3203 /* Microsoft icon button â neutral white face in both themes, 3204 matching Google's own icon button, which never inverts either. 3205 40px circle to sit level with what GSI emits for 3206 {type:icon,shape:circle,size:large}. */ 3207 .gk-sc-ms-btn { 3208 width: 40px; height: 40px; padding: 0; border-radius: 50%; 3209 border: 1px solid #dadce0; background: #fff; cursor: pointer; 3210 display: inline-flex; align-items: center; justify-content: center; 3211 line-height: 0; transition: box-shadow .15s ease; 3212 margin: 0 7px; vertical-align: middle; 3213 } 3214 .gk-sc-ms-btn:hover { box-shadow: 0 1px 6px rgba(0,0,0,.18); } 3215 .gk-sc-ms-btn:disabled { opacity: .55; cursor: default; } 3216 .gk-sc-ms-btn[hidden] { display: none; } 3217 .gk-sc-divider { 3218 text-align: center; color: var(--gk-sc-muted); 3219 font-size: 12px; letter-spacing: 0.05em; text-transform: uppercase; 3220 margin: 4px 0 12px 0; position: relative; 3221 } 3222 .gk-sc-divider span { 3223 background: var(--gk-sc-surface); 3224 padding: 0 12px; position: relative; z-index: 1; 3225 } 3226 .gk-sc-divider::before { 3227 content: ""; position: absolute; left: 0; top: 50%; 3228 height: 1px; width: 100%; 3229 background: color-mix(in srgb, var(--gk-sc-muted) 30%, transparent); 3230 z-index: 0; 3231 } 3232 .gk-sc-google-btn { 3233 display: flex; align-items: center; justify-content: center; 3234 margin-bottom: 14px; 3235 } 3236 /* Show one Google button per active theme â same technique as the 3237 logo variants (never miss a theme flip; zero JS detection). GSI 3238 renders into both hosts; CSS shows the correct one. */ 3239 /* color-scheme: light â SANS quoi Chromium peint un canvas BLANC 3240 opaque derrière l'iframe GSI (restée light) dès que la page 3241 utilise le scheme sombre : le « carré blanc » de la capture
3242 Elen 2026-07-12. Workaround documenté côté embarqueur. */ 3243 .gk-sc-google-btn { color-scheme: light; } 3244 /* type:icon â clip circulaire du slot : le bouton PERSONNALISà de 3245 Google (session Google active) est un chip blanc rectangulaire 3246 non thémable â on le découpe en pastille nette plutôt que de 3247 subir le carré (Guillaume 2026-07-12). */ 3248 .gk-sc-google-btn--iconclip { 3249 /* centrée par marges â le wrap contient aussi le séparateur OU, 3250 un flex sur le wrap les alignerait côte à côte. */ 3251 margin: 0 7px; vertical-align: middle; 3252 width: 44px; height: 44px; border-radius: 999px; overflow: hidden; 3253 align-items: center; justify-content: center; 3254 border: 1px solid rgba(128,128,128,0.3); 3255 box-sizing: border-box; 3256 } 3257 .gk-sc-google-btn--dark { display: none; } 3258 .gk-sc-google-btn--light { display: inline-flex; } 3259 /* Read the modal's own data-entrance stamp (set at render) AND the 3260 modal's data-theme (synced from <html> at load by JS below). Using 3261 the modal's own attribute â not <html>'s â lets us survive any 3262 site-specific theme mechanism (data-theme on html/body, class-based, 3263 class="dark-mode", prefers-color-scheme, whatever). Single source 3264 of truth. */ 3265 #signin[data-theme="dark"] .gk-sc-google-btn--light { display: none; } 3266 #signin[data-theme="dark"] .gk-sc-google-btn--dark { display: inline-flex; } 3267 /* .gk-sc-toggle-row retired 2026-06-30 (password toggle removed). */ 3268 .gk-sc-link { 3269 color: var(--gk-sc-muted); 3270 text-decoration: underline; 3271 font-size: 13px; 3272 background: none; border: none; padding: 0; 3273 cursor: pointer; font: inherit; 3274 } 3275 .gk-sc-link:hover { color: var(--gk-sc-ink); } 3276 .gk-sc-link-small { font-size: 12px; margin-top: 8px; display: inline-block; } 3277 .gk-sc-link-strong { 3278 color: var(--gk-sc-ink); font-weight: 600; 3279 } 3280 .gk-sc-link-strong:hover { color: var(--gk-sc-accent-dim); } 3281 /* .gk-sc-pwd-wrap retired 2026-06-30 (password field always visible; 3282 no wrapping div needed to reveal it). */ 3283 .gk-sc-pwd-inner { position: relative; margin-top: 4px; } 3284 .gk-sc-eye { 3285 position: absolute; right: 6px; top: 50%; transform: translateY(-50%); 3286 background: none; border: none; padding: 4px 8px; 3287 color: var(--gk-sc-muted); cursor: pointer; 3288 display: inline-flex; align-items: center; justify-content: center; 3289 } 3290 .gk-sc-eye:hover { color: var(--gk-sc-ink); background: rgba(0,0,0,0.05); border-radius: 4px; } 3291 /* Signup footer */ 3292 .gk-sc-signup { 3293 text-align: center; margin-top: 14px; padding-top: 12px; 3294 border-top: 1px solid var(--gk-sc-border); 3295 font-size: 12px; color: var(--gk-sc-muted); 3296 } 3297 /* Mobile */ 3298 @media (max-width: 480px) { 3299 .gk-sc-modal { padding: 12px; } 3300 .gk-sc-panel { padding: 22px 20px 18px; } 3301 } 3302</style> 3303
3304<script src="https://accounts.google.com/gsi/client" async defer></script>
3304 3305
3306<script src="https://cdn.jsdelivr.net/npm/@azure/[email protected]/lib/msal-browser.min.js" async defer></script>
vendor: 1 bytes, line 3306
3306
3307<script> 3308 window.GK_SC_CONFIG = { 3309 lang: "fr", 3310 magic_endpoint: "/connect/magic/send", 3311 password_endpoint: "/connect/email", 3312 google_endpoint: "/connect/google", 3313 google_client_id: "728864339187-n1934ts2pd4bq81v8s50vngki4l11jlu.apps.googleusercontent.com", 3314 google_button: {"shape": "circle", "size": "large", "theme": "outline", "type": "icon"}, 3315 google_theme_light: "outline", 3316 google_theme_dark: "outline", 3317 microsoft_endpoint: "/connect/microsoft", 3318 microsoft_client_id: "8db11b3f-f470-4f40-b081-9964253d24c1", 3319 microsoft_redirect_uri: "/msal-blank.html", 3320 redirect: "/confidentialite", 3321 strings: {"back_to_magic": "Revenir au lien de connexion", "both_required": "Saisissez email et mot de passe.", "close": "Fermer", "email": "Email", "email_ph": "Votre email", "forgot": "Mot de passe oubli\u00e9 ?", "google_fail": "Connexion Google \u00e9chou\u00e9e \u2014 essayez par email.", "hide_pwd": "Masquer le mot de passe", "magic_cta": "Recevoir mon lien de connexion", "magic_invalid": "Veuillez saisir une adresse email valide.", "magic_sent": "Lien envoy\u00e9 \u2014 v\u00e9rifiez votre bo\u00eete.", "network_err": "Erreur r\u00e9seau \u2014 veuillez r\u00e9essayer.", "no_account": "Pas encore de compte ?", "or": "ou", "password": "Mot de passe", "password_ph": "Mot de passe", "show_pwd": "Afficher le mot de passe", "signin_fail": "Connexion \u00e9chou\u00e9e. Merci de r\u00e9essayer.", "signin_password": "Se connecter", "signup": "S\u0027inscrire", "title": "Se connecter", "use_password": "Utiliser mon mot de passe"} 3322 }; 3323 // ââ Legacy-anchor compat shim (2026-08-16) ââââââââââââââââââââââ 3324 // This node's id was `gk-sc-signin` until the owner made /#signin 3325 // the one way to connect. Inside gateka-core the rename was a 3326 // single-file edit â but the old spelling is ALSO written in three 3327 // OTHER repos that link here and are not deployed in lockstep: 3328 // oxygen-core src/shop/templates/gate.html, src/shop/server.py, 3329 // src/shop/templates/storyteller_foodtruck.html 3330 // artsiderz.fr src/artsiderz/templates/base.html 3331 // elen src/elen/templates/_knowledge_topbar_trigger.html 3332 // Without this shim every one of those links silently stops opening 3333 // the modal â :target simply matches nothing, so there is no error 3334 // to notice. Rewrite the legacy hash to the canonical one on load 3335 // and on hashchange. DELETE this block only once those five call 3336 // sites are migrated and deployed (grep gk-sc-signin across repos). 3337 (function () { 3338 var LEGACY = '#gk-sc-signin', CANON = '#signin'; 3339 function alias() { 3340 if (location.hash === LEGACY) { 3341 // replaceState first so the legacy spelling does not linger in 3342 // history â otherwise Back lands on a hash that matches nothing. 3343 try { history.replaceState(null, '', location.pathname + location.search); } catch (e) {} 3344 location.hash = CANON; 3345 } 3346 } 3347 alias(); 3348 window.addEventListener('hashchange', alias); 3349 })(); 3350 // Single-source-of-truth theme sync: stamp #signin[data-theme=â¦] 3351 // from the active theme so every CSS toggle (palette, logo variant, 3352 // Google button host) reads the modal's own attr â not <html>'s. 3353 // Reads html[data-theme] â body[data-theme] â prefers-color-scheme. 3354 // Re-syncs on data-theme attribute changes ONLY (not class changes â 3355 // those can incidentally toggle for unrelated reasons, flipping the 3356 // theme). Only writes when the resolved value actually changes so 3357 // we don't keep firing our own observer. 3358 (function () { 3359 function _resolvedTheme() { 3360 var t = document.documentElement.getAttribute('data-theme'); 3361 if (t === 'dark' || t === 'light') return t; 3362 if (document.body) { 3363 var b = document.body.getAttribute('data-theme'); 3364 if (b === 'dark' || b === 'light') return b; 3365 } 3366 return (window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light'; 3367 } 3368 function _sync() { 3369 var m = document.getElementById('signin'); 3370 if (!m) return; 3371 var next = _resolvedTheme(); 3372 if (m.getAttribute('data-theme') !== next) m.setAttribute('data-theme', next); 3373 } 3374 _sync(); 3375 try { 3376 new MutationObserver(_sync).observe(document.documentElement, 3377 {attributes: true, attributeFilter: ['data-theme']}); 3378 if (document.body) { 3379 new MutationObserver(_sync).observe(document.body, 3380 {attributes: true, attributeFilter: ['data-theme']}); 3381 } 3382 if (window.matchMedia) { 3383 matchMedia('(prefers-color-scheme: dark)').addEventListener('change', _sync); 3384 } 3385 } catch (_) {} 3386 document.addEventListener('DOMContentLoaded', _sync); 3387 })(); 3388 window.gkScSubmit = function (ev) { 3389 if (ev && ev.preventDefault) ev.preventDefault(); 3390 var C = window.GK_SC_CONFIG || {}; 3391 var input = document.getElementById('gk-sc-email'); 3392 var pwInput = document.getElementById('gk-sc-pwd'); 3393 var errEl = document.getElementById('gk-sc-err'); 3394 var email = (input && input.value || '').trim(); 3395 var password = (pwInput && pwInput.value || ''); 3396 if (!email || !password) { 3397 if (errEl) { errEl.textContent = C.strings.both_required; errEl.hidden = false; } 3398 return false;
3399 } 3400 if (errEl) { errEl.textContent = ''; errEl.hidden = true; } 3401 fetch(C.password_endpoint, { 3402 method: 'POST', credentials: 'same-origin', 3403 headers: {'content-type': 'application/json'}, 3404 body: JSON.stringify({email: email, password: password}) 3405 }) 3406 .then(function (r) { return r.json().then(function (d) { return {ok: r.ok, body: d}; }); }) 3407 .then(function (res) { 3408 if (res.ok && res.body && res.body.ok) { 3409 window.location.href = C.redirect; 3410 return; 3411 } 3412 if (errEl) { 3413 var body = res.body || {}; 3414 errEl.textContent = body.error || C.strings.signin_fail; 3415 errEl.hidden = false; 3416 if (body.cta_label && body.cta_url) { 3417 var br = document.createElement('br'); 3418 var a = document.createElement('a'); 3419 a.href = body.cta_url; 3420 a.textContent = body.cta_label; 3421 a.style.cssText = 'display:inline-block;margin-top:6px;color:inherit;text-decoration:underline;font-weight:600'; 3422 errEl.appendChild(br); 3423 errEl.appendChild(a); 3424 } 3425 } 3426 }) 3427 .catch(function () { 3428 if (errEl) { errEl.textContent = C.strings.network_err; errEl.hidden = false; } 3429 }); 3430 return false; 3431 }; 3432 (function () { 3433 var C = window.GK_SC_CONFIG || {}; 3434 var input = document.getElementById('gk-sc-email'); 3435 var magicBtn = document.getElementById('gk-sc-magic-btn'); 3436 var magicSent = document.getElementById('gk-sc-magic-sent'); 3437 var errEl = document.getElementById('gk-sc-err'); 3438 // 2026-06-30 â password field is now always visible (no pwWrap / 3439 // pwToggle); keep pwInput for the eye-toggle button + submit handler. 3440 var pwInput = document.getElementById('gk-sc-pwd'); 3441 if (!input) return; 3442 // Hijack the shared widget's anon signin triggers so they open THIS 3443 // Shape C modal instead of the (non-existent-on-this-page) shared 3444 // #gk-connect-overlay. gateka-core's _header_auth_menu.html emits 3445 // <button data-gk-connect-open> for anon â that button clicked in 3446 // connect.js would call openModal() which looks up an overlay that 3447 // isn't mounted here. Redirect the click at capture phase so we 3448 // beat connect.js's document-level listener. Only fires when the 3449 // Shape C modal is actually present in the DOM (this script only 3450 // runs when the partial renders). 3451 document.addEventListener('click', function (ev) { 3452 var t = ev.target && ev.target.closest && ev.target.closest('[data-gk-connect-open]'); 3453 if (!t) return; 3454 // Skip if a widget overlay is ALSO present â some future page may 3455 // render both; let connect.js handle the widget in that case. 3456 if (document.getElementById('gk-connect-overlay')) return; 3457 ev.preventDefault(); 3458 ev.stopPropagation(); 3459 if (location.hash === '#signin') { 3460 // Force a re-fire so :target CSS reveals the modal even if the 3461 // hash was already set (e.g. from a prior open+backdrop-close). 3462 location.hash = ''; 3463 setTimeout(function () { location.hash = '#signin'; }, 0); 3464 } else { 3465 location.hash = '#signin'; 3466 } 3467 }, true); 3468 function looksLikeEmail(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); } 3469 // Magic-link primary submit 3470 if (magicBtn) { 3471 magicBtn.addEventListener('click', function () { 3472 var email = (input.value || '').trim().toLowerCase(); 3473 if (!looksLikeEmail(email)) { 3474 if (errEl) { errEl.textContent = C.strings.magic_invalid; errEl.hidden = false; } 3475 input.focus(); 3476 return; 3477 } 3478 magicBtn.disabled = true; magicBtn.style.opacity = '0.6'; 3479 if (errEl) errEl.hidden = true; 3480 fetch(C.magic_endpoint, { 3481 method: 'POST', credentials: 'same-origin', 3482 headers: {'content-type': 'application/json'}, 3483 body: JSON.stringify({email: email}) 3484 }).finally(function () { 3485 magicBtn.style.display = 'none'; 3486 if (magicSent) magicSent.hidden = false; 3487 }); 3488 }); 3489 } 3490 // 2026-06-30 (Bogue: "GLOBAL, magic link should still be there") â 3491 // password toggle logic retired. Password field is now visible from 3492 // first paint; magic-link becomes a secondary CTA below Login (still 3493 // wired to gk-sc-magic-btn, unchanged). No JS needed to reveal 3494 // anything â the form is what you see. 3495 // Google Identity Services init â same pattern as v1 Shape C 3496 function _initGis() { 3497 var CID = C.google_client_id; 3498 if (!CID || !window.google || !google.accounts || !google.accounts.id) { 3499 var wrap0 = document.getElementById('gk-sc-google-wrap'); 3500 if (wrap0) wrap0.hidden = true; 3501 return; 3502 } 3503 var hostLight = document.getElementById('gk-sc-google-btn-l
3503ight'); 3504 var hostDark = document.getElementById('gk-sc-google-btn-dark'); 3505 if (!hostLight || !hostDark) { 3506 var wrap0 = document.getElementById('gk-sc-google-wrap'); 3507 if (wrap0) wrap0.hidden = true; 3508 return; 3509 } 3510 try { 3511 google.accounts.id.initialize({ 3512 client_id: CID, 3513 callback: function (credential) { 3514 if (!credential || !credential.credential) return; 3515 fetch(C.google_endpoint, { 3516 method: 'POST', credentials: 'same-origin', 3517 headers: {'content-type': 'application/json'}, 3518 body: JSON.stringify({id_token: credential.credential}) 3519 }) 3520 .then(function (r) { return r.json().then(function(d){return {ok:r.ok, body:d};}); }) 3521 .then(function (res) { 3522 if (res.ok && res.body && (res.body.ok || res.body.status === 'ok')) { 3523 window.location.href = C.redirect; 3524 } else if (errEl) { 3525 var body = res.body || {}; 3526 errEl.textContent = body.error || C.strings.google_fail; 3527 errEl.hidden = false; 3528 if (body.cta_label && body.cta_url) { 3529 var br = document.createElement('br'); 3530 var a = document.createElement('a'); 3531 a.href = body.cta_url; a.textContent = body.cta_label; 3532 a.style.cssText = 'display:inline-block;margin-top:6px;color:inherit;text-decoration:underline;font-weight:600'; 3533 errEl.appendChild(br); errEl.appendChild(a); 3534 } 3535 } 3536 }) 3537 .catch(function () { 3538 if (errEl) { errEl.textContent = C.strings.network_err; errEl.hidden = false; } 3539 }); 3540 } 3541 }); 3542 // Zero-detection theme handling: render TWO buttons (light + 3543 // dark themes) into their own hosts; CSS toggles which one is 3544 // visible based on html[data-theme=â¦] + prefers-color-scheme. 3545 // The invisible host is display:none â takes no layout, no 3546 // click surface. Composer's google_button.theme (if pinned) 3547 // wins on both hosts. 3548 var baseOpts = Object.assign( 3549 {size: 'large', type: 'standard'}, 3550 C.google_button || {} 3551 ); 3552 var lightOpts = Object.assign({}, baseOpts, 3553 {theme: baseOpts.theme || (C.google_theme_light || 'outline')}); 3554 var darkOpts = Object.assign({}, baseOpts, 3555 {theme: baseOpts.theme || (C.google_theme_dark || 'filled_black')}); 3556 google.accounts.id.renderButton(hostLight, lightOpts); 3557 google.accounts.id.renderButton(hostDark, darkOpts); 3558 var wrapx = document.getElementById('gk-sc-google-wrap'); 3559 if (wrapx) wrapx.hidden = false; 3560 } catch (e) { 3561 var wrap2 = document.getElementById('gk-sc-google-wrap'); 3562 if (wrap2) wrap2.hidden = true; 3563 } 3564 } 3565 (function _pollGis() { 3566 var tries = 0; 3567 var iv = setInterval(function () { 3568 tries++; 3569 if (window.google && google.accounts && google.accounts.id) { 3570 clearInterval(iv); _initGis(); 3571 } else if (tries > 40) { 3572 clearInterval(iv); 3573 var wrap = document.getElementById('gk-sc-google-wrap'); 3574 if (wrap) wrap.hidden = true; 3575 } 3576 }, 100); 3577 })(); 3578 3579 // ââ Microsoft (Entra ID) ââââââââââââââââââââââââââââââââââââââââââ 3580 // Popup â id_token â POST /connect/microsoft. redirectUri is a BLANK 3581 // page, never the site root: Entra lands the popup there carrying 3582 // `#code=â¦`, and a real page competes for that fragment (Lorvert's 3583 // landing drives its modals off the hash) besides being slow to load 3584 // inside a popup â which made sign-in fail silently. 3585 function _initMsal() { 3586 var btn = document.getElementById('gk-sc-microsoft-btn'); 3587 if (!btn) return; 3588 var MSID = C.microsoft_client_id; 3589 if (!MSID || !window.msal || !msal.PublicClientApplication) return; 3590 var app;
3591 try { 3592 app = new msal.PublicClientApplication({ 3593 auth: { 3594 clientId: MSID, 3595 // /common/ admits work, school AND personal. Which of those 3596 // the app really accepts is the registration's signInAudience, 3597 // decided server-side â not here. 3598 authority: 'https://login.microsoftonline.com/common', 3599 redirectUri: window.location.origin + C.microsoft_redirect_uri, 3600 }, 3601 cache: {cacheLocation: 'sessionStorage'} 3602 }); 3603 } catch (e) { return; } 3604 btn.hidden = false; 3605 // The wrap is shared with Google and starts hidden. Microsoft alone 3606 // must still reveal it, or a product with only Microsoft configured 3607 // renders an invisible button. 3608 var wrapm = document.getElementById('gk-sc-google-wrap'); 3609 if (wrapm) wrapm.hidden = false; 3610 btn.addEventListener('click', function () { 3611 btn.disabled = true; 3612 Promise.resolve(app.initialize ? app.initialize() : null) 3613 .then(function () { 3614 // `email` must be REQUESTED here, not merely granted in the 3615 // portal â the claim is omitted from the token otherwise. 3616 return app.loginPopup({scopes: ['openid', 'profile', 'email'], 3617 prompt: 'select_account'}); 3618 }) 3619 .then(function (res) { 3620 if (!res || !res.idToken) throw new Error('no id_token'); 3621 return fetch(C.microsoft_endpoint, { 3622 method: 'POST', credentials: 'same-origin', 3623 headers: {'content-type': 'application/json'}, 3624 body: JSON.stringify({id_token: res.idToken}) 3625 }); 3626 }) 3627 .then(function (r) { return r.json().then(function (d) { return {ok: r.ok, body: d}; }); }) 3628 .then(function (res) { 3629 if (res.ok && res.body && (res.body.ok || res.body.status === 'ok')) { 3630 window.location.href = C.redirect; return; 3631 } 3632 if (errEl) { 3633 var body = res.body || {}; 3634 errEl.textContent = body.error || C.strings.google_fail; 3635 errEl.hidden = false; 3636 // Skipped when the refusal carries an oid â that message 3637 // already tells the visitor what to send an admin, and a 3638 // "create an account" link would send them round a loop 3639 // they cannot finish. 3640 if (body.cta_label && body.cta_url && !body.oid) { 3641 var br = document.createElement('br'); 3642 var a = document.createElement('a'); 3643 a.href = body.cta_url; a.textContent = body.cta_label; 3644 a.style.cssText = 'display:inline-block;margin-top:6px;color:inherit;text-decoration:underline;font-weight:600'; 3645 errEl.appendChild(br); errEl.appendChild(a); 3646 } 3647 } 3648 }) 3649 .catch(function (e) { 3650 var code = String((e && (e.errorCode || e.message)) || ''); 3651 // Only a deliberate close is silent. Swallowing popup_window* 3652 // too was the bug that hid a blocked popup AND a redirect-URI 3653 // mismatch behind an empty screen on Lorvert. 3654 if (code.indexOf('user_cancelled') !== -1) return; 3655 console.error('[msal]', code, e); 3656 if (errEl) { errEl.textContent = C.strings.network_err; errEl.hidden = false; } 3657 }) 3658 .then(function () { btn.disabled = false; }); 3659 }); 3660 } 3661 (function _pollMsal() { 3662 var tries = 0; 3663 var iv = setInterval(function () { 3664 tries++; 3665 if (window.msal && msal.PublicClientApplication) { clearInterval(iv); _initMsal(); } 3666 else if (tries > 40) { clearInterval(iv); } 3667 }, 100); 3668 })(); 3669 })(); 3670</script>
3670 3671 3672 3673 3674 3675 3676 3677 3678 3679 3680 3681 3682 3683 3684 3685 3686 3687 3688 3689 3690 3691 3692 3693 3694 3695 3696 3697
3697<script defer src="/gateka/static/edit-in-place.js?v=prod-20260926-a"></script>
3697 3698 3699 3700 3701
3702<script src="/api/editorial/zone-edit-stub.js?product=Lorvert-paris&v=prod-20260926-a" defer></script>
3702 3703</body> 3704</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.