<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>[ia] Design génératif - Conceptions de contenus 2D / 3D / Espaces</title>
	<atom:link href="https://presentcomposedesign.fr/category/ia/feed/" rel="self" type="application/rss+xml" />
	<link>https://presentcomposedesign.fr/category/ia/</link>
	<description>Direction Artistique, concepts &#38; innovations, Design 2D/3D/ia/AR/VR</description>
	<lastBuildDate>Mon, 11 May 2026 21:46:47 +0000</lastBuildDate>
	<language>fr-FR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0</generator>

<image>
	<url>https://presentcomposedesign.fr/wp-content/uploads/2025/07/cropped-logo_PCd_2025-512-32x32.png</url>
	<title>[ia] Design génératif - Conceptions de contenus 2D / 3D / Espaces</title>
	<link>https://presentcomposedesign.fr/category/ia/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>//*</title>
		<link>https://presentcomposedesign.fr/36446-2/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Sun, 19 Apr 2026 23:40:28 +0000</pubDate>
				<category><![CDATA[[ia]]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=36446</guid>

					<description><![CDATA[<p>Préparation VR confortable WebGPU non disponible Ce microcosme interactif nécessite l’API WebGPU, qui n’est pas prise en charge par votre navigateur actuel. Essayez d’utiliser Google Chrome 113+ ou Microsoft Edge 113+ sur bureau. Sous macOS, essayez également Safari Technology Preview. ☰ ☾ &#8211;:&#8211; &#8212; AUTO Affiner &#8211;° · microclimat Vent &#8212; · humidité &#8212; Temp.&#8211;°C Vent&#8212; Humidité&#8212; Microclimat en observation Arrosage à évaluer Jours sans pluie : &#8212; ☾ Lune en calcul Phase · mouvement · type lunaire OK GPS ↻ Microclimat local &#8212; &#8212; &#8212; &#8212; &#8212; &#8212; &#8212; &#8212; &#8212; Organic Control Island Cycle vivant Jour 000/365 · graine en éveil Mémoire navigateur auto Arroser mon arbre Rituel volontaire Voir ma carte Carte vivante Déverrouiller la mémoire autonome Mémoire locale : la progression est automatiquement conservée dans ce navigateur. La mémoire HTML autonome est disponible avec l’add-on ou l’arbre à vie. Carte mémoire Microcosm Viewer collector dynamique · clic/tap pour retourner × Voir verso Microcosm Interactif 9 #&#8212;&#8212; Graine en éveil Mémoire vivante 0% Jour000/365 Temp.&#8211;°C Vent&#8212; km/h Humidité&#8211;% Nuages&#8211;% Synchro&#8212; Certificat de graine Certificat vivant Identité de l’arbre Seed public&#8212; Lieu récent&#8212; Générée&#8212; Activité&#8212; Carte mémoire autonome prête à être enregistrée. AR/XRpresentcomposedesign.fr/microcosm-ar Carte HTML autonome Carte mémoire Carte unique : le même HTML autonome sert au viewer et à la sauvegarde payante. Jour&#8212; Tier&#8212; Météo&#8212; Seed public&#8212; La carte flotte ici. En gratuit, elle reste consultable ; “Enregistrer” sert à déverrouiller la mémoire HTML autonome. Fermer options dans le HTML autonome Ouvrir AR/XR Enregistrer Jouer contre vents et marées débloquer l’expérience Ton arbre a accompli son premier cycle. 365 jours : une année complète de croissance. Tu peux conserver cet arbre, créer une card visuelle gratuite, repartir d’une nouvelle graine, ou garder une vraie carte mémoire premium pour lui redonner vie plus tard. Conserver l’arbrele garder comme page d’accueil Créer une card HTML vivantepage autonome partageable Couper et recommencernouvelle graine Continuer à vie · 19,99€carte mémoire illimitée + partage avancé Jouer contre vents et marées Deux modes de jeu existent : gratuit ou à vie. L’extension “vents &#038; marées” est un supplément séparé, activable aussi bien sur le mode gratuit que sur le mode à vie. Elle débloque des outils, artefacts, entretiens supplémentaires et le mode accueil plein écran sans HUD. Gratuit : 1 clic / jour pendant 365 jours À vie : arbre éternel + mémoire illimitée Add-on “vents &#038; marées” : 4,99€ sur gratuit ou à vie Débloque 5 clics/jour au départ + outils + artefacts + mode accueil sans HUD Cartes : 2D originales en gratuit · version glass collector pour les arbres à vie Mode accueil Plein écran sans HUD restaurant · accueil entreprise · sensibilisation “un clic par jour”</p>
<p>Cet article <a href="https://presentcomposedesign.fr/36446-2/">//*</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="36446" class="elementor elementor-36446">
				<div class="elementor-element elementor-element-cbb3427 e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent" data-id="cbb3427" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-d905f2b elementor-widget elementor-widget-html" data-id="d905f2b" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!-- MICROCOSM INTERACTIF 9 · WORDPRESS / ELEMENTOR CUSTOM HTML BLOCK · V8.27 MOBILE GROUND · CARD PARITY · ORGANIC SHORE
Base corrigée depuis la version standalone v8.25.
À coller dans un widget HTML Elementor sur une page Canvas / plein écran.
Patch orienté UX joueur : Organic Control Island, carte autonome en preview, gland au sol, rivage organique, sol mobile plus propre.
Croissance validée préservée : seul “Arroser mon arbre” déclenche la progression.
-->
<style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    :root { --microcosm-weather-stack-bottom: 64px; }

    /* WordPress / theme hard override: Microcosm doit occuper la page entière et ignorer les styles du site. */
    html body #microcosm-app-root,
    html body #microcosm-app-root canvas {
      max-width: none !important;
      max-height: none !important;
      min-width: 0 !important;
      min-height: 0 !important;
      margin: 0 !important;
      padding: 0 !important;
      border: 0 !important;
      box-shadow: none !important;
    }
    html body .elementor,
    html body .elementor-section,
    html body .elementor-container,
    html body .elementor-column,
    html body .elementor-widget,
    html body .elementor-widget-container,
    html body .entry-content,
    html body .site-content,
    html body .content-area,
    html body main.site-main {
      margin: 0 !important;
      padding: 0 !important;
      max-width: none !important;
      width: 100% !important;
      min-height: 0 !important;
      overflow: visible !important;
      background: transparent !important;
    }
    html body.microcosm-immersive-page {
      margin: 0 !important;
      padding: 0 !important;
      overflow: hidden !important;
      background: #0b0a06 !important;
    }

    html, body {
      width: 100% !important;
      height: 100% !important;
      min-height: 100% !important;
      margin: 0 !important;
      padding: 0 !important;
      overflow: hidden !important;
      background: linear-gradient(180deg, #8fd3ff 0%, #cdefff 55%, #ffe0a8 100%) !important;
    }
    body.elementor-page,
    body.page-template-elementor_canvas,
    body.page-template-elementor_header_footer {
      overflow: hidden !important;
    }
    .elementor-location-header,
    .elementor-location-footer,
    .site-header,
    .site-footer,
    header.site-header,
    footer.site-footer {
      display: none !important;
    }

    /* WordPress / Elementor hard override : la page Microcosm doit écraser le thème hôte. */
    html:has(#microcosm-app-root),
    body:has(#microcosm-app-root) {
      margin: 0 !important;
      padding: 0 !important;
      width: 100vw !important;
      min-width: 100vw !important;
      height: 100dvh !important;
      min-height: 100dvh !important;
      overflow: hidden !important;
      background: #090704 !important;
    }
    .elementor-widget-container:has(#microcosm-app-root),
    .elementor-widget-html:has(#microcosm-app-root),
    .entry-content:has(#microcosm-app-root),
    .site-main:has(#microcosm-app-root),
    .page-content:has(#microcosm-app-root) {
      margin: 0 !important;
      padding: 0 !important;
      width: 100vw !important;
      max-width: none !important;
      height: 100dvh !important;
      overflow: hidden !important;
      background: transparent !important;
    }

    #microcosm-app-root {
      position: fixed !important;
      inset: 0 !important;
      width: 100vw !important;
      height: 100dvh !important;
      overflow: hidden !important;
      z-index: 0 !important;
      background: #9edcff;
      isolation: isolate;
    }
    canvas {
      display: block;
      position: fixed !important;
      inset: 0 !important;
      width: 100vw !important;
      height: 100dvh !important;
      z-index: 1;
      touch-action: none;
    }
    /* panneaux d’interface superposés */
    #no-webgpu {
      display: none;
      position: fixed;
      inset: 0;
      background: #0a1628;
      color: #7ec8e3;
      font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
      z-index: 9999;
      justify-content: center;
      align-items: center;
      flex-direction: column;
      text-align: center;
      padding: 40px;
    }
    #no-webgpu h1 { font-size: 28px; margin-bottom: 16px; color: #fff; }
    #no-webgpu p { font-size: 16px; line-height: 1.7; max-width: 520px; opacity: 0.8; }
    #no-webgpu .hint { margin-top: 24px; padding: 16px 24px; background: rgba(126,200,227,0.08); border: 1px solid rgba(126,200,227,0.2); border-radius: 8px; font-size: 14px; }
    /* panneau UI océan */
    #ui-toggle {
      position: fixed;
      top: 20px;
      right: 20px;
      z-index: 1003;
      width: 42px;
      height: 42px;
      border: 1px solid rgba(216, 171, 97, 0.26);
      background: linear-gradient(145deg, rgba(27, 20, 12, 0.66), rgba(52, 39, 21, 0.42));
      box-shadow: 0 14px 34px rgba(7, 10, 5, 0.22), inset 0 1px 0 rgba(255, 238, 193, 0.14);
      backdrop-filter: blur(22px) saturate(128%);
      -webkit-backdrop-filter: blur(22px) saturate(128%);
      border-radius: 14px;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: all 0.3s ease;
      color: rgba(255,255,255,0.86);
      font-size: 16px;
    }
    #ui-toggle:hover {
      background: linear-gradient(145deg, rgba(52, 39, 21, 0.72), rgba(79, 110, 38, 0.34));
      border-color: rgba(232, 190, 113, 0.34);
      color: rgba(255,255,255,0.98);
      transform: translateY(-1px);
    }
    #ui-toggle.active {
      border-color: rgba(232, 190, 113, 0.40);
      box-shadow: 0 14px 34px rgba(7, 10, 5, 0.24), inset 0 1px 0 rgba(255, 238, 193, 0.16), 0 0 0 1px rgba(210, 183, 111, 0.10);
    }
    #ui-panel {
      position: fixed;
      top: 20px;
      right: 70px;
      z-index: 1002;
      width: clamp(300px, 26vw, 360px);
      max-height: calc(100dvh - 40px);
      background:
        radial-gradient(circle at 12% 0%, rgba(238, 196, 116, 0.12), transparent 36%),
        linear-gradient(145deg, rgba(27, 20, 12, 0.78), rgba(52, 39, 21, 0.60));
      backdrop-filter: blur(28px) saturate(128%);
      -webkit-backdrop-filter: blur(28px) saturate(128%);
      border: 1px solid rgba(216, 171, 97, 0.22);
      box-shadow: 0 26px 58px rgba(7, 10, 5, 0.28), inset 0 1px 0 rgba(255, 238, 193, 0.14);
      border-radius: 20px;
      font-family: 'Inter', -apple-system, sans-serif;
      color: rgba(255, 248, 225, 0.88);
      overflow-y: auto;
      overflow-x: hidden;
      transform: translateX(12px) scale(0.985);
      opacity: 0;
      pointer-events: none;
      transition: width 0.28s ease, transform 0.35s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.35s ease, box-shadow 0.28s ease;
      scrollbar-width: thin;
      scrollbar-color: rgba(255,255,255,0.16) transparent;
    }
    #ui-panel::-webkit-scrollbar { width: 4px; }
    #ui-panel::-webkit-scrollbar-track { background: transparent; }
    #ui-panel::-webkit-scrollbar-thumb {
      background: rgba(255,255,255,0.12);
      border-radius: 4px;
    }
    #ui-panel::-webkit-scrollbar-thumb:hover {
      background: rgba(255,255,255,0.25);
    }
    #ui-panel.open {
      transform: translateX(0) scale(1);
      opacity: 1;
      pointer-events: all;
    }
    #ui-panel.open:hover,
    #ui-panel.open:focus-within {
      width: clamp(360px, 42vw, 560px);
      box-shadow: 0 32px 72px rgba(7, 10, 5, 0.34), inset 0 1px 0 rgba(255, 238, 193, 0.18);
    }
    #ui-panel.open:hover .ui-section-body,
    #ui-panel.open:focus-within .ui-section-body {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 4px 10px;
      align-items: start;
    }
    #ui-panel.open:hover .ui-section.dev-wide .ui-section-body,
    #ui-panel.open:focus-within .ui-section.dev-wide .ui-section-body {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    #ui-panel.open:hover .ui-section.product-section .ui-section-body,
    #ui-panel.open:focus-within .ui-section.product-section .ui-section-body {
      grid-template-columns: 1fr;
    }
    #ui-panel.open:hover .ui-row,
    #ui-panel.open:focus-within .ui-row {
      min-width: 0;
    }
    .ui-section {
      padding: 0 18px;
      border-bottom: 1px solid rgba(216, 171, 97, 0.10);
    }
    .ui-section:last-child { border-bottom: none; }
    .ui-section-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 12px 0 8px;
      cursor: pointer;
      user-select: none;
    }
    .ui-section-title {
      font-size: 10px;
      font-weight: 700;
      letter-spacing: 1.25px;
      text-transform: uppercase;
      color: rgba(255, 248, 225, 0.88);
    }
    .ui-section-arrow {
      font-size: 8px;
      color: rgba(255,255,255,0.2);
      transition: transform 0.25s ease;
    }
    .ui-section.collapsed .ui-section-arrow { transform: rotate(-90deg); }
    .ui-section-body {
      overflow: hidden;
      max-height: 600px;
      transition: max-height 0.3s ease, opacity 0.25s ease;
      padding-bottom: 10px;
    }
    .ui-section.collapsed .ui-section-body {
      max-height: 0;
      opacity: 0;
      padding-bottom: 0;
    }
    .ui-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      height: 28px;
      margin-bottom: 2px;
    }
    .ui-label {
      font-size: 11px;
      font-weight: 500;
      color: rgba(226, 218, 188, 0.70);
      white-space: nowrap;
      flex-shrink: 0;
      margin-right: 10px;
    }
    .ui-slider-wrap {
      flex: 1;
      display: flex;
      align-items: center;
      gap: 6px;
      min-width: 0;
    }
    .ui-slider {
      flex: 1;
      -webkit-appearance: none;
      appearance: none;
      height: 4px;
      background: linear-gradient(90deg, rgba(216, 171, 97, 0.18), rgba(255, 248, 225, 0.08));
      border-radius: 2px;
      outline: none;
      cursor: pointer;
    }
    .ui-slider::-webkit-slider-thumb {
      -webkit-appearance: none;
      width: 14px;
      height: 14px;
      border-radius: 50%;
      background: linear-gradient(180deg, rgba(255, 248, 225, 0.98), rgba(206, 177, 110, 0.82));
      border: 1px solid rgba(255, 238, 193, 0.46);
      box-shadow: 0 4px 10px rgba(11,24,40,0.18);
      cursor: pointer;
      transition: transform 0.15s ease, background 0.15s ease;
    }
    .ui-slider::-webkit-slider-thumb:hover {
      transform: scale(1.3);
      background: #fff;
    }
    .ui-slider::-moz-range-thumb {
      width: 14px;
      height: 14px;
      border-radius: 50%;
      background: linear-gradient(180deg, rgba(255, 248, 225, 0.98), rgba(206, 177, 110, 0.82));
      border: 1px solid rgba(255, 238, 193, 0.46);
      box-shadow: 0 4px 10px rgba(11,24,40,0.18);
      cursor: pointer;
    }
    .ui-val {
      font-size: 10px;
      font-weight: 700;
      color: rgba(255,255,255,0.52);
      min-width: 28px;
      text-align: right;
      font-variant-numeric: tabular-nums;
    }
    .ui-color-wrap {
      position: relative;
      width: 20px;
      height: 20px;
      border-radius: 50%;
      overflow: hidden;
      border: 1px solid rgba(255,255,255,0.1);
      flex-shrink: 0;
    }
    .ui-color-input {
      position: absolute;
      inset: -4px;
      width: 28px;
      height: 28px;
      border: none;
      cursor: pointer;
      opacity: 0;
    }
    .ui-color-swatch {
      width: 100%;
      height: 100%;
      border-radius: 50%;
      pointer-events: none;
    }
    .ui-select {
      background: rgba(255,255,255,0.06);
      border: 1px solid rgba(255,255,255,0.08);
      border-radius: 6px;
      color: rgba(255,255,255,0.65);
      font-size: 10px;
      font-family: inherit;
      padding: 3px 6px;
      outline: none;
      cursor: pointer;
      max-width: 110px;
    }
    .ui-select:focus { border-color: rgba(255,255,255,0.2); }
    .ui-brand {
      padding: 14px 16px 6px;
      font-size: 11px;
      font-weight: 400;
      letter-spacing: 0.5px;
      color: rgba(255,255,255,0.25);
      text-align: center;
    }
    /* panneau herbe (contrôles) */
    .grass-panel {
      position: fixed;
      top: 20px;
      left: 20px;
      z-index: 1000;
      background: rgba(20, 30, 40, 0.7);
      backdrop-filter: blur(10px);
      border: 1px solid rgba(120, 160, 200, 0.2);
      border-radius: 12px;
      font-family: 'Inter', system-ui, -apple-system, sans-serif;
      color: rgba(200, 220, 240, 0.9);
      font-size: 13px;
      padding: 16px 20px;
      display: flex;
      flex-direction: column;
      gap: 12px;
      user-select: none;
    }
    .grass-panel .slider-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
    }
    .grass-panel .slider-row span {
      white-space: nowrap;
      font-weight: 500;
    }
    .grass-panel .slider-row input[type=range] {
      width: 120px;
      height: 4px;
      appearance: none;
      background: rgba(100, 140, 180, 0.3);
      border-radius: 2px;
      outline: none;
      cursor: pointer;
    }
    .grass-panel input[type=range]::-webkit-slider-thumb {
      -webkit-appearance: none;
      width: 14px;
      height: 14px;
      background: rgba(140, 180, 220, 0.9);
      border-radius: 50%;
      cursor: pointer;
      border: 2px solid rgba(200, 220, 240, 0.3);
    }


    /* Interface de croissance Fibonacci */
    .tree-growth-ui {
      position: fixed;
      bottom: 28px;
      left: 50%;
      transform: translateX(-50%);
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 12px;
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      z-index: 1000;
      pointer-events: none;
      color: rgba(255, 248, 225, 0.92);
    }
    .tree-growth-card {
      min-width: 295px;
      padding: 14px 16px 15px;
      border: 1px solid rgba(198, 151, 80, 0.38);
      border-radius: 20px;
      background:
        radial-gradient(circle at 20% 0%, rgba(255, 217, 140, 0.18), transparent 45%),
        linear-gradient(145deg, rgba(27, 18, 10, 0.82), rgba(62, 41, 21, 0.64));
      box-shadow: 0 18px 45px rgba(0, 0, 0, 0.34), inset 0 1px 0 rgba(255, 255, 255, 0.12);
      backdrop-filter: blur(16px);
      -webkit-backdrop-filter: blur(16px);
      pointer-events: all;
    }
    .tree-growth-title {
      font-size: 11px;
      letter-spacing: 0.16em;
      text-transform: uppercase;
      color: rgba(255, 224, 166, 0.7);
      margin-bottom: 6px;
      text-align: center;
    }
    .tree-growth-status {
      font-size: 13px;
      color: rgba(255, 251, 232, 0.92);
      text-align: center;
      margin-bottom: 11px;
      font-variant-numeric: tabular-nums;
    }
    .tree-progress-track {
      width: 100%;
      height: 9px;
      border-radius: 999px;
      overflow: hidden;
      background: rgba(15, 10, 5, 0.55);
      border: 1px solid rgba(255, 226, 174, 0.16);
      box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.45);
    }
    .tree-progress-fill {
      width: 0%;
      height: 100%;
      border-radius: inherit;
      background: linear-gradient(90deg, #6e8f2e 0%, #c79d43 55%, #f4d58a 100%);
      box-shadow: 0 0 14px rgba(244, 213, 138, 0.55);
      transition: width 0.18s ease-out;
    }
    .tree-button-row {
      display: flex;
      gap: 10px;
      justify-content: center;
      margin-top: 13px;
      flex-wrap: wrap;
    }
    .tree-btn {
      border: 1px solid rgba(255, 222, 162, 0.28);
      color: rgba(255, 248, 225, 0.92);
      padding: 10px 16px;
      min-width: 132px;
      font-size: 12px;
      font-weight: 700;
      font-family: inherit;
      border-radius: 999px;
      cursor: pointer;
      letter-spacing: 0.03em;
      background: linear-gradient(145deg, rgba(79, 110, 38, 0.92), rgba(38, 69, 28, 0.88));
      box-shadow: 0 8px 18px rgba(0, 0, 0, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.18);
      transition: transform 0.18s ease, filter 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
    }
    .tree-btn:hover {
      transform: translateY(-2px) scale(1.02);
      filter: brightness(1.12);
      border-color: rgba(255, 235, 190, 0.55);
      box-shadow: 0 12px 24px rgba(0, 0, 0, 0.34), 0 0 18px rgba(205, 174, 92, 0.26);
    }
    .tree-btn:active { transform: translateY(0) scale(0.98); }
    .tree-btn.secondary {
      background: linear-gradient(145deg, rgba(85, 48, 28, 0.86), rgba(42, 26, 18, 0.88));
    }
    .tree-btn:disabled {
      cursor: default;
      opacity: 0.55;
      filter: grayscale(0.2);
      transform: none;
    }


    /* HUD météo allégé : valeurs essentielles uniquement, fond neutralisé. */
    #weather-hud.weather-lite {
      width: min(382px, calc(100vw - 86px));
      padding: 12px 13px;
      border-radius: 20px;
      background: linear-gradient(145deg, rgba(18, 14, 8, 0.42), rgba(37, 29, 16, 0.24));
      border: 1px solid rgba(216, 171, 97, 0.16);
      box-shadow: 0 14px 34px rgba(7, 10, 5, 0.16), inset 0 1px 0 rgba(255, 238, 193, 0.08);
      backdrop-filter: blur(14px) saturate(116%);
      -webkit-backdrop-filter: blur(14px) saturate(116%);
    }
    #weather-hud.weather-lite::after { opacity: 0.18; }
    .weather-lite-top {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 10px;
      position: relative;
      z-index: 1;
      margin-bottom: 9px;
    }
    .weather-lite-clock {
      font-size: 18px;
      line-height: 1;
      font-weight: 900;
      letter-spacing: -0.02em;
      color: rgba(255, 248, 225, 0.96);
    }
    .weather-lite-date {
      margin-top: 4px;
      font-size: 10px;
      font-weight: 700;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: rgba(226, 218, 188, 0.62);
    }
    .weather-lite-source {
      padding: 6px 9px;
      border-radius: 999px;
      border: 1px solid rgba(216, 171, 97, 0.14);
      background: rgba(79, 110, 38, 0.20);
      color: rgba(255, 248, 225, 0.82);
      font-size: 9px;
      font-weight: 850;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      white-space: nowrap;
    }

    /* ─────────────────────────────────────────────────────────────
       CONFIGURABLE UI · météo allégée + module lunaire.
       Ces classes ne touchent pas à la croissance validée de l'arbre.
    ───────────────────────────────────────────────────────────── */
    .weather-lite-mainline {
      display: grid;
      grid-template-columns: 44px 1fr auto;
      align-items: start;
      gap: 10px;
      position: relative;
      z-index: 1;
      margin-bottom: 9px;
    }
    .weather-visual-icon {
      width: 42px;
      height: 42px;
      display: grid;
      place-items: center;
      border-radius: 16px;
      border: 1px solid rgba(216, 171, 97, 0.16);
      background: radial-gradient(circle at 35% 25%, rgba(255,248,225,0.18), rgba(79,110,38,0.10) 64%, rgba(15,10,5,0.34));
      color: rgba(255,248,225,0.96);
      font-size: 24px;
      line-height: 1;
      box-shadow: inset 0 1px 0 rgba(255,238,193,0.10), 0 8px 16px rgba(0,0,0,0.16);
    }
    .weather-lunar-strip {
      position: relative;
      z-index: 1;
      display: grid;
      grid-template-columns: 42px 1fr;
      gap: 10px;
      align-items: center;
      margin-top: 8px;
      padding: 9px 10px;
      border-radius: 16px;
      border: 1px solid rgba(216,171,97,0.10);
      background:
        radial-gradient(circle at 18% 0%, rgba(244,213,138,0.11), transparent 42%),
        rgba(15,10,5,0.22);
      box-shadow: inset 0 1px 0 rgba(255,238,193,0.06);
    }
    .weather-lunar-moon {
      width: 40px;
      height: 40px;
      border-radius: 999px;
      display: grid;
      place-items: center;
      color: rgba(255,248,225,0.96);
      font-size: 24px;
      background: radial-gradient(circle at 35% 34%, rgba(255,240,184,0.92), rgba(143,124,76,0.56) 55%, rgba(15,10,5,0.42) 100%);
      border: 1px solid rgba(255,248,225,0.16);
    }
    .weather-lunar-text strong {
      display: block;
      color: rgba(255,248,225,0.94);
      font-size: 11px;
      font-weight: 900;
      letter-spacing: 0.08em;
      text-transform: uppercase;
    }
    .weather-lunar-text span {
      display: block;
      margin-top: 2px;
      color: rgba(226,218,188,0.68);
      font-size: 10px;
      line-height: 1.32;
    }

    .weather-lite-values {
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 7px;
      position: relative;
      z-index: 1;
    }
    .weather-lite-values > div {
      min-width: 0;
      padding: 8px 9px;
      border-radius: 14px;
      background: rgba(18, 13, 7, 0.26);
      border: 1px solid rgba(216, 171, 97, 0.09);
      box-shadow: inset 0 1px 0 rgba(255, 238, 193, 0.06);
    }
    .weather-lite-values small {
      display: block;
      color: rgba(226, 218, 188, 0.56);
      font-size: 9px;
      font-weight: 700;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      white-space: nowrap;
    }
    .weather-lite-values b {
      display: block;
      margin-top: 2px;
      color: rgba(255, 248, 225, 0.94);
      font-size: 14px;
      font-weight: 900;
      white-space: nowrap;
    }
    .weather-lite-advice {
      position: relative;
      z-index: 1;
      margin-top: 8px;
      padding: 8px 10px;
      border-radius: 14px;
      border: 1px solid rgba(216, 171, 97, 0.10);
      background: rgba(79, 110, 38, 0.14);
      color: rgba(238, 230, 199, 0.76);
      font-size: 10.5px;
      line-height: 1.35;
    }
    .weather-lite-advice b {
      display: block;
      color: rgba(255, 248, 225, 0.94);
      margin-top: 2px;
      font-size: 11px;
      font-weight: 850;
    }
    .weather-rainline {
      position: relative;
      z-index: 1;
      margin-top: 7px;
      color: rgba(226, 218, 188, 0.62);
      font-size: 10px;
      font-weight: 700;
      letter-spacing: 0.04em;
    }
    .weather-rainline b { color: rgba(255, 248, 225, 0.92); }
    .weather-lite-advanced {
      position: relative;
      z-index: 1;
      display: grid;
      grid-template-columns: 1fr auto auto auto;
      gap: 7px;
      margin-top: 8px;
    }
    #weather-hud.compact .weather-lite-advanced { display: none; }
    .weather-lite-hidden { display: none !important; }


    /* HUD microclimat / météo réelle — direction organique glassmorphism */
    #weather-hud {
      position: fixed;
      left: 18px;
      top: 18px;
      z-index: 1001;
      width: min(376px, calc(100vw - 36px));
      color: rgba(255, 248, 225, 0.94);
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      background:
        radial-gradient(circle at 18% 0%, rgba(238, 196, 116, 0.18), transparent 38%),
        radial-gradient(circle at 86% 10%, rgba(118, 145, 63, 0.12), transparent 34%),
        linear-gradient(145deg, rgba(27, 20, 12, 0.72), rgba(52, 39, 21, 0.58));
      border: 1px solid rgba(216, 171, 97, 0.30);
      border-radius: 28px;
      box-shadow:
        0 26px 58px rgba(7, 10, 5, 0.28),
        inset 0 1px 0 rgba(255, 238, 193, 0.16),
        inset 0 -1px 0 rgba(30, 20, 10, 0.28);
      backdrop-filter: blur(28px) saturate(128%);
      -webkit-backdrop-filter: blur(28px) saturate(128%);
      padding: 16px 16px 15px;
      user-select: none;
      pointer-events: auto;
      overflow: hidden;
      transition: opacity 0.25s ease, transform 0.25s ease, box-shadow 0.25s ease;
    }
    #weather-hud::after {
      content: '';
      position: absolute;
      inset: 0;
      border-radius: inherit;
      pointer-events: none;
      background:
        linear-gradient(180deg, rgba(255, 236, 191, 0.12), transparent 27%, transparent 74%, rgba(96, 62, 26, 0.08));
      opacity: 0.78;
    }
    #weather-hud:hover {
      box-shadow:
        0 30px 64px rgba(7, 10, 5, 0.34),
        inset 0 1px 0 rgba(255, 238, 193, 0.19),
        0 0 0 1px rgba(216, 171, 97, 0.08);
    }
    #weather-hud.compact .weather-grid,
    #weather-hud.compact .weather-compare,
    #weather-hud.compact .weather-location-tools {
      display: none;
    }
    .weather-topline,
    .weather-main,
    .weather-location-tools,
    .weather-grid,
    .weather-compare {
      position: relative;
      z-index: 1;
    }
    .weather-topline {
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 8px;
      margin-bottom: 10px;
    }
    .weather-place {
      font-size: 15px;
      font-weight: 800;
      letter-spacing: 0.01em;
      color: rgba(255, 248, 225, 0.94);
    }
    .weather-state {
      font-size: 10px;
      text-transform: uppercase;
      letter-spacing: 0.14em;
      padding: 7px 10px;
      border-radius: 999px;
      background: linear-gradient(145deg, rgba(77, 90, 43, 0.54), rgba(32, 43, 20, 0.46));
      border: 1px solid rgba(185, 205, 102, 0.22);
      box-shadow: inset 0 1px 0 rgba(255, 246, 211, 0.13);
      color: rgba(241, 235, 205, 0.84);
      white-space: nowrap;
    }
    .weather-main {
      display: grid;
      grid-template-columns: 58px 1fr auto;
      align-items: center;
      gap: 12px;
      margin: 8px 0 12px;
      padding: 12px;
      border-radius: 22px;
      background: linear-gradient(145deg, rgba(23, 18, 11, 0.42), rgba(73, 54, 28, 0.28));
      border: 1px solid rgba(216, 171, 97, 0.18);
      box-shadow: inset 0 1px 0 rgba(255, 238, 193, 0.11);
    }
    .weather-sun {
      width: 48px;
      height: 48px;
      border-radius: 999px;
      background: radial-gradient(circle at 35% 30%, rgba(255, 238, 181, 0.95), rgba(176, 126, 54, 0.74) 58%, rgba(87, 62, 31, 0.52));
      border: 1px solid rgba(255, 226, 159, 0.26);
      box-shadow: inset 0 1px 0 rgba(255, 246, 218, 0.34), 0 8px 20px rgba(140, 98, 35, 0.16);
    }
    .weather-temp {
      font-size: 42px;
      line-height: 0.95;
      font-weight: 800;
      letter-spacing: -0.04em;
      color: rgba(255, 248, 225, 0.96);
      text-shadow: 0 1px 0 rgba(255, 232, 170, 0.08);
    }
    .weather-temp small {
      font-size: 18px;
      letter-spacing: 0;
      vertical-align: super;
      margin-left: 3px;
      opacity: 0.78;
    }
    .weather-desc {
      font-size: 17px;
      font-weight: 700;
      margin-top: 5px;
      color: rgba(235, 228, 199, 0.88);
    }
    .weather-refresh {
      border: 1px solid rgba(216, 171, 97, 0.22);
      color: rgba(255, 248, 225, 0.88);
      background: linear-gradient(145deg, rgba(74, 53, 30, 0.42), rgba(22, 17, 10, 0.36));
      box-shadow: inset 0 1px 0 rgba(255, 238, 193, 0.13);
      border-radius: 14px;
      min-width: 44px;
      height: 36px;
      cursor: pointer;
      font-weight: 700;
    }
    .weather-grid {
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      gap: 10px 10px;
      margin-top: 8px;
    }
    .weather-grid > div,
    .weather-location-tools input,
    .weather-location-tools button,
    .save-share-dock .micro-btn,
    #premium-cta,
    #premium-panel {
      backdrop-filter: blur(22px) saturate(128%);
      -webkit-backdrop-filter: blur(22px) saturate(128%);
    }
    .weather-grid > div {
      padding: 10px 10px 9px;
      border-radius: 16px;
      background: linear-gradient(145deg, rgba(23, 18, 11, 0.34), rgba(77, 59, 31, 0.22));
      border: 1px solid rgba(216, 171, 97, 0.13);
      box-shadow: inset 0 1px 0 rgba(255, 238, 193, 0.08);
    }
    .weather-metric-label {
      font-size: 10px;
      font-weight: 600;
      color: rgba(221, 210, 174, 0.58);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .weather-metric-value {
      margin-top: 2px;
      font-size: 14px;
      font-weight: 800;
      white-space: nowrap;
      color: rgba(255, 248, 225, 0.92);
    }
    .weather-compare {
      margin-top: 12px;
      padding-top: 12px;
      border-top: 1px solid rgba(216, 171, 97, 0.12);
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 10px;
      font-size: 11px;
      color: rgba(226, 218, 188, 0.72);
    }
    .weather-compare > div:not(.weather-note) {
      padding: 10px 12px;
      border-radius: 16px;
      background: linear-gradient(145deg, rgba(22, 17, 10, 0.30), rgba(88, 66, 35, 0.16));
      border: 1px solid rgba(216, 171, 97, 0.10);
    }
    .weather-compare b {
      color: rgba(255, 248, 225, 0.94);
      font-weight: 800;
    }
    .weather-note {
      grid-column: 1 / -1;
      color: rgba(221, 210, 174, 0.56);
      font-size: 10px;
      line-height: 1.4;
      padding: 0 2px;
    }
    @media (max-width: 700px) {
      #weather-hud {
        left: 10px;
        top: 10px;
        width: min(330px, calc(100vw - 20px));
        padding: 12px;
        border-radius: 24px;
      }
      .weather-grid { grid-template-columns: repeat(2, 1fr); }
      .weather-temp { font-size: 34px; }
    }

    .weather-location-tools {
      display: grid;
      grid-template-columns: 1fr auto auto;
      gap: 8px;
      margin: 8px 0 10px;
    }
    .weather-location-tools input {
      min-width: 0;
      height: 34px;
      border: 1px solid rgba(216, 171, 97, 0.16);
      border-radius: 14px;
      padding: 0 12px;
      background: linear-gradient(145deg, rgba(23, 18, 11, 0.32), rgba(77, 59, 31, 0.18));
      color: rgba(255, 248, 225, 0.94);
      outline: none;
      font-family: inherit;
      font-size: 12px;
      box-shadow: inset 0 1px 0 rgba(255, 238, 193, 0.11);
    }
    .weather-location-tools input::placeholder {
      color: rgba(221, 210, 174, 0.44);
    }
    .weather-location-tools button,
    .micro-btn {
      height: 34px;
      border: 1px solid rgba(216, 171, 97, 0.18);
      border-radius: 14px;
      color: rgba(255, 248, 225, 0.90);
      background: linear-gradient(145deg, rgba(79, 110, 38, 0.50), rgba(38, 69, 28, 0.36));
      cursor: pointer;
      font-family: inherit;
      font-weight: 700;
      font-size: 11px;
      padding: 0 12px;
      box-shadow: inset 0 1px 0 rgba(255, 238, 193, 0.12);
    }
    .weather-location-tools button:hover,
    .micro-btn:hover {
      background: linear-gradient(145deg, rgba(91, 124, 47, 0.62), rgba(50, 78, 34, 0.44));
      border-color: rgba(232, 190, 113, 0.28);
    }
    #premium-cta {
      position: fixed;
      right: 18px;
      bottom: 22px;
      z-index: 1002;
      border: 1px solid rgba(216, 171, 97, 0.28);
      border-radius: 999px;
      padding: 12px 18px;
      color: rgba(255, 248, 225, 0.94);
      background:
        radial-gradient(circle at 15% 0%, rgba(238, 196, 116, 0.18), transparent 42%),
        linear-gradient(145deg, rgba(31, 23, 13, 0.70), rgba(74, 52, 27, 0.44));
      box-shadow: 0 18px 42px rgba(7, 10, 5, 0.24), inset 0 1px 0 rgba(255, 238, 193, 0.14);
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      font-size: 13px;
      font-weight: 800;
      letter-spacing: 0.02em;
      cursor: pointer;
    }
    #premium-cta small {
      display: block;
      font-size: 9px;
      font-weight: 600;
      opacity: 0.64;
      letter-spacing: 0.10em;
      text-transform: uppercase;
      margin-top: 2px;
      color: rgba(221, 210, 174, 0.72);
    }
    #premium-panel {
      position: fixed;
      right: 18px;
      bottom: 82px;
      z-index: 1002;
      width: min(330px, calc(100vw - 36px));
      display: none;
      padding: 16px;
      border-radius: 24px;
      color: rgba(255, 248, 225, 0.92);
      background: linear-gradient(145deg, rgba(27, 20, 12, 0.76), rgba(52, 39, 21, 0.58));
      border: 1px solid rgba(216, 171, 97, 0.22);
      box-shadow: 0 24px 54px rgba(7, 10, 5, 0.28), inset 0 1px 0 rgba(255, 238, 193, 0.14);
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    }
    #premium-panel.open { display: block; }
    .premium-title {
      font-weight: 850;
      font-size: 16px;
      margin-bottom: 6px;
      color: rgba(255, 248, 225, 0.96);
    }
    .premium-copy {
      font-size: 12px;
      color: rgba(226, 218, 188, 0.72);
      line-height: 1.45;
      margin-bottom: 10px;
    }
    .premium-list {
      display: grid;
      gap: 6px;
      margin-top: 8px;
      font-size: 12px;
      color: rgba(238, 230, 199, 0.82);
    }
    .premium-list span::before {
      content: '✦';
      color: rgba(210, 183, 111, 0.86);
      margin-right: 7px;
    }
    .premium-action-btn {
      width: 100%;
      margin-top: 13px;
      min-height: 48px;
      border: 1px solid rgba(216, 171, 97, 0.26);
      border-radius: 18px;
      color: rgba(255, 248, 225, 0.94);
      background:
        radial-gradient(circle at 16% 0%, rgba(238, 196, 116, 0.20), transparent 42%),
        linear-gradient(145deg, rgba(79, 110, 38, 0.58), rgba(42, 26, 18, 0.46));
      box-shadow: 0 16px 34px rgba(7, 10, 5, 0.22), inset 0 1px 0 rgba(255, 238, 193, 0.14);
      font-family: inherit;
      font-weight: 850;
      font-size: 13px;
      cursor: pointer;
      text-align: left;
      padding: 11px 13px;
    }
    .premium-action-btn:hover {
      background:
        radial-gradient(circle at 16% 0%, rgba(238, 196, 116, 0.28), transparent 42%),
        linear-gradient(145deg, rgba(91, 124, 47, 0.70), rgba(74, 53, 30, 0.54));
      border-color: rgba(232, 190, 113, 0.42);
    }
    .premium-action-btn small {
      display: block;
      margin-top: 3px;
      color: rgba(226, 218, 188, 0.68);
      font-size: 10px;
      font-weight: 650;
      line-height: 1.35;
    }

    .premium-action-group {
      margin-top: 12px;
      padding: 10px;
      border-radius: 20px;
      background: rgba(7, 6, 3, 0.28);
      border: 1px solid rgba(255, 248, 225, 0.08);
    }
    .premium-action-label {
      margin-bottom: 8px;
      color: rgba(226, 218, 188, 0.58);
      font-size: 9px;
      font-weight: 950;
      letter-spacing: 0.16em;
      text-transform: uppercase;
    }
    .save-share-dock {
      position: fixed;
      left: 18px;
      bottom: 22px;
      z-index: 1002;
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
      max-width: min(420px, calc(100vw - 36px));
    }
    .save-share-dock .micro-btn {
      background: linear-gradient(145deg, rgba(27, 20, 12, 0.58), rgba(52, 39, 21, 0.36));
      box-shadow: 0 16px 34px rgba(7, 10, 5, 0.20), inset 0 1px 0 rgba(255, 238, 193, 0.12);
    }

    #storage-warning {
      position: fixed;
      left: 18px;
      bottom: 76px;
      z-index: 1002;
      max-width: min(460px, calc(100vw - 36px));
      padding: 11px 13px;
      border-radius: 16px;
      border: 1px solid rgba(216, 171, 97, 0.18);
      color: rgba(255, 248, 225, 0.86);
      background: linear-gradient(145deg, rgba(27, 20, 12, 0.62), rgba(52, 39, 21, 0.44));
      box-shadow: 0 14px 34px rgba(7, 10, 5, 0.18), inset 0 1px 0 rgba(255, 238, 193, 0.10);
      backdrop-filter: blur(18px) saturate(125%);
      -webkit-backdrop-filter: blur(18px) saturate(125%);
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      font-size: 10.5px;
      line-height: 1.45;
      user-select: none;
    }
    #storage-warning strong {
      color: rgba(255, 248, 225, 0.96);
      font-weight: 850;
    }

    .ui-text-input {
      width: 100%;
      height: 30px;
      min-width: 0;
      border: 1px solid rgba(216, 171, 97, 0.16);
      border-radius: 10px;
      background: rgba(24, 18, 10, 0.28);
      color: rgba(255, 248, 225, 0.88);
      padding: 0 10px;
      font-size: 10px;
      outline: none;
      font-family: inherit;
      box-shadow: inset 0 1px 0 rgba(255, 238, 193, 0.10);
    }
    .ui-button {
      min-height: 28px;
      border: 1px solid rgba(216, 171, 97, 0.16);
      border-radius: 10px;
      background: linear-gradient(145deg, rgba(27, 20, 12, 0.44), rgba(79, 110, 38, 0.18));
      color: rgba(255, 248, 225, 0.90);
      font-size: 10px;
      font-family: inherit;
      cursor: pointer;
      padding: 4px 10px;
      box-shadow: inset 0 1px 0 rgba(255, 238, 193, 0.10);
    }
    .ui-button:hover {
      background: linear-gradient(145deg, rgba(42, 30, 16, 0.52), rgba(79, 110, 38, 0.28));
    }

    .microcosm-avatar-note {
      display: block;
      margin-top: 4px;
      color: rgba(226, 218, 188, 0.58);
      font-size: 9px;
      line-height: 1.25;
      letter-spacing: 0.02em;
    }

    /* Console de réglages responsive : compacte au repos, large au hover/focus pour dev/premium */
    .ui-section.product-section .ui-section-body {
      display: grid;
      gap: 8px;
    }
    .ui-info-card {
      width: 100%;
      padding: 10px 12px;
      border-radius: 14px;
      border: 1px solid rgba(216, 171, 97, 0.14);
      background: linear-gradient(145deg, rgba(23, 18, 11, 0.34), rgba(77, 59, 31, 0.18));
      color: rgba(226, 218, 188, 0.78);
      font-size: 10.5px;
      line-height: 1.42;
      box-shadow: inset 0 1px 0 rgba(255, 238, 193, 0.08);
    }
    .ui-info-card strong {
      color: rgba(255, 248, 225, 0.94);
      font-weight: 800;
    }
    .ui-pill-row {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      margin-top: 7px;
    }
    .ui-pill {
      border-radius: 999px;
      padding: 5px 8px;
      border: 1px solid rgba(216, 171, 97, 0.14);
      background: rgba(79, 110, 38, 0.20);
      color: rgba(238, 230, 199, 0.78);
      font-size: 9.5px;
      letter-spacing: 0.02em;
    }
    #cycle-modal {
      position: fixed;
      inset: 0;
      display: none;
      z-index: 3000;
      align-items: center;
      justify-content: center;
      padding: 22px;
      background: rgba(5, 7, 4, 0.28);
      backdrop-filter: blur(14px) saturate(110%);
      -webkit-backdrop-filter: blur(14px) saturate(110%);
    }
    #cycle-modal.open { display: flex; }
    .cycle-card {
      width: min(560px, calc(100vw - 28px));
      border-radius: 30px;
      padding: 22px;
      color: rgba(255, 248, 225, 0.94);
      background:
        radial-gradient(circle at 20% 0%, rgba(238, 196, 116, 0.20), transparent 38%),
        linear-gradient(145deg, rgba(27, 20, 12, 0.86), rgba(52, 39, 21, 0.70));
      border: 1px solid rgba(216, 171, 97, 0.32);
      box-shadow: 0 28px 70px rgba(5, 7, 4, 0.38), inset 0 1px 0 rgba(255, 238, 193, 0.16);
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    }
    .cycle-card h2 {
      margin: 0 0 8px;
      font-size: 22px;
      line-height: 1.15;
      letter-spacing: -0.02em;
    }
    .cycle-card p {
      margin: 0 0 14px;
      color: rgba(226, 218, 188, 0.76);
      line-height: 1.5;
      font-size: 13px;
    }
    .cycle-actions {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 10px;
      margin-top: 14px;
    }
    .cycle-actions button {
      min-height: 42px;
      border: 1px solid rgba(216, 171, 97, 0.20);
      border-radius: 16px;
      background: linear-gradient(145deg, rgba(79, 110, 38, 0.44), rgba(38, 69, 28, 0.30));
      color: rgba(255, 248, 225, 0.92);
      font-weight: 800;
      font-family: inherit;
      cursor: pointer;
      padding: 10px 12px;
      text-align: left;
    }
    .cycle-actions button.secondary {
      background: linear-gradient(145deg, rgba(74, 53, 30, 0.46), rgba(22, 17, 10, 0.36));
    }
    .cycle-actions button.premium {
      grid-column: 1 / -1;
      background:
        radial-gradient(circle at 12% 0%, rgba(238, 196, 116, 0.18), transparent 42%),
        linear-gradient(145deg, rgba(79, 110, 38, 0.54), rgba(74, 53, 30, 0.42));
    }
    @media (max-width: 900px) {
      #ui-toggle {
        top: 12px;
        right: 12px;
      }
      #ui-panel,
      #ui-panel.open,
      #ui-panel.open:hover,
      #ui-panel.open:focus-within {
        top: var(--microcosm-weather-stack-bottom, 64px);
        right: 10px;
        left: 10px;
        width: auto;
        max-height: calc(100dvh - var(--microcosm-weather-stack-bottom, 64px) - 10px);
        border-radius: 22px;
      }
      #ui-panel.open:hover .ui-section-body,
      #ui-panel.open:focus-within .ui-section-body {
        grid-template-columns: 1fr;
      }
      #weather-hud {
        left: 10px;
        top: 10px;
        width: min(340px, calc(100vw - 74px));
      }
      .save-share-dock {
        left: 10px;
        bottom: 12px;
      }

      #storage-warning {
        left: 10px;
        right: 10px;
        bottom: 64px;
        max-width: none;
      }

      #premium-cta {
        right: 10px;
        bottom: 12px;
      }
    }


    #weather-hud.weather-lite .weather-lite-advanced.weather-location-tools {
      display: grid;
      grid-template-columns: 1fr auto auto auto;
      gap: 7px;
      margin: 8px 0 0;
    }
    #weather-hud.weather-lite.compact .weather-lite-advanced.weather-location-tools { display: none; }
    #weather-hud.weather-lite .weather-lite-advanced input { height: 30px; font-size: 11px; }
    #weather-hud.weather-lite .weather-lite-advanced button { height: 30px; min-width: 34px; padding: 0 9px; }

    /* Empêche les CTA / HUD de se superposer sur les écrans courants. */
    .save-share-dock {
      left: 18px;
      bottom: 16px;
      max-width: min(560px, calc(100vw - 236px));
      padding-right: 0;
    }
    #premium-cta {
      right: 18px;
      bottom: 16px;
      max-width: 210px;
      text-align: center;
    }
    .tree-growth-ui {
      bottom: 86px;
    }
    #storage-warning {
      left: 18px;
      bottom: 62px;
      max-width: min(560px, calc(100vw - 236px));
      opacity: 0.72;
      transform-origin: left bottom;
      transition: opacity 0.2s ease, transform 0.2s ease;
    }
    #storage-warning:hover {
      opacity: 1;
      transform: translateY(-1px);
    }
    @media (max-width: 980px) {
      .save-share-dock {
        left: 10px;
        right: 10px;
        bottom: 72px;
        max-width: none;
      }
      #premium-cta {
        left: 10px;
        right: 10px;
        bottom: 10px;
        max-width: none;
      }
      #storage-warning {
        left: 10px;
        right: 10px;
        bottom: 128px;
        max-width: none;
      }
      .tree-growth-ui { bottom: 160px; }
    }
    @media (max-width: 560px) {
      #weather-hud.weather-lite {
        top: 62px;
        left: 10px;
        width: calc(100vw - 20px);
      }
      .save-share-dock {
        display: grid;
        grid-template-columns: 1fr 1fr;
        bottom: 72px;
      }
      .save-share-dock .micro-btn { width: 100%; }
      #storage-warning { display: none; }
      .tree-growth-ui { bottom: 150px; width: calc(100vw - 20px); }
      .tree-growth-card { min-width: 0; width: 100%; }
    }

    @media (max-width: 520px) {
      #weather-hud {
        width: calc(100vw - 20px);
        top: 62px;
      }
      .save-share-dock {
        display: none;
      }
      #premium-cta {
        left: 10px;
        right: 10px;
        bottom: 10px;
        text-align: center;
      }
      .cycle-actions {
        grid-template-columns: 1fr;
      }
      .cycle-actions button.premium {
        grid-column: auto;
      }
    }

    /* V8.14 CTA UX CLEANUP — actions regroupées par intention.
       1) sauvegarde sûre, 2) outils secondaires, 3) premium séparé. */
    .save-share-dock.cta-clean {
      left: 18px !important;
      bottom: 16px !important;
      width: min(390px, calc(100vw - 236px)) !important;
      max-width: min(390px, calc(100vw - 236px)) !important;
      display: flex !important;
      flex-direction: column !important;
      gap: 10px !important;
      padding: 12px !important;
      border-radius: 24px !important;
      border: 1px solid rgba(216, 171, 97, 0.20) !important;
      background:
        radial-gradient(circle at 12% 0%, rgba(238, 196, 116, 0.14), transparent 42%),
        linear-gradient(145deg, rgba(27, 20, 12, 0.72), rgba(52, 39, 21, 0.48)) !important;
      box-shadow: 0 22px 52px rgba(7, 10, 5, 0.28), inset 0 1px 0 rgba(255, 238, 193, 0.12) !important;
      backdrop-filter: blur(22px) saturate(128%) !important;
      -webkit-backdrop-filter: blur(22px) saturate(128%) !important;
    }
    .dock-header {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 12px;
      padding: 0 2px 2px;
      color: rgba(255, 248, 225, 0.92);
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    }
    .dock-header span {
      font-size: 11px;
      font-weight: 950;
      letter-spacing: 0.13em;
      text-transform: uppercase;
      white-space: nowrap;
    }
    .dock-header small {
      color: rgba(226, 218, 188, 0.62);
      font-size: 10px;
      font-weight: 750;
      text-align: right;
      line-height: 1.25;
    }
    .save-share-dock.cta-clean .micro-btn {
      width: 100%;
      min-height: 38px;
      height: auto;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      text-align: center;
      border-radius: 16px;
      padding: 10px 12px;
      white-space: normal;
      line-height: 1.15;
    }
    .save-share-dock.cta-clean .micro-btn small {
      display: block;
      margin-top: 4px;
      color: rgba(226, 218, 188, 0.66);
      font-size: 10px;
      font-weight: 650;
      line-height: 1.25;
    }
    .save-share-dock.cta-clean .micro-btn-primary {
      min-height: 58px;
      flex-direction: column;
      border-color: rgba(244, 213, 138, 0.36);
      background:
        radial-gradient(circle at 14% 0%, rgba(244, 213, 138, 0.20), transparent 46%),
        linear-gradient(145deg, rgba(79, 110, 38, 0.72), rgba(42, 26, 18, 0.58));
      box-shadow: 0 16px 34px rgba(7, 10, 5, 0.22), inset 0 1px 0 rgba(255, 238, 193, 0.16);
      font-size: 12px;
      font-weight: 900;
    }
    .dock-more {
      border-top: 1px solid rgba(255, 248, 225, 0.08);
      padding-top: 8px;
    }
    .dock-more summary {
      list-style: none;
      cursor: pointer;
      color: rgba(255, 248, 225, 0.78);
      font: 850 10px/1.2 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      display: flex;
      align-items: center;
      justify-content: space-between;
      user-select: none;
    }
    .dock-more summary::-webkit-details-marker { display: none; }
    .dock-more summary::after {
      content: '›';
      transform: rotate(90deg);
      transition: transform 0.18s ease;
      color: rgba(244, 213, 138, 0.70);
      font-size: 16px;
      line-height: 1;
    }
    .dock-more[open] summary::after { transform: rotate(-90deg); }
    .dock-secondary-grid {
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 8px;
      margin-top: 9px;
    }
    .dock-secondary-grid .micro-btn {
      font-size: 10px;
      padding: 9px 8px;
      background: linear-gradient(145deg, rgba(27, 20, 12, 0.58), rgba(52, 39, 21, 0.36));
    }
    #storage-warning {
      bottom: 184px !important;
      max-width: min(390px, calc(100vw - 236px)) !important;
      font-size: 10.5px !important;
      line-height: 1.35 !important;
      opacity: 0.76;
    }
    .tree-growth-ui { bottom: 118px !important; }
    @media (max-width: 980px) {
      .save-share-dock.cta-clean {
        left: 10px !important;
        right: 10px !important;
        bottom: 74px !important;
        width: auto !important;
        max-width: none !important;
      }
      #storage-warning {
        left: 10px !important;
        right: 10px !important;
        bottom: 250px !important;
        max-width: none !important;
      }
      .tree-growth-ui { bottom: 232px !important; }
      #premium-cta { bottom: 10px !important; }
    }
    @media (max-width: 560px) {
      .save-share-dock.cta-clean {
        display: flex !important;
        bottom: 72px !important;
        padding: 10px !important;
      }
      .dock-header { display: none; }
      .save-share-dock.cta-clean .micro-btn-primary { min-height: 48px; }
      .save-share-dock.cta-clean .micro-btn-primary small { display: none; }
      .dock-secondary-grid { grid-template-columns: 1fr; }
      #storage-warning { display: none !important; }
      .tree-growth-ui { bottom: 182px !important; }
    }
    @media (max-width: 520px) {
      .save-share-dock.cta-clean { display: flex !important; }
    }

    /* =====================================================
       MICROCOSM V8.15 STABLE PROD — HUD responsive compact
       Objectif : limiter les chevauchements entre météo, mémoire,
       croissance, premium et console dev, tout en gardant les CTA lisibles.
       ===================================================== */
    .save-share-dock.cta-clean {
      width: min(340px, calc(100vw - 248px)) !important;
      max-width: min(340px, calc(100vw - 248px)) !important;
      gap: 8px !important;
      padding: 10px !important;
    }
    .dock-header small { font-size: 9px; }
    .save-share-dock.cta-clean .micro-btn-primary { min-height: 50px; }
    .save-share-dock.cta-clean .micro-btn[disabled],
    .save-share-dock.cta-clean .micro-btn.is-disabled {
      cursor: not-allowed;
      opacity: 0.48;
      filter: grayscale(0.28) saturate(0.72);
      background: linear-gradient(145deg, rgba(45, 37, 23, 0.64), rgba(24, 19, 12, 0.56)) !important;
      border-color: rgba(226, 218, 188, 0.12) !important;
      box-shadow: inset 0 1px 0 rgba(255, 248, 225, 0.06) !important;
    }
    .dock-more summary {
      color: rgba(226, 218, 188, 0.56);
      font-size: 9px;
      letter-spacing: 0.10em;
      text-transform: lowercase;
    }
    .dock-secondary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .dock-secondary-grid .micro-btn { min-height: 34px; }
    #weather-hud.weather-lite {
      width: min(330px, calc(100vw - 86px));
      padding: 10px 11px;
    }
    .weather-lite-values { gap: 5px; }
    .weather-lite-values > div { padding: 7px 8px; }
    #premium-cta {
      max-width: min(300px, calc(100vw - 40px));
      text-align: center;
    }
    #storage-warning {
      bottom: 154px !important;
      max-width: min(340px, calc(100vw - 248px)) !important;
    }
    /* Popup glass de carte mémoire : pas d'iframe, pas de seconde fenêtre imbriquée.
       Le microcosme reste visible en fond, avec un blur doux. */
    /* V8.19 · CARD_VIEWER_FLOW_CLEANUP
       La carte doit rester le sujet principal : le décor est encore lisible,
       les HUD deviennent fantômes et ne capturent plus les interactions. */
    body.microcosm-memory-card-open #microcosm-app-root,
    body.microcosm-memory-card-open canvas {
      filter: blur(5px) saturate(0.92) brightness(0.78);
      transform: translateZ(0);
      transition: filter 0.28s ease;
    }
    body.microcosm-memory-card-open #weather-hud,
    body.microcosm-memory-card-open #ui-toggle,
    body.microcosm-memory-card-open #ui-panel,
    body.microcosm-memory-card-open .tree-growth-ui,
    body.microcosm-memory-card-open .save-share-dock,
    body.microcosm-memory-card-open #premium-cta,
    body.microcosm-memory-card-open #premium-panel,
    body.microcosm-memory-card-open #storage-warning {
      opacity: 0.16;
      filter: blur(3px) saturate(0.72) brightness(0.72);
      pointer-events: none;
      transform: translateZ(0);
      transition: opacity 0.22s ease, filter 0.22s ease;
    }
    .memory-card-modal {
      position: fixed;
      inset: 0;
      z-index: 6000;
      display: none;
      align-items: center;
      justify-content: center;
      padding: clamp(16px, 4vw, 56px);
      background:
        radial-gradient(circle at 50% 18%, rgba(244,213,138,0.10), transparent 34%),
        radial-gradient(circle at 18% 82%, rgba(79,110,38,0.12), transparent 38%),
        rgba(5, 7, 4, 0.28);
      backdrop-filter: blur(8px) saturate(112%) brightness(0.92);
      -webkit-backdrop-filter: blur(8px) saturate(112%) brightness(0.92);
    }
    .memory-card-modal.open { display: flex; }
    .memory-card-modal::before {
      content: '';
      position: absolute;
      inset: 0;
      pointer-events: none;
      background: linear-gradient(180deg, rgba(255,248,225,0.05), transparent 34%, rgba(0,0,0,0.10));
    }
    .memory-card-shell {
      position: relative;
      z-index: 1;
      width: min(940px, calc(100vw - 32px));
      max-height: min(720px, calc(100dvh - 32px));
      display: grid;
      grid-template-rows: auto minmax(0, 1fr) auto;
      overflow: hidden;
      border-radius: 30px;
      border: 1px solid rgba(216,171,97,0.30);
      background:
        radial-gradient(circle at 12% 0%, rgba(244,213,138,0.13), transparent 38%),
        radial-gradient(circle at 88% 18%, rgba(79,110,38,0.14), transparent 36%),
        linear-gradient(145deg, rgba(22,17,10,0.78), rgba(8,8,5,0.74));
      box-shadow:
        0 38px 92px rgba(0,0,0,0.44),
        0 0 0 1px rgba(255,248,225,0.04),
        inset 0 1px 0 rgba(255,248,225,0.14);
      backdrop-filter: blur(24px) saturate(122%);
      -webkit-backdrop-filter: blur(24px) saturate(122%);
      color: rgba(255,248,225,0.92);
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    }
    .memory-card-modal-bar {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
      padding: 14px 16px 10px 18px;
      border-bottom: 1px solid rgba(216,171,97,0.12);
      background: linear-gradient(180deg, rgba(7,6,3,0.36), rgba(7,6,3,0.12));
    }
    .memory-card-modal-title {
      min-width: 0;
      display: grid;
      gap: 2px;
      font-weight: 950;
      font-size: 12px;
      letter-spacing: 0.10em;
      text-transform: uppercase;
    }
    .memory-card-modal-title small {
      color: rgba(226,218,188,0.64);
      font-size: 10px;
      font-weight: 650;
      letter-spacing: 0;
      text-transform: none;
    }
    .memory-card-close-x {
      width: 34px;
      height: 34px;
      border-radius: 999px;
      border: 1px solid rgba(244,213,138,0.20);
      color: rgba(255,248,225,0.88);
      background: rgba(20,15,9,0.52);
      font: 900 18px/1 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      cursor: pointer;
    }
    .memory-card-popup-body {
      min-height: 0;
      display: grid;
      grid-template-columns: minmax(260px, 410px) minmax(260px, 1fr);
      gap: 18px;
      padding: 18px;
      overflow: auto;
      scrollbar-width: thin;
      scrollbar-color: rgba(244,213,138,0.24) transparent;
    }
    .memory-card-popup-body::-webkit-scrollbar { width: 6px; }
    .memory-card-popup-body::-webkit-scrollbar-thumb { background: rgba(244,213,138,0.20); border-radius: 999px; }
    .memory-card-preview-panel {
      display: grid;
      place-items: center;
      min-height: 0;
      border-radius: 24px;
      border: 1px solid rgba(216,171,97,0.13);
      background:
        radial-gradient(circle at 50% 0%, rgba(244,213,138,0.09), transparent 34%),
        rgba(10,9,5,0.24);
      overflow: hidden;
      padding: clamp(10px, 2vw, 18px);
    }
    .memory-card-preview-panel img {
      display: block;
      width: min(100%, 360px);
      max-height: min(52dvh, 520px);
      aspect-ratio: 1080 / 1600;
      object-fit: contain;
      border-radius: 22px;
      box-shadow: 0 24px 54px rgba(0,0,0,0.34), 0 0 0 1px rgba(255,248,225,0.08);
      background: rgba(8,7,4,0.52);
    }
    .memory-card-info-panel {
      min-width: 0;
      display: grid;
      align-content: start;
      gap: 12px;
      padding: 4px 2px;
    }
    .memory-card-kicker {
      color: rgba(226,218,188,0.72);
      font-size: 12px;
      line-height: 1.45;
    }
    .memory-card-info-panel h2 {
      margin: 0;
      color: rgba(255,248,225,0.96);
      font-size: clamp(24px, 3.5vw, 42px);
      line-height: .94;
      letter-spacing: -0.03em;
      text-transform: uppercase;
    }
    .memory-card-info-grid {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 8px;
    }
    .memory-card-info-grid div {
      min-width: 0;
      padding: 10px;
      border-radius: 16px;
      border: 1px solid rgba(216,171,97,0.12);
      background: rgba(7,6,3,0.28);
    }
    .memory-card-info-grid small {
      display: block;
      color: rgba(226,218,188,0.54);
      font-size: 8px;
      font-weight: 900;
      letter-spacing: .13em;
      text-transform: uppercase;
    }
    .memory-card-info-grid b {
      display: block;
      margin-top: 4px;
      color: rgba(255,248,225,0.94);
      font-size: 13px;
      line-height: 1.1;
      overflow-wrap: anywhere;
    }
    .memory-card-popup-note {
      color: rgba(226,218,188,0.62);
      font-size: 11px;
      line-height: 1.45;
    }
    .memory-card-modal-footer {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      padding: 12px 16px 16px;
      border-top: 1px solid rgba(216,171,97,0.12);
      background: rgba(7,6,3,0.20);
    }
    .memory-card-modal-footer button,
    .memory-card-inline-actions button {
      min-height: 38px;
      border: 1px solid rgba(244,213,138,0.22);
      border-radius: 999px;
      padding: 9px 14px;
      color: rgba(255,248,225,0.92);
      background: linear-gradient(145deg, rgba(79,110,38,0.54), rgba(42,26,18,0.42));
      font: 900 12px/1 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      cursor: pointer;
    }
    .memory-card-modal-footer button.secondary,
    .memory-card-inline-actions button.secondary {
      background: linear-gradient(145deg, rgba(27,20,12,0.58), rgba(52,39,21,0.36));
    }
    .memory-card-modal-footer button[disabled] {
      opacity: .48;
      cursor: not-allowed;
      filter: grayscale(.25);
    }
    .memory-card-inline-actions {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      align-items: center;
    }
    .memory-card-more-options-note {
      color: rgba(226,218,188,0.42);
      font-size: 10px;
      font-weight: 750;
      letter-spacing: .03em;
      white-space: nowrap;
    }

    /* DYNAMIC_MEMORY_VIEWER_V8_17
       La popup reste une vraie couche glass native, mais la carte n'est plus une image figée :
       elle flotte dans un viewer 3D CSS avec recto/verso, reflet holo et tilt au pointeur. */
    .memory-card-preview-panel {
      overflow: visible;
      perspective: 1200px;
      min-height: min(62dvh, 580px);
      padding: clamp(14px, 2.4vw, 24px);
    }
    .memory-card-viewer {
      --tilt-x: 0deg;
      --tilt-y: 0deg;
      --shine-x: 45%;
      --shine-y: 18%;
      position: relative;
      width: min(100%, 380px);
      display: grid;
      place-items: center;
      transform-style: preserve-3d;
      isolation: isolate;
    }
    .memory-card-flip-btn {
      position: absolute;
      top: -10px;
      right: -6px;
      z-index: 6;
      min-height: 32px;
      border: 1px solid rgba(244,213,138,0.22);
      border-radius: 999px;
      padding: 7px 11px;
      color: rgba(255,248,225,0.88);
      background: linear-gradient(145deg, rgba(30,22,12,0.72), rgba(79,110,38,0.34));
      box-shadow: 0 10px 24px rgba(0,0,0,0.26), inset 0 1px 0 rgba(255,248,225,0.10);
      font: 900 10px/1 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      letter-spacing: .08em;
      text-transform: uppercase;
      cursor: pointer;
    }
    .memory-card-tilt {
      width: min(100%, 370px);
      aspect-ratio: 1080 / 1600;
      transform-style: preserve-3d;
      transform: rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
      animation: memoryCardFloat 5.4s ease-in-out infinite;
      transition: transform 0.18s ease-out;
      filter: drop-shadow(0 34px 38px rgba(0,0,0,0.46));
    }
    .memory-card-flipper {
      position: relative;
      width: 100%;
      height: 100%;
      transform-style: preserve-3d;
      transition: transform 0.72s cubic-bezier(.16,1,.3,1);
    }
    .memory-card-viewer.is-flipped .memory-card-flipper { transform: rotateY(180deg); }
    .memory-card-face {
      position: absolute;
      inset: 0;
      display: grid;
      grid-template-rows: auto auto auto minmax(0, 1fr) auto auto;
      gap: 2.3%;
      padding: 8.2% 7.4%;
      border-radius: 7.2%;
      overflow: hidden;
      backface-visibility: hidden;
      transform: translateZ(0.1px);
      border: 1px solid rgba(244,213,138,0.24);
      color: rgba(255,248,225,0.94);
      background:
        radial-gradient(circle at var(--shine-x) var(--shine-y), rgba(255,248,225,0.20), transparent 20%),
        radial-gradient(circle at 18% 5%, rgba(244,213,138,0.15), transparent 33%),
        linear-gradient(145deg, rgba(43,37,21,0.94), rgba(9,10,5,0.98) 58%, rgba(20,15,9,0.96));
      box-shadow: inset 0 0 0 10px rgba(255,248,225,0.035), inset 0 0 0 12px rgba(216,171,97,0.08);
    }
    .memory-card-face::before {
      content: '';
      position: absolute;
      inset: -18%;
      pointer-events: none;
      opacity: .34;
      mix-blend-mode: screen;
      background:
        linear-gradient(115deg, transparent 0 28%, rgba(156,255,196,.14) 36%, rgba(244,213,138,.20) 45%, rgba(194,174,255,.12) 54%, transparent 66% 100%),
        repeating-linear-gradient(35deg, rgba(255,255,255,.035) 0 1px, transparent 1px 9px);
      transform: translate3d(calc((var(--shine-x) - 50%) * .08), calc((var(--shine-y) - 50%) * .08), 1px) rotate(2deg);
      animation: memoryCardHoloSweep 6.8s linear infinite;
    }
    .memory-card-back { transform: rotateY(180deg) translateZ(0.1px); }
    .memory-card-face-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 10px;
      position: relative;
      z-index: 1;
      color: rgba(255,248,225,0.76);
      font-size: clamp(8px, 1.15vw, 11px);
      font-weight: 950;
      letter-spacing: .20em;
      text-transform: uppercase;
    }
    .memory-card-face-header b {
      border-radius: 999px;
      padding: .42em .78em;
      background: rgba(8,7,4,0.55);
      border: 1px solid rgba(244,213,138,0.14);
      letter-spacing: .10em;
    }
    .memory-card-face h3 {
      position: relative;
      z-index: 1;
      margin: 0;
      font-size: clamp(30px, 4.5vw, 54px);
      line-height: .84;
      letter-spacing: .04em;
      text-transform: uppercase;
      color: rgba(255,248,225,0.97);
      text-shadow: 0 2px 0 rgba(0,0,0,0.22);
    }
    .memory-card-face p {
      position: relative;
      z-index: 1;
      margin: 0;
      color: rgba(244,213,138,0.82);
      font-size: clamp(9px, 1.35vw, 12px);
      font-weight: 950;
      letter-spacing: .18em;
      text-transform: uppercase;
    }
    .memory-card-view-shot {
      position: relative;
      z-index: 1;
      min-height: 0;
      border-radius: 5.8%;
      overflow: hidden;
      background:
        linear-gradient(180deg, rgba(255,248,225,0.10), rgba(0,0,0,0.12)),
        var(--memory-card-shot, radial-gradient(circle at 50% 55%, rgba(79,110,38,0.55), rgba(8,7,4,0.90)));
      background-size: cover;
      background-position: center;
      border: 1px solid rgba(244,213,138,0.16);
      box-shadow: inset 0 1px 0 rgba(255,248,225,0.10);
    }
    .memory-card-view-progress {
      position: absolute;
      left: 7%;
      right: 7%;
      bottom: 9%;
      height: 8.5%;
      border-radius: 999px;
      overflow: hidden;
      background: rgba(8,7,4,0.82);
      border: 1px solid rgba(244,213,138,0.14);
      display: flex;
      align-items: center;
      justify-content: flex-end;
      padding-right: 4%;
      color: rgba(255,248,225,0.96);
      font-size: clamp(10px, 1.7vw, 15px);
      font-weight: 950;
    }
    .memory-card-view-progress i {
      position: absolute;
      inset: 0 auto 0 0;
      width: var(--memory-card-progress, 0%);
      border-radius: inherit;
      background: linear-gradient(90deg, rgba(118,145,63,0.95), rgba(244,213,138,0.88));
      box-shadow: 0 0 20px rgba(244,213,138,0.22);
    }
    .memory-card-view-progress span { position: relative; z-index: 1; }
    .memory-card-view-stats,
    .memory-card-back-grid {
      position: relative;
      z-index: 1;
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 2.2%;
    }
    .memory-card-view-stats div,
    .memory-card-back-grid div {
      min-width: 0;
      border-radius: 14px;
      padding: 9% 10%;
      background: rgba(3,4,2,0.46);
      border: 1px solid rgba(244,213,138,0.10);
      box-shadow: inset 0 1px 0 rgba(255,248,225,0.05);
    }
    .memory-card-view-stats small,
    .memory-card-back-grid small {
      display: block;
      color: rgba(226,218,188,0.58);
      font-size: clamp(6px, .9vw, 8px);
      font-weight: 950;
      letter-spacing: .16em;
      text-transform: uppercase;
    }
    .memory-card-view-stats b,
    .memory-card-back-grid b {
      display: block;
      margin-top: .35em;
      color: rgba(255,248,225,0.95);
      font-size: clamp(10px, 1.5vw, 15px);
      line-height: 1.08;
      overflow-wrap: anywhere;
    }
    .memory-card-view-cert,
    .memory-card-back-ar {
      position: relative;
      z-index: 1;
      display: grid;
      grid-template-columns: auto 1fr;
      gap: 4%;
      align-items: center;
      min-height: 0;
      padding: 3.2%;
      border-radius: 18px;
      background: rgba(5,5,2,0.42);
      border: 1px solid rgba(244,213,138,0.12);
      color: rgba(255,248,225,0.90);
      font-weight: 950;
      font-size: clamp(10px, 1.3vw, 13px);
      letter-spacing: .08em;
      text-transform: uppercase;
    }
    .memory-card-view-cert img,
    .memory-card-back-ar img {
      width: clamp(42px, 6.2vw, 64px);
      aspect-ratio: 1;
      border-radius: 10px;
      background: rgba(255,248,225,0.92);
      padding: 4px;
      object-fit: contain;
    }
    .memory-card-holo-mask {
      position: absolute;
      right: -10%;
      top: 16%;
      width: 42%;
      aspect-ratio: 1;
      border-radius: 999px;
      opacity: .12;
      filter: blur(.2px);
      background:
        radial-gradient(circle at 42% 26%, rgba(255,248,225,.75), transparent 8%),
        conic-gradient(from 20deg, rgba(194,174,255,.3), rgba(156,255,196,.36), rgba(244,213,138,.42), rgba(194,174,255,.3));
      mask-image: var(--yggdrasil-logo-mask, radial-gradient(circle, #000 0 66%, transparent 67%));
      -webkit-mask-image: var(--yggdrasil-logo-mask, radial-gradient(circle, #000 0 66%, transparent 67%));
      mask-size: contain;
      -webkit-mask-size: contain;
      mask-repeat: no-repeat;
      -webkit-mask-repeat: no-repeat;
      mask-position: center;
      -webkit-mask-position: center;
      transform: translate3d(calc((var(--shine-x) - 50%) * .04), calc((var(--shine-y) - 50%) * .04), 2px);
    }
    .memory-card-holo-mask.large {
      inset: auto -8% 9% auto;
      width: 66%;
      opacity: .16;
    }
    .memory-card-back-kicker {
      position: relative;
      z-index: 1;
      color: rgba(244,213,138,0.74);
      font-size: clamp(8px, 1vw, 10px);
      font-weight: 950;
      letter-spacing: .22em;
      text-transform: uppercase;
    }
    .memory-card-back-grid {
      grid-template-columns: repeat(2, minmax(0, 1fr));
      align-self: start;
    }
    .memory-card-back #memory-card-back-note {
      align-self: start;
      color: rgba(226,218,188,0.70);
      font-size: clamp(10px, 1.4vw, 13px);
      line-height: 1.42;
      letter-spacing: 0;
      text-transform: none;
      font-weight: 650;
    }
    .memory-card-back-ar small {
      display: block;
      margin-top: 2px;
      color: rgba(226,218,188,0.62);
      font-size: clamp(7px, 1vw, 9px);
      line-height: 1.2;
      letter-spacing: .02em;
      text-transform: none;
      overflow-wrap: anywhere;
    }
    @keyframes memoryCardFloat {
      0%, 100% { translate: 0 0; }
      50% { translate: 0 -10px; }
    }
    @keyframes memoryCardHoloSweep {
      0% { translate: -8% -2%; }
      50% { translate: 8% 2%; }
      100% { translate: -8% -2%; }
    }
    /* V8.18 · Ajustement popup carte : la carte reste dynamique, mais ne déborde plus.
       Les longs stades comme “chêne centenaire accompli” sont cassés proprement dans le cadre. */
    .memory-card-shell {
      width: min(980px, calc(100vw - 44px));
      max-height: min(700px, calc(100dvh - 44px));
    }
    .memory-card-popup-body {
      grid-template-columns: minmax(300px, 390px) minmax(260px, 1fr);
      overflow-x: hidden;
    }
    .memory-card-preview-panel {
      overflow: hidden;
      min-height: min(60dvh, 560px);
    }
    .memory-card-viewer { width: min(100%, 330px); }
    .memory-card-tilt { width: min(100%, 318px); }
    .memory-card-face { gap: 1.85%; padding: 7.4% 7%; }
    .memory-card-face h3 {
      font-size: clamp(24px, 3.15vw, 40px);
      line-height: .88;
      letter-spacing: .035em;
      overflow-wrap: anywhere;
      word-break: break-word;
      hyphens: auto;
    }
    .memory-card-face p { font-size: clamp(8px, 1.05vw, 10px); }
    .memory-card-view-stats div,
    .memory-card-back-grid div { padding: 7.5% 8%; }
    .memory-card-view-cert,
    .memory-card-back-ar { border-radius: 14px; padding: 2.8%; }

    /* V8.19 · Popup = glass viewer, pas fenêtre bureautique.
       Le panneau d'informations devient secondaire, la carte respire et la rotation
       est possible par clic/tap sur la carte ou dans la zone autour. */
    .memory-card-modal {
      padding: clamp(18px, 5vw, 68px);
      background:
        radial-gradient(circle at 48% 20%, rgba(244,213,138,0.08), transparent 34%),
        radial-gradient(circle at 24% 82%, rgba(79,110,38,0.10), transparent 40%),
        rgba(4, 6, 3, 0.20);
      backdrop-filter: blur(10px) saturate(0.96) brightness(0.92);
      -webkit-backdrop-filter: blur(10px) saturate(0.96) brightness(0.92);
    }
    .memory-card-shell {
      width: min(1120px, calc(100vw - 52px));
      height: min(720px, calc(100dvh - 52px));
      max-height: none;
      border-radius: 32px;
      background:
        radial-gradient(circle at 24% 10%, rgba(244,213,138,0.08), transparent 36%),
        radial-gradient(circle at 76% 22%, rgba(79,110,38,0.08), transparent 42%),
        linear-gradient(145deg, rgba(18,15,8,0.54), rgba(6,7,4,0.42));
      border-color: rgba(244,213,138,0.18);
      box-shadow:
        0 42px 92px rgba(0,0,0,0.46),
        0 0 0 1px rgba(255,248,225,0.03),
        inset 0 1px 0 rgba(255,248,225,0.08);
    }
    .memory-card-modal-bar {
      min-height: 48px;
      padding: 12px 16px 8px 18px;
      background: linear-gradient(180deg, rgba(7,6,3,0.18), transparent);
      border-bottom-color: rgba(216,171,97,0.07);
    }
    .memory-card-popup-body {
      grid-template-columns: minmax(400px, 1.04fr) minmax(280px, .70fr);
      gap: clamp(20px, 3vw, 38px);
      padding: clamp(16px, 2.6vw, 30px);
      overflow: hidden;
      scrollbar-width: none;
    }
    .memory-card-popup-body::-webkit-scrollbar { display: none; }
    .memory-card-preview-panel {
      min-height: 0;
      overflow: visible;
      padding: clamp(10px, 2vw, 24px);
      border-color: rgba(216,171,97,0.06);
      background:
        radial-gradient(circle at 50% 22%, rgba(244,213,138,0.08), transparent 42%),
        radial-gradient(circle at 50% 100%, rgba(79,110,38,0.10), transparent 48%);
      cursor: pointer;
    }
    .memory-card-preview-panel::after {
      content: 'Cliquer / toucher pour retourner';
      position: absolute;
      left: 50%;
      bottom: 10px;
      transform: translateX(-50%);
      color: rgba(226,218,188,0.38);
      font-size: 10px;
      font-weight: 800;
      letter-spacing: .10em;
      text-transform: uppercase;
      pointer-events: none;
      white-space: nowrap;
    }
    .memory-card-viewer {
      width: min(100%, 390px);
      cursor: pointer;
    }
    .memory-card-tilt {
      width: min(100%, 382px);
      filter: drop-shadow(0 38px 44px rgba(0,0,0,0.48));
    }
    .memory-card-flip-btn {
      opacity: .68;
      top: -6px;
      right: -4px;
      background: linear-gradient(145deg, rgba(20,15,9,0.62), rgba(79,110,38,0.22));
    }
    .memory-card-flip-btn:hover { opacity: .96; }
    .memory-card-info-panel {
      align-self: center;
      gap: 10px;
      max-width: 420px;
      opacity: .88;
    }
    .memory-card-info-panel h2 {
      font-size: clamp(24px, 3.1vw, 40px);
      line-height: .96;
      max-width: 12ch;
    }
    .memory-card-popup-note {
      max-width: 48ch;
      color: rgba(226,218,188,0.54);
    }
    .memory-card-modal-footer {
      padding: 10px 16px 16px;
      border-top-color: rgba(216,171,97,0.07);
      background: linear-gradient(180deg, transparent, rgba(7,6,3,0.16));
    }
    .memory-card-modal-footer button,
    .memory-card-inline-actions button {
      min-height: 36px;
      padding: 8px 14px;
    }
    .memory-card-face h3 {
      max-width: 100%;
      text-wrap: balance;
      overflow: visible;
      word-break: normal;
      overflow-wrap: anywhere;
      hyphens: none;
    }
    .memory-card-view-shot { min-height: 145px; }
    .memory-card-back #memory-card-back-note {
      display: -webkit-box;
      -webkit-line-clamp: 4;
      -webkit-box-orient: vertical;
      overflow: hidden;
    }

    @media (prefers-reduced-motion: reduce) {
      .memory-card-tilt,
      .memory-card-face::before { animation: none; }
      .memory-card-tilt { transition: none; }
    }

    @media (max-width: 840px) {
      .save-share-dock.cta-clean { width: auto !important; max-width: none !important; }
      #storage-warning { bottom: 236px !important; }
      .memory-card-modal { padding: 12px; }
      .memory-card-shell { width: calc(100vw - 24px); max-height: calc(100dvh - 24px); border-radius: 24px; }
      .memory-card-popup-body { grid-template-columns: 1fr; gap: 12px; padding: 12px; }
      .memory-card-tilt { width: min(100%, 286px); }
    }
    @media (max-width: 540px), (max-height: 560px) {
      #weather-hud.weather-lite { width: min(300px, calc(100vw - 72px)); }
      .tree-growth-card { transform: scale(0.92); transform-origin: center bottom; }
      .save-share-dock.cta-clean { bottom: 66px !important; }
      .memory-card-modal { padding: 8px; align-items: end; }
      .memory-card-shell { width: calc(100vw - 16px); max-height: calc(100dvh - 16px); border-radius: 20px; }
      .memory-card-modal-bar { padding: 10px 12px 8px; }
      .memory-card-modal-title small { display: none; }
      .memory-card-popup-body { padding: 10px; }
      .memory-card-tilt { width: min(100%, 232px); }
      .memory-card-info-grid { grid-template-columns: 1fr; }
      .memory-card-modal-footer { flex-direction: column-reverse; align-items: stretch; padding: 10px 12px 12px; }
      .memory-card-modal-footer button { width: 100%; }
      .memory-card-inline-actions { width: 100%; }
      .memory-card-inline-actions button { flex: 1 1 140px; }
    }
    @media (display-mode: fullscreen) {
      .memory-card-modal { padding: clamp(10px, 3vw, 42px); }
    }
    @media (max-width: 840px) {
      .memory-card-shell {
        width: calc(100vw - 22px);
        height: calc(100dvh - 22px);
      }
      .memory-card-popup-body {
        grid-template-columns: 1fr;
        grid-template-rows: minmax(0, 1fr) auto;
        gap: 12px;
        overflow: auto;
        scrollbar-width: none;
      }
      .memory-card-popup-body::-webkit-scrollbar { display: none; }
      .memory-card-preview-panel { min-height: min(58dvh, 500px); }
      .memory-card-viewer { width: min(100%, 320px); }
      .memory-card-tilt { width: min(100%, 312px); }
      .memory-card-info-panel { align-self: stretch; max-width: none; }
      .memory-card-info-panel h2 { max-width: none; font-size: clamp(22px, 7vw, 30px); }
      .memory-card-info-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    }
    @media (max-width: 540px), (max-height: 560px) {
      .memory-card-modal { align-items: center; padding: 8px; }
      .memory-card-shell { width: calc(100vw - 16px); height: calc(100dvh - 16px); }
      .memory-card-preview-panel { min-height: min(52dvh, 390px); }
      .memory-card-viewer { width: min(100%, 260px); }
      .memory-card-tilt { width: min(100%, 254px); }
      .memory-card-face { gap: 1.45%; padding: 7% 6.6%; }
      .memory-card-view-shot { min-height: 96px; }
      .memory-card-info-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
      .memory-card-popup-summary,
      .memory-card-popup-note { display: none; }
      .memory-card-preview-panel::after { display: none; }
    }



    /* Mode accueil premium : plein écran sans HUD pour restaurant, entreprise ou exposition.
       L'utilisateur sort avec Échap, ou avec la touche H si le navigateur ne quitte pas le fullscreen. */
    body.microcosm-kiosk-active #weather-hud,
    body.microcosm-kiosk-active #ui-panel,
    body.microcosm-kiosk-active #ui-toggle,
    body.microcosm-kiosk-active .save-share-dock,
    body.microcosm-kiosk-active #premium-cta,
    body.microcosm-kiosk-active #premium-panel,
    body.microcosm-kiosk-active #storage-warning,
    body.microcosm-kiosk-active #cycle-modal,
    body.microcosm-kiosk-active #vr-status-chip,
    body.microcosm-kiosk-active .tree-growth-ui {
      display: none !important;
      opacity: 0 !important;
      pointer-events: none !important;
    }
    body.microcosm-kiosk-active canvas {
      cursor: none;
    }

    /* Mode VR : les HUD DOM restent consultables hors VR, mais disparaissent en immersion.
       Le rendu VR doit rester confortable, sans panneau collé au visage. */
    body.microcosm-xr-active #weather-hud,
    body.microcosm-xr-active #ui-panel,
    body.microcosm-xr-active #ui-toggle,
    body.microcosm-xr-active .save-share-dock,
    body.microcosm-xr-active #premium-cta,
    body.microcosm-xr-active #premium-panel,
    body.microcosm-xr-active #storage-warning,
    body.microcosm-xr-active #cycle-modal {
      display: none !important;
      pointer-events: none !important;
    }
    body.microcosm-xr-active .tree-growth-ui {
      opacity: 0 !important;
      pointer-events: none !important;
      transform: translateX(-50%) translateY(12px) scale(0.94);
    }
    #vr-status-chip {
      position: fixed;
      left: 50%;
      top: 18px;
      transform: translateX(-50%);
      z-index: 1003;
      display: none;
      padding: 9px 14px;
      border-radius: 999px;
      border: 1px solid rgba(216, 171, 97, 0.22);
      background:
        radial-gradient(circle at 20% 0%, rgba(238, 196, 116, 0.16), transparent 42%),
        linear-gradient(145deg, rgba(27, 20, 12, 0.66), rgba(52, 39, 21, 0.44));
      color: rgba(255, 248, 225, 0.88);
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      font-size: 11px;
      font-weight: 800;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      box-shadow: 0 16px 34px rgba(7, 10, 5, 0.22), inset 0 1px 0 rgba(255, 238, 193, 0.13);
      backdrop-filter: blur(20px) saturate(128%);
      -webkit-backdrop-filter: blur(20px) saturate(128%);
      pointer-events: none;
    }
    body.microcosm-xr-preparing #vr-status-chip {
      display: block;
    }

  

    /* =====================================================
       V8.20 · GAMEPLAY BOTTOM BAR
       Interface joueur : une barre centrale basse, pensée comme un rituel de jeu.
       Le menu dev reste séparé. Les exports restent dans la carte HTML autonome.
    ===================================================== */
    .save-share-dock.cta-clean.gameplay-bar {
      left: 50% !important;
      right: auto !important;
      bottom: max(14px, env(safe-area-inset-bottom)) !important;
      transform: translateX(-50%) !important;
      width: min(740px, calc(100vw - 32px)) !important;
      max-width: min(740px, calc(100vw - 32px)) !important;
      display: grid !important;
      grid-template-columns: 1fr !important;
      gap: 8px !important;
      padding: 10px 12px !important;
      border-radius: 28px !important;
      background:
        radial-gradient(circle at 50% -30%, rgba(244, 213, 138, 0.18), transparent 48%),
        linear-gradient(145deg, rgba(20, 15, 8, 0.64), rgba(41, 32, 18, 0.42)) !important;
      border-color: rgba(244, 213, 138, 0.22) !important;
      box-shadow:
        0 24px 56px rgba(7, 10, 5, 0.30),
        inset 0 1px 0 rgba(255, 248, 225, 0.12) !important;
      backdrop-filter: blur(24px) saturate(126%) !important;
      -webkit-backdrop-filter: blur(24px) saturate(126%) !important;
    }
    .gameplay-bar .dock-header {
      align-items: center;
      justify-content: center;
      padding: 0 10px;
      gap: 10px;
      min-height: 18px;
      text-align: center;
    }
    .gameplay-bar .dock-header span {
      font-size: 10px;
      opacity: 0.78;
    }
    .gameplay-bar .dock-header small {
      text-align: center;
      font-size: 10px;
      max-width: 420px;
      opacity: 0.70;
    }
    .gameplay-main-actions {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 10px;
      align-items: stretch;
    }
    .save-share-dock.cta-clean.gameplay-bar .micro-btn {
      min-height: 58px;
      border-radius: 20px;
      font-size: 13px;
      letter-spacing: 0.02em;
    }
    .save-share-dock.cta-clean.gameplay-bar .micro-btn small {
      font-size: 10px;
      line-height: 1.18;
      opacity: 0.72;
    }
    .save-share-dock.cta-clean.gameplay-bar .micro-btn-primary {
      min-height: 64px;
    }
    #water-tree-btn {
      background:
        radial-gradient(circle at 18% 0%, rgba(244, 213, 138, 0.22), transparent 44%),
        linear-gradient(145deg, rgba(91, 124, 47, 0.82), rgba(37, 69, 28, 0.70)) !important;
      border-color: rgba(244, 213, 138, 0.36) !important;
    }
    #save-tree-btn {
      background:
        radial-gradient(circle at 18% 0%, rgba(255, 248, 225, 0.17), transparent 44%),
        linear-gradient(145deg, rgba(68, 54, 28, 0.84), rgba(79, 110, 38, 0.56)) !important;
    }
    .gameplay-bar .dock-more {
      padding-top: 4px;
      border-top-color: rgba(255, 248, 225, 0.06);
    }
    .gameplay-bar .dock-more summary {
      justify-content: center;
      gap: 8px;
      opacity: 0.62;
    }
    .gameplay-bar .dock-secondary-grid {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }
    .tree-growth-ui {
      bottom: 112px !important;
      width: min(520px, calc(100vw - 32px));
      pointer-events: none !important;
    }
    .tree-growth-card {
      min-width: 0 !important;
      width: 100% !important;
      padding: 10px 14px 11px !important;
      border-radius: 18px !important;
      background:
        radial-gradient(circle at 18% 0%, rgba(244, 213, 138, 0.12), transparent 42%),
        linear-gradient(145deg, rgba(22, 16, 9, 0.54), rgba(52, 39, 21, 0.34)) !important;
      box-shadow: 0 12px 30px rgba(7, 10, 5, 0.18), inset 0 1px 0 rgba(255, 248, 225, 0.10) !important;
      backdrop-filter: blur(16px) saturate(112%) !important;
      -webkit-backdrop-filter: blur(16px) saturate(112%) !important;
    }
    .tree-growth-title {
      margin-bottom: 4px !important;
      font-size: 9px !important;
      opacity: 0.72;
    }
    .tree-growth-status {
      margin-bottom: 8px !important;
      font-size: 11px !important;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .tree-button-row {
      display: none !important;
    }
    @media (max-width: 760px) {
      .save-share-dock.cta-clean.gameplay-bar {
        width: calc(100vw - 18px) !important;
        max-width: calc(100vw - 18px) !important;
        padding: 9px !important;
        border-radius: 22px !important;
      }
      .gameplay-main-actions { gap: 8px; }
      .save-share-dock.cta-clean.gameplay-bar .micro-btn {
        min-height: 56px;
        padding: 9px 8px;
        font-size: 12px;
      }
      .save-share-dock.cta-clean.gameplay-bar .micro-btn small { display: none; }
      .gameplay-bar .dock-header small { display: none; }
      .tree-growth-ui { bottom: 98px !important; width: calc(100vw - 22px); }
      .tree-growth-title { display: none; }
      .tree-growth-status { font-size: 10px !important; }
    }
    @media (max-width: 420px) {
      .gameplay-main-actions { grid-template-columns: 1fr 1fr; }
      .save-share-dock.cta-clean.gameplay-bar .micro-btn { font-size: 11px; }
      .gameplay-bar .dock-more summary { font-size: 9px; }
    }



    /* =====================================================
       V8.21 · SIMPLIFIED GAME UI + WEATHER FOCUS + PERF
       Joueur : 2 actions claires. Météo : utile d'abord, détails à la demande.
       Objectif : réduire la charge visuelle et les coûts GPU/CSS hors menu dev.
    ===================================================== */
    #weather-hud.weather-lite {
      width: min(314px, calc(100vw - 74px)) !important;
      padding: 10px 11px !important;
      border-radius: 18px !important;
      background:
        radial-gradient(circle at 18% 0%, rgba(238,196,116,0.10), transparent 36%),
        linear-gradient(145deg, rgba(18,14,8,0.36), rgba(37,29,16,0.20)) !important;
      box-shadow: 0 12px 28px rgba(7,10,5,0.13), inset 0 1px 0 rgba(255,238,193,0.07) !important;
      border-color: rgba(216,171,97,0.13) !important;
    }
    .weather-lite-mainline {
      grid-template-columns: 38px minmax(0, 1fr) auto !important;
      gap: 8px !important;
      margin-bottom: 7px !important;
    }
    .weather-visual-icon {
      width: 36px !important;
      height: 36px !important;
      border-radius: 14px !important;
      font-size: 20px !important;
    }
    .weather-lite-clock { font-size: 16px !important; }
    .weather-lite-date { font-size: 9px !important; margin-top: 2px !important; }
    .weather-mini-actions {
      display: grid;
      gap: 4px;
      justify-items: end;
      align-items: center;
    }
    .weather-lite-source {
      padding: 4px 7px !important;
      font-size: 8px !important;
      opacity: 0.72;
    }
    .weather-tune-btn {
      border: 1px solid rgba(216,171,97,0.12);
      border-radius: 999px;
      padding: 4px 8px;
      background: rgba(15,10,5,0.18);
      color: rgba(255,248,225,0.72);
      font-family: inherit;
      font-size: 8px;
      font-weight: 850;
      letter-spacing: 0.10em;
      text-transform: uppercase;
      cursor: pointer;
    }
    .weather-focus-line {
      position: relative;
      z-index: 1;
      display: grid;
      grid-template-columns: 1fr;
      gap: 2px;
      padding: 8px 10px;
      border-radius: 14px;
      border: 1px solid rgba(216,171,97,0.09);
      background: rgba(79,110,38,0.12);
      color: rgba(255,248,225,0.90);
    }
    .weather-focus-line strong {
      font-size: 12px;
      line-height: 1.18;
      font-weight: 900;
      letter-spacing: 0.01em;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .weather-focus-line span {
      font-size: 9.5px;
      line-height: 1.2;
      color: rgba(226,218,188,0.62);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    #weather-hud.compact .weather-lite-values,
    #weather-hud.compact .weather-lite-advice,
    #weather-hud.compact .weather-rainline {
      display: none !important;
    }
    #weather-hud:not(.compact) .weather-focus-line { margin-bottom: 7px; }
    #weather-hud:not(.compact) .weather-lite-values {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }
    #weather-hud:not(.compact) .weather-rainline {
      opacity: 0.72;
      font-size: 9px;
    }
    .save-share-dock.cta-clean.gameplay-bar {
      width: min(620px, calc(100vw - 28px)) !important;
      max-width: min(620px, calc(100vw - 28px)) !important;
      padding: 8px !important;
      gap: 6px !important;
      border-radius: 24px !important;
      background:
        radial-gradient(circle at 50% -25%, rgba(244,213,138,0.12), transparent 44%),
        linear-gradient(145deg, rgba(20,15,8,0.52), rgba(41,32,18,0.34)) !important;
      box-shadow: 0 18px 42px rgba(7,10,5,0.22), inset 0 1px 0 rgba(255,248,225,0.10) !important;
      backdrop-filter: blur(18px) saturate(114%) !important;
      -webkit-backdrop-filter: blur(18px) saturate(114%) !important;
    }
    .gameplay-bar .dock-header {
      min-height: 0 !important;
      padding: 0 6px 1px !important;
    }
    .gameplay-bar .dock-header span {
      font-size: 8.5px !important;
      letter-spacing: 0.15em;
      opacity: 0.52 !important;
    }
    .gameplay-bar .dock-header small { display: none !important; }
    .gameplay-main-actions { gap: 8px !important; }
    .save-share-dock.cta-clean.gameplay-bar .micro-btn {
      min-height: 50px !important;
      border-radius: 18px !important;
      font-size: 12px !important;
      padding: 9px 10px !important;
    }
    .save-share-dock.cta-clean.gameplay-bar .micro-btn small {
      font-size: 9px !important;
      opacity: 0.58 !important;
      margin-top: 2px;
    }
    .save-share-dock.cta-clean.gameplay-bar .micro-btn-primary { min-height: 54px !important; }
    .gameplay-bar .dock-more { padding-top: 0 !important; border-top: 0 !important; }
    .gameplay-bar .dock-more summary {
      min-height: 18px;
      font-size: 8.5px !important;
      opacity: 0.48 !important;
    }
    .gameplay-bar .dock-secondary-grid {
      grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
      gap: 6px !important;
      padding-top: 5px;
    }
    .gameplay-bar .dock-secondary-grid .micro-btn {
      min-height: 34px !important;
      font-size: 9px !important;
      border-radius: 13px !important;
    }
    .tree-growth-ui { bottom: 94px !important; width: min(420px, calc(100vw - 30px)) !important; }
    .tree-growth-card {
      padding: 8px 12px !important;
      border-radius: 16px !important;
      background: linear-gradient(145deg, rgba(22,16,9,0.42), rgba(52,39,21,0.24)) !important;
      box-shadow: 0 10px 24px rgba(7,10,5,0.14), inset 0 1px 0 rgba(255,248,225,0.08) !important;
      backdrop-filter: blur(12px) saturate(106%) !important;
      -webkit-backdrop-filter: blur(12px) saturate(106%) !important;
    }
    .tree-growth-title { display: none !important; }
    .tree-growth-status { margin-bottom: 6px !important; font-size: 10.5px !important; opacity: 0.84; }
    .tree-progress-track { height: 7px !important; }
    .memory-card-shell {
      background:
        radial-gradient(circle at 28% 18%, rgba(244,213,138,0.07), transparent 34%),
        linear-gradient(145deg, rgba(18,15,8,0.50), rgba(6,7,4,0.36)) !important;
      backdrop-filter: blur(22px) saturate(112%) !important;
      -webkit-backdrop-filter: blur(22px) saturate(112%) !important;
    }
    .memory-card-popup-summary { opacity: 0.78; }

    @media (max-width: 760px) {
      #weather-hud.weather-lite { width: min(286px, calc(100vw - 66px)) !important; }
      .weather-focus-line strong { font-size: 11px; }
      .weather-focus-line span { display: none; }
      .save-share-dock.cta-clean.gameplay-bar {
        width: calc(100vw - 16px) !important;
        max-width: calc(100vw - 16px) !important;
        padding: 7px !important;
        border-radius: 20px !important;
      }
      .save-share-dock.cta-clean.gameplay-bar .micro-btn {
        min-height: 48px !important;
        font-size: 11px !important;
      }
      .save-share-dock.cta-clean.gameplay-bar .micro-btn small { display: none !important; }
      .tree-growth-ui { bottom: 86px !important; }
    }
    @media (max-width: 420px) {
      #weather-hud.weather-lite { width: min(258px, calc(100vw - 58px)) !important; }
      .weather-tune-btn { display: none; }
      .save-share-dock.cta-clean.gameplay-bar .micro-btn { padding-inline: 7px !important; }
    }
    @media (prefers-reduced-motion: reduce) {
      .memory-card-tilt,
      .memory-card-viewer,
      .memory-card-face,
      .save-share-dock,
      .tree-btn,
      .micro-btn {
        animation: none !important;
        transition-duration: 0.01ms !important;
      }
    }



    /* =====================================================
       V8.22 · ORGANIC CONTROL ISLAND
       Benchmark appliqué : spatial UI légère + bento contextuel + disclosure.
       Toutes les informations joueur utiles sont réunies au même endroit :
       progression, microclimat, rituel, carte, mémoire automatique.
    ===================================================== */
    #premium-cta,
    #storage-warning {
      display: none !important;
    }

    .microcosm-ritual-dock {
      position: fixed !important;
      left: 50% !important;
      right: auto !important;
      bottom: max(14px, env(safe-area-inset-bottom)) !important;
      transform: translateX(-50%) !important;
      width: min(680px, calc(100vw - 28px)) !important;
      max-width: min(680px, calc(100vw - 28px)) !important;
      display: grid !important;
      grid-template-columns: minmax(0, 1fr) !important;
      gap: 8px !important;
      padding: 10px !important;
      border-radius: 28px !important;
      border: 1px solid rgba(244, 213, 138, 0.18) !important;
      background:
        radial-gradient(circle at 50% -35%, rgba(244, 213, 138, 0.16), transparent 42%),
        radial-gradient(circle at 12% 100%, rgba(96, 122, 48, 0.13), transparent 44%),
        linear-gradient(145deg, rgba(18, 14, 8, 0.62), rgba(31, 26, 15, 0.44)) !important;
      box-shadow:
        0 18px 44px rgba(0, 0, 0, 0.28),
        0 0 0 1px rgba(255, 248, 225, 0.035),
        inset 0 1px 0 rgba(255, 248, 225, 0.11) !important;
      backdrop-filter: blur(12px) saturate(112%) !important;
      -webkit-backdrop-filter: blur(12px) saturate(112%) !important;
      contain: layout style;
      z-index: 1004 !important;
    }

    .ritual-topline {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      gap: 8px;
      align-items: center;
      padding: 0 2px;
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    }
    .ritual-stage-pill,
    .ritual-memory-pill {
      min-width: 0;
      min-height: 32px;
      display: flex;
      align-items: center;
      gap: 8px;
      border: 1px solid rgba(216, 171, 97, 0.10);
      border-radius: 999px;
      background: rgba(8, 7, 4, 0.24);
      color: rgba(255, 248, 225, 0.82);
      padding: 7px 10px;
      box-shadow: inset 0 1px 0 rgba(255, 248, 225, 0.045);
    }
    .ritual-stage-pill span {
      color: rgba(226, 218, 188, 0.52);
      font-size: 8.5px;
      font-weight: 900;
      letter-spacing: 0.14em;
      text-transform: uppercase;
      white-space: nowrap;
    }
    .ritual-stage-pill b {
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      font-size: 11px;
      font-weight: 850;
      letter-spacing: 0.01em;
    }
    .ritual-memory-pill {
      justify-content: center;
      color: rgba(226, 218, 188, 0.64);
      font-size: 9.5px;
      font-weight: 800;
      letter-spacing: 0.04em;
      white-space: nowrap;
    }
    .ritual-memory-dot {
      width: 6px;
      height: 6px;
      border-radius: 999px;
      background: rgba(168, 205, 98, 0.92);
      box-shadow: 0 0 10px rgba(168, 205, 98, 0.58);
      flex: 0 0 auto;
    }

    .ritual-progress-anchor .tree-growth-ui {
      position: static !important;
      inset: auto !important;
      transform: none !important;
      width: 100% !important;
      pointer-events: none !important;
      display: block !important;
    }
    .ritual-progress-anchor .tree-growth-card {
      width: 100% !important;
      min-width: 0 !important;
      padding: 0 !important;
      border: 0 !important;
      border-radius: 0 !important;
      background: transparent !important;
      box-shadow: none !important;
      backdrop-filter: none !important;
      -webkit-backdrop-filter: none !important;
    }
    .ritual-progress-anchor .tree-growth-title,
    .ritual-progress-anchor .tree-growth-status,
    .ritual-progress-anchor .tree-button-row {
      display: none !important;
    }
    .ritual-progress-anchor .tree-progress-track {
      height: 7px !important;
      border-radius: 999px !important;
      border: 1px solid rgba(255, 248, 225, 0.09) !important;
      background: rgba(7, 6, 3, 0.38) !important;
      box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.32) !important;
    }

    .ritual-weather-anchor #weather-hud.weather-lite {
      position: static !important;
      inset: auto !important;
      width: 100% !important;
      max-width: none !important;
      padding: 8px !important;
      border-radius: 20px !important;
      background:
        radial-gradient(circle at 0% 0%, rgba(118, 145, 63, 0.12), transparent 38%),
        rgba(7, 6, 3, 0.20) !important;
      border: 1px solid rgba(216, 171, 97, 0.10) !important;
      box-shadow: inset 0 1px 0 rgba(255, 248, 225, 0.055) !important;
      backdrop-filter: none !important;
      -webkit-backdrop-filter: none !important;
      cursor: pointer;
      contain: layout style;
    }
    .ritual-weather-anchor #weather-hud.weather-lite::after { display: none !important; }
    .ritual-weather-anchor .weather-lite-mainline {
      grid-template-columns: 34px minmax(0, 1fr) auto !important;
      gap: 8px !important;
      margin-bottom: 6px !important;
      align-items: center !important;
    }
    .ritual-weather-anchor .weather-visual-icon {
      width: 34px !important;
      height: 34px !important;
      border-radius: 14px !important;
      font-size: 18px !important;
      background: rgba(255, 248, 225, 0.06) !important;
      box-shadow: none !important;
    }
    .ritual-weather-anchor .weather-lite-clock { font-size: 15px !important; }
    .ritual-weather-anchor .weather-lite-date { font-size: 8.5px !important; opacity: 0.72; }
    .ritual-weather-anchor .weather-lite-source {
      padding: 4px 7px !important;
      font-size: 7.5px !important;
      opacity: 0.68;
    }
    .ritual-weather-anchor .weather-tune-btn {
      padding: 4px 7px !important;
      font-size: 7.5px !important;
      opacity: 0.68;
      background: rgba(15, 10, 5, 0.18);
    }
    .ritual-weather-anchor .weather-focus-line {
      padding: 0 !important;
      border: 0 !important;
      border-radius: 0 !important;
      background: transparent !important;
    }
    .ritual-weather-anchor .weather-focus-line strong {
      font-size: 12px !important;
      line-height: 1.2 !important;
      color: rgba(255, 248, 225, 0.92);
    }
    .ritual-weather-anchor .weather-focus-line span {
      font-size: 9px !important;
      color: rgba(226, 218, 188, 0.58);
    }
    .ritual-weather-anchor #weather-hud.compact .weather-lite-values,
    .ritual-weather-anchor #weather-hud.compact .weather-lite-advice,
    .ritual-weather-anchor #weather-hud.compact .weather-rainline,
    .ritual-weather-anchor #weather-hud.compact .weather-lunar-strip,
    .ritual-weather-anchor #weather-hud.compact .weather-location-tools {
      display: none !important;
    }
    .ritual-weather-anchor #weather-hud:not(.compact) .weather-lite-values {
      grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
      gap: 6px !important;
      margin-top: 8px !important;
    }
    .ritual-weather-anchor #weather-hud:not(.compact) .weather-lite-values > div {
      padding: 7px 8px !important;
      border-radius: 12px !important;
      background: rgba(8, 7, 4, 0.24) !important;
    }
    .ritual-weather-anchor #weather-hud:not(.compact) .weather-lite-advice,
    .ritual-weather-anchor #weather-hud:not(.compact) .weather-rainline {
      margin-top: 6px !important;
      font-size: 9.5px !important;
    }

    .ritual-action-row {
      display: grid !important;
      grid-template-columns: 1fr 1fr !important;
      gap: 8px !important;
      margin-top: 0 !important;
    }
    .microcosm-ritual-dock .micro-btn {
      min-height: 58px !important;
      border-radius: 20px !important;
      border: 1px solid rgba(244, 213, 138, 0.18) !important;
      color: rgba(255, 248, 225, 0.94) !important;
      background:
        radial-gradient(circle at 50% 0%, rgba(244, 213, 138, 0.16), transparent 52%),
        linear-gradient(145deg, rgba(80, 106, 42, 0.72), rgba(36, 48, 24, 0.58)) !important;
      box-shadow: 0 9px 20px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,248,225,0.12) !important;
      font-size: 13px !important;
      font-weight: 950 !important;
      letter-spacing: 0.01em !important;
      transition: transform 0.16s ease, filter 0.16s ease, border-color 0.16s ease !important;
    }
    .microcosm-ritual-dock .micro-btn:hover {
      transform: translateY(-1px);
      filter: brightness(1.06);
      border-color: rgba(244, 213, 138, 0.30) !important;
    }
    .microcosm-ritual-dock .card-btn {
      background:
        radial-gradient(circle at 50% 0%, rgba(244, 213, 138, 0.14), transparent 54%),
        linear-gradient(145deg, rgba(54, 42, 24, 0.72), rgba(26, 22, 14, 0.58)) !important;
    }
    .microcosm-ritual-dock .micro-btn small {
      display: block !important;
      margin-top: 3px !important;
      color: rgba(226, 218, 188, 0.56) !important;
      font-size: 9px !important;
      font-weight: 700 !important;
      letter-spacing: 0.02em !important;
    }
    .ritual-unlock-link {
      justify-self: center;
      border: 0;
      background: transparent;
      color: rgba(226, 218, 188, 0.52);
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      font-size: 9.5px;
      font-weight: 850;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      cursor: pointer;
      padding: 2px 8px 0;
    }
    .ritual-unlock-link:hover { color: rgba(255, 248, 225, 0.82); }

    body.microcosm-memory-card-open .microcosm-ritual-dock {
      opacity: 0.14 !important;
      filter: blur(2px) saturate(0.72) !important;
      pointer-events: none !important;
    }
    body:not(.microcosm-memory-card-open) .memory-card-tilt,
    body:not(.microcosm-memory-card-open) .memory-card-face::before {
      animation-play-state: paused !important;
    }

    @media (max-width: 740px) {
      .microcosm-ritual-dock {
        width: calc(100vw - 16px) !important;
        max-width: calc(100vw - 16px) !important;
        gap: 7px !important;
        padding: 8px !important;
        border-radius: 22px !important;
      }
      .ritual-topline {
        grid-template-columns: 1fr;
        gap: 5px;
      }
      .ritual-memory-pill {
        min-height: 26px;
        justify-content: flex-start;
        font-size: 8.5px;
      }
      .ritual-weather-anchor .weather-lite-mainline {
        grid-template-columns: 32px minmax(0,1fr) auto !important;
      }
      .ritual-weather-anchor .weather-focus-line span { display: none !important; }
      .microcosm-ritual-dock .micro-btn {
        min-height: 54px !important;
        font-size: 12px !important;
        padding-inline: 8px !important;
      }
    }
    @media (max-width: 440px) {
      .ritual-stage-pill span { display: none; }
      .ritual-action-row { gap: 6px !important; }
      .microcosm-ritual-dock .micro-btn small { display: none !important; }
      .microcosm-ritual-dock .micro-btn { min-height: 50px !important; font-size: 11.5px !important; }
      .ritual-unlock-link { font-size: 8px; }
    }


    /* =====================================================
       V8.23 · PROD FLOW POLISH
       Patch de finition uniquement : hiérarchie joueur, météo utile,
       carte flottante, responsive et coût visuel plus bas.
       La croissance Fibonacci et les seeds ne sont pas modifiés.
    ===================================================== */
    :root {
      --mic-cream: rgba(255, 248, 225, 0.94);
      --mic-cream-soft: rgba(226, 218, 188, 0.66);
      --mic-earth: rgba(18, 14, 8, 0.58);
      --mic-earth-soft: rgba(31, 25, 14, 0.42);
      --mic-olive: rgba(82, 110, 43, 0.70);
      --mic-gold: rgba(244, 213, 138, 0.28);
    }

    .microcosm-ritual-dock {
      width: min(720px, calc(100vw - 26px)) !important;
      max-width: min(720px, calc(100vw - 26px)) !important;
      gap: 7px !important;
      padding: 11px 12px 10px !important;
      border-radius: 30px !important;
      background:
        radial-gradient(circle at 50% -45%, rgba(244, 213, 138, 0.13), transparent 44%),
        radial-gradient(circle at 9% 118%, rgba(96, 122, 48, 0.12), transparent 42%),
        linear-gradient(145deg, rgba(17, 13, 7, 0.56), rgba(30, 25, 14, 0.38)) !important;
      border-color: rgba(244, 213, 138, 0.15) !important;
      box-shadow:
        0 16px 36px rgba(0, 0, 0, 0.24),
        0 0 0 1px rgba(255, 248, 225, 0.028),
        inset 0 1px 0 rgba(255, 248, 225, 0.10) !important;
      backdrop-filter: blur(10px) saturate(108%) !important;
      -webkit-backdrop-filter: blur(10px) saturate(108%) !important;
    }

    .ritual-island-label {
      justify-self: center;
      margin: -1px 0 -2px;
      color: rgba(255, 248, 225, 0.44);
      font: 950 8.5px/1.1 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      letter-spacing: 0.20em;
      text-transform: uppercase;
      pointer-events: none;
    }

    .ritual-topline {
      grid-template-columns: minmax(0, 1fr) auto !important;
      gap: 7px !important;
    }

    .ritual-stage-pill,
    .ritual-memory-pill {
      min-height: 29px !important;
      padding: 6px 9px !important;
      background: rgba(7, 6, 3, 0.19) !important;
      border-color: rgba(216, 171, 97, 0.075) !important;
    }

    .ritual-stage-pill span { font-size: 8px !important; opacity: 0.82; }
    .ritual-stage-pill b { font-size: 10.5px !important; }
    .ritual-memory-pill { font-size: 8.8px !important; color: rgba(226, 218, 188, 0.58) !important; }

    .ritual-progress-anchor .tree-progress-track {
      height: 6px !important;
      background: rgba(5, 5, 3, 0.32) !important;
      border-color: rgba(255, 248, 225, 0.07) !important;
    }
    .ritual-progress-anchor .tree-progress-fill {
      background: linear-gradient(90deg, rgba(103, 135, 54, 0.92), rgba(193, 156, 72, 0.94), rgba(244, 213, 138, 0.96)) !important;
      box-shadow: 0 0 11px rgba(244, 213, 138, 0.34) !important;
    }

    .ritual-weather-anchor #weather-hud.weather-lite {
      padding: 8px 9px !important;
      border-radius: 19px !important;
      background:
        radial-gradient(circle at 0% 0%, rgba(118, 145, 63, 0.10), transparent 38%),
        rgba(7, 6, 3, 0.16) !important;
      transition: background 0.18s ease, border-color 0.18s ease;
    }
    .ritual-weather-anchor #weather-hud.weather-lite:not(.compact) {
      background:
        radial-gradient(circle at 0% 0%, rgba(118, 145, 63, 0.14), transparent 38%),
        rgba(7, 6, 3, 0.24) !important;
      border-color: rgba(216, 171, 97, 0.13) !important;
    }
    .weather-mini-actions {
      display: inline-grid;
      grid-template-columns: auto auto;
      gap: 5px;
      align-items: center;
    }
    .weather-tune-btn {
      border: 1px solid rgba(216, 171, 97, 0.12);
      border-radius: 999px;
      color: rgba(255, 248, 225, 0.70);
      background: rgba(9, 7, 4, 0.22);
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      font-weight: 900;
      letter-spacing: 0.10em;
      text-transform: uppercase;
      cursor: pointer;
    }
    .weather-tune-btn[aria-expanded="true"] {
      color: rgba(255, 248, 225, 0.92);
      border-color: rgba(244, 213, 138, 0.22);
      background: rgba(80, 106, 42, 0.24);
    }
    .ritual-weather-anchor .weather-focus-line strong {
      font-size: 12.4px !important;
      letter-spacing: -0.01em;
    }
    .ritual-weather-anchor .weather-focus-line span {
      display: block;
      max-width: 100%;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .microcosm-ritual-dock .micro-btn {
      min-height: 56px !important;
      border-radius: 19px !important;
      box-shadow: 0 8px 18px rgba(0,0,0,0.16), inset 0 1px 0 rgba(255,248,225,0.11) !important;
    }
    #water-tree-btn {
      border-color: rgba(244, 213, 138, 0.24) !important;
      background:
        radial-gradient(circle at 50% 0%, rgba(244, 213, 138, 0.18), transparent 52%),
        linear-gradient(145deg, rgba(88, 119, 46, 0.76), rgba(38, 53, 25, 0.62)) !important;
    }
    #save-tree-btn {
      background:
        radial-gradient(circle at 50% 0%, rgba(244, 213, 138, 0.13), transparent 52%),
        linear-gradient(145deg, rgba(58, 45, 25, 0.66), rgba(25, 22, 14, 0.54)) !important;
    }
    .ritual-unlock-link {
      opacity: 0.82;
      padding-top: 0 !important;
    }

    .memory-card-modal {
      background:
        radial-gradient(circle at 50% 18%, rgba(244,213,138,0.075), transparent 34%),
        radial-gradient(circle at 18% 82%, rgba(79,110,38,0.085), transparent 38%),
        rgba(4, 6, 3, 0.18) !important;
      backdrop-filter: blur(8px) saturate(0.98) brightness(0.92) !important;
      -webkit-backdrop-filter: blur(8px) saturate(0.98) brightness(0.92) !important;
    }
    .memory-card-shell {
      width: min(1080px, calc(100vw - 44px)) !important;
      height: min(690px, calc(100dvh - 44px)) !important;
      background:
        radial-gradient(circle at 26% 10%, rgba(244,213,138,0.075), transparent 36%),
        radial-gradient(circle at 80% 24%, rgba(79,110,38,0.075), transparent 42%),
        linear-gradient(145deg, rgba(16,13,7,0.48), rgba(6,7,4,0.38)) !important;
      backdrop-filter: blur(18px) saturate(112%) !important;
      -webkit-backdrop-filter: blur(18px) saturate(112%) !important;
    }
    .memory-card-popup-body { gap: clamp(16px, 2.4vw, 30px) !important; }
    .memory-card-info-panel { opacity: 0.80 !important; }
    .memory-card-modal-footer button.is-soft-locked,
    .memory-card-modal-footer button[aria-disabled="true"] {
      opacity: 0.82;
      filter: none;
      cursor: pointer;
      background:
        radial-gradient(circle at 16% 0%, rgba(244, 213, 138, 0.12), transparent 44%),
        linear-gradient(145deg, rgba(74, 53, 30, 0.50), rgba(22, 17, 10, 0.42)) !important;
    }

    body.microcosm-memory-card-open .microcosm-ritual-dock {
      opacity: 0.10 !important;
      filter: blur(2px) saturate(0.70) brightness(0.82) !important;
    }

    @media (max-width: 740px) {
      .microcosm-ritual-dock {
        width: calc(100vw - 14px) !important;
        max-width: calc(100vw - 14px) !important;
        padding: 8px !important;
        gap: 6px !important;
        border-radius: 22px !important;
      }
      .ritual-island-label { display: none; }
      .ritual-topline { grid-template-columns: 1fr !important; }
      .ritual-stage-pill,
      .ritual-memory-pill { min-height: 25px !important; }
      .ritual-memory-pill { display: none !important; }
      .ritual-weather-anchor .weather-lite-mainline {
        grid-template-columns: 30px minmax(0, 1fr) auto !important;
        gap: 7px !important;
      }
      .ritual-weather-anchor .weather-lite-clock { font-size: 13px !important; }
      .ritual-weather-anchor .weather-lite-date { display: none !important; }
      .weather-mini-actions { grid-template-columns: auto; }
      .weather-lite-source { display: none !important; }
      .ritual-action-row { gap: 7px !important; }
      .microcosm-ritual-dock .micro-btn { min-height: 51px !important; font-size: 12px !important; }
      .microcosm-ritual-dock .micro-btn small { font-size: 8px !important; }
      .memory-card-shell {
        width: calc(100vw - 16px) !important;
        height: calc(100dvh - 16px) !important;
      }
    }

    @media (max-width: 440px) {
      .ritual-stage-pill b { font-size: 9.8px !important; }
      .ritual-weather-anchor .weather-focus-line strong { font-size: 11.3px !important; }
      .ritual-weather-anchor .weather-focus-line span { display: none !important; }
      .microcosm-ritual-dock .micro-btn { min-height: 48px !important; font-size: 11px !important; }
      .ritual-unlock-link { font-size: 7.7px !important; letter-spacing: .06em !important; }
    }

    @media (max-height: 680px) and (min-width: 741px) {
      .microcosm-ritual-dock {
        width: min(760px, calc(100vw - 24px)) !important;
        grid-template-columns: minmax(180px, .85fr) minmax(240px, 1.1fr) minmax(260px, 1fr) !important;
        align-items: center !important;
      }
      .ritual-island-label { grid-column: 1 / -1; margin-bottom: -4px; }
      .ritual-topline { grid-column: 1; grid-template-columns: 1fr !important; }
      .ritual-progress-anchor { grid-column: 1; }
      .ritual-weather-anchor { grid-column: 2; grid-row: 2 / span 2; }
      .ritual-action-row { grid-column: 3; grid-row: 2; }
      .ritual-unlock-link { grid-column: 3; grid-row: 3; }
      .ritual-memory-pill { display: none !important; }
    }


    /* =====================================================
       V8.27 · MOBILE GROUND / CARD PARITY / ORGANIC SHORE
       - Terre visible autour du gland plus subtile.
       - Ancien proxy géosphère réellement invisible sans ombre.
       - Voir ma carte affiche la texture de carte autonome exportée.
       - Le liseré de rivage suit le tracé organique de l'îlot.
       ===================================================== */
    .memory-card-face.memory-card-front.export-card-front {
      grid-template-rows: 1fr !important;
      gap: 0 !important;
      padding: 0 !important;
      background:
        var(--memory-card-export-texture),
        radial-gradient(circle at 50% 55%, rgba(79,110,38,0.55), rgba(8,7,4,0.90)) !important;
      background-size: cover !important;
      background-position: center !important;
      background-repeat: no-repeat !important;
      border-color: rgba(244,213,138,0.34) !important;
      box-shadow:
        inset 0 0 0 8px rgba(255,248,225,0.025),
        inset 0 0 0 11px rgba(216,171,97,0.08) !important;
    }
    .memory-card-face.memory-card-front.export-card-front > * {
      display: none !important;
    }
    .memory-card-face.memory-card-front.export-card-front::before {
      opacity: .26 !important;
    }
    .memory-card-face.memory-card-front.export-card-front::after {
      content: 'texture autonome' !important;
      position: absolute !important;
      right: 8.4% !important;
      bottom: 6.4% !important;
      z-index: 4 !important;
      padding: .55em .8em !important;
      border-radius: 999px !important;
      border: 1px solid rgba(244,213,138,.22) !important;
      background: rgba(5,5,3,.42) !important;
      color: rgba(255,248,225,.72) !important;
      font: 950 clamp(7px, .95vw, 10px)/1 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
      letter-spacing: .14em !important;
      text-transform: uppercase !important;
      pointer-events: none !important;
    }
    .memory-card-face.memory-card-front.export-card-front + .memory-card-back {
      border-color: rgba(244,213,138,0.28) !important;
    }
    .memory-card-preview-panel::after {
      content: 'Même texture que la carte HTML autonome · toucher pour retourner' !important;
    }
    .memory-card-modal .memory-card-popup-note {
      max-width: 52ch;
    }

  </style>
<script async src="https://cdn.jsdelivr.net/npm/es-module-shims@1.7.1/dist/es-module-shims.min.js"></script>
<script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.183.2/build/three.webgpu.js",
      "three/webgpu": "https://cdn.jsdelivr.net/npm/three@0.183.2/build/three.webgpu.js",
      "three/tsl": "https://cdn.jsdelivr.net/npm/three@0.183.2/build/three.tsl.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/",
      "stats-gl": "https://cdn.jsdelivr.net/npm/stats-gl@2.4.2/dist/main.js"
    }
  }
  </script>
<div id="microcosm-app-root" aria-label="Microcosm immersive WebGPU canvas"></div>
  <div id="vr-status-chip">Préparation VR confortable</div>

  <!-- Panneau d’erreur WebGPU -->
  <div id="no-webgpu">
    <h1>WebGPU non disponible</h1>
    <p>Ce microcosme interactif nécessite l’API WebGPU, qui n’est pas prise en charge par votre navigateur actuel.</p>
    <div class="hint">
      Essayez d’utiliser <strong>Google Chrome 113+</strong> ou <strong>Microsoft Edge 113+</strong> sur bureau.<br>
      Sous macOS, essayez également <strong>Safari Technology Preview</strong>.
    </div>
  </div>
  <!-- Bouton et panneau UI de l’océan -->
  <button id="ui-toggle" title="Paramètres">☰</button>
  <div id="ui-panel"></div>
  <!-- Panneau d’herbe (contrôles) -->
  <!-- Removed separate grass panel; grass controls are integrated into the main UI -->

  <div id="weather-hud" class="weather-lite compact" title="Microclimat utile à la croissance · bouton Affiner pour les détails">
    <div class="weather-lite-mainline">
      <div class="weather-visual-icon" id="weather-visual-icon" aria-label="Icône météo">☾</div>
      <div>
        <div class="weather-lite-clock" id="weather-time">--:--</div>
        <div class="weather-lite-date" id="weather-date">--</div>
      </div>
      <div class="weather-mini-actions">
        <div class="weather-lite-source" id="weather-source">AUTO</div>
        <button class="weather-tune-btn" type="button" aria-label="Affiner la météo" aria-expanded="false">Affiner</button>
      </div>
    </div>
    <div class="weather-focus-line" aria-live="polite">
      <strong id="weather-focus-main">--° · microclimat</strong>
      <span id="weather-focus-sub">Vent -- · humidité --</span>
    </div>
    <div class="weather-lite-values">
      <div><small>Temp.</small><b><span id="weather-temp">--</span>°C</b></div>
      <div><small>Vent</small><b id="weather-wind">--</b></div>
      <div><small>Humidité</small><b id="weather-humidity">--</b></div>
    </div>
    <div class="weather-lite-advice">
      <span id="weather-desc">Microclimat en observation</span>
      <b id="weather-advice">Arrosage à évaluer</b>
    </div>
    <div class="weather-rainline">Jours sans pluie : <b id="weather-rainless-days">--</b></div>
    <div class="weather-lunar-strip weather-lite-advanced">
      <div class="weather-lunar-moon" id="weather-lunar-icon">☾</div>
      <div class="weather-lunar-text">
        <strong id="weather-lunar-phase">Lune en calcul</strong>
        <span id="weather-lunar-detail">Phase · mouvement · type lunaire</span>
      </div>
    </div>
    <div class="weather-lite-advanced weather-location-tools">
      <input id="weather-location-input" type="text" placeholder="Adresse ou ville">
      <button id="weather-location-search" title="Chercher cette météo">OK</button>
      <button id="weather-location-gps" title="Revenir à ma position GPS">GPS</button>
      <button id="weather-refresh" title="Forcer un relevé météo live">↻</button>
    </div>
    <div class="weather-lite-hidden">
      <div id="weather-place">Microclimat local</div>
      <div id="weather-icon"></div>
      <div id="weather-feels">--</div>
      <div id="weather-pressure">--</div>
      <div id="weather-clouds">--</div>
      <div id="weather-rain">--</div>
      <div id="weather-daylight">--</div>
      <div id="weather-sync">--</div>
      <b id="weather-compare-api">--</b>
      <b id="weather-compare-world">--</b>
      <div id="weather-note">--</div>
    </div>
  </div>

  <div class="save-share-dock cta-clean gameplay-bar microcosm-ritual-dock" aria-label="Organic Control Island · rituel principal Microcosm">
    <div class="ritual-island-label" aria-hidden="true">Organic Control Island</div>
    <div class="ritual-topline" aria-live="polite">
      <div class="ritual-stage-pill">
        <span>Cycle vivant</span>
        <b id="save-tree-state-label">Jour 000/365 · graine en éveil</b>
      </div>
      <div class="ritual-memory-pill" title="La progression est automatiquement conservée dans ce navigateur.">
        <span class="ritual-memory-dot" aria-hidden="true"></span>
        Mémoire navigateur auto
      </div>
    </div>
    <div class="ritual-progress-anchor" aria-label="Progression de l’arbre"></div>
    <div class="ritual-weather-anchor" aria-label="Microclimat utile à la croissance"></div>
    <div class="gameplay-main-actions ritual-action-row">
      <button class="micro-btn micro-btn-primary ritual-btn" id="water-tree-btn" type="button">
        Arroser mon arbre
        <small id="water-tree-btn-hint">Rituel volontaire</small>
      </button>
      <button class="micro-btn micro-btn-primary card-btn" id="save-tree-btn" type="button">
        Voir ma carte
        <small id="save-tree-btn-hint">Carte vivante</small>
      </button>
    </div>
    <button class="ritual-unlock-link" id="open-premium-options-btn" type="button">Déverrouiller la mémoire autonome</button>
    <input id="import-settings-file" type="file" accept="application/json,.json" style="display:none">
  </div>
  <div id="storage-warning" hidden><strong>Mémoire locale :</strong> la progression est automatiquement conservée dans ce navigateur. La mémoire HTML autonome est disponible avec l’add-on ou l’arbre à vie.</div>


  <div id="memory-card-modal" class="memory-card-modal" aria-hidden="true" role="dialog" aria-label="Carte mémoire Microcosm flottante" hidden>
    <div class="memory-card-shell" role="document">
      <div class="memory-card-modal-bar">
        <div class="memory-card-modal-title">
          Carte mémoire Microcosm
          <small id="memory-card-modal-subtitle">Viewer collector dynamique · clic/tap pour retourner</small>
        </div>
        <button type="button" class="memory-card-close-x" id="memory-card-close-x" aria-label="Fermer la carte mémoire">×</button>
      </div>
      <div class="memory-card-popup-body">
        <div class="memory-card-preview-panel" id="memory-card-preview-panel" aria-label="Viewer 3D de la carte mémoire">
          <div class="memory-card-viewer" id="memory-card-viewer">
            <button type="button" class="memory-card-flip-btn" id="memory-card-flip-btn">Voir verso</button>
            <div class="memory-card-tilt" id="memory-card-tilt">
              <div class="memory-card-flipper" id="memory-card-flipper">
                <article class="memory-card-face memory-card-front" aria-label="Recto de la carte mémoire">
                  <div class="memory-card-holo-mask" aria-hidden="true"></div>
                  <header class="memory-card-face-header">
                    <span>Microcosm Interactif 9</span>
                    <b id="memory-card-view-edition">#------</b>
                  </header>
                  <h3 id="memory-card-view-title">Graine en éveil</h3>
                  <p id="memory-card-view-rarity">Mémoire vivante</p>
                  <div class="memory-card-view-shot" id="memory-card-view-shot">
                    <div class="memory-card-view-progress">
                      <i id="memory-card-view-progress-bar"></i>
                      <span id="memory-card-view-progress-label">0%</span>
                    </div>
                  </div>
                  <div class="memory-card-view-stats">
                    <div><small>Jour</small><b id="memory-card-view-day">000/365</b></div>
                    <div><small>Temp.</small><b id="memory-card-view-temp">--°C</b></div>
                    <div><small>Vent</small><b id="memory-card-view-wind">-- km/h</b></div>
                    <div><small>Humidité</small><b id="memory-card-view-humidity">--%</b></div>
                    <div><small>Nuages</small><b id="memory-card-view-clouds">--%</b></div>
                    <div><small>Synchro</small><b id="memory-card-view-sync">--</b></div>
                  </div>
                  <footer class="memory-card-view-cert">
                    <img id="memory-card-view-qr" alt="QR AR/XR de la carte mémoire">
                    <span id="memory-card-view-cert">Certificat de graine</span>
                  </footer>
                </article>
                <article class="memory-card-face memory-card-back" aria-label="Verso certificat de la carte mémoire">
                  <div class="memory-card-holo-mask large" aria-hidden="true"></div>
                  <span class="memory-card-back-kicker">Certificat vivant</span>
                  <h3 id="memory-card-back-title">Identité de l’arbre</h3>
                  <div class="memory-card-back-grid">
                    <div><small>Seed public</small><b id="memory-card-back-seed">--</b></div>
                    <div><small>Lieu récent</small><b id="memory-card-back-place">--</b></div>
                    <div><small>Générée</small><b id="memory-card-back-generated">--</b></div>
                    <div><small>Activité</small><b id="memory-card-back-activity">--</b></div>
                  </div>
                  <p id="memory-card-back-note">Carte mémoire autonome prête à être enregistrée.</p>
                  <div class="memory-card-back-ar">
                    <img id="memory-card-back-qr" alt="QR AR/XR de la carte mémoire">
                    <span>AR/XR<br><small id="memory-card-back-url">presentcomposedesign.fr/microcosm-ar</small></span>
                  </div>
                </article>
              </div>
            </div>
          </div>
        </div>
        <div class="memory-card-info-panel">
          <div class="memory-card-kicker" id="memory-card-edition-meta">Carte HTML autonome</div>
          <h2 id="memory-card-title-line">Carte mémoire</h2>
          <p class="memory-card-kicker" id="memory-card-popup-summary">Carte unique : le même HTML autonome sert au viewer et à la sauvegarde payante.</p>
          <div class="memory-card-info-grid" aria-label="Résumé de la carte mémoire">
            <div><small>Jour</small><b id="memory-card-popup-day">--</b></div>
            <div><small>Tier</small><b id="memory-card-popup-tier">--</b></div>
            <div><small>Météo</small><b id="memory-card-popup-weather">--</b></div>
            <div><small>Seed public</small><b id="memory-card-popup-seed">--</b></div>
          </div>
          <p class="memory-card-popup-note" id="memory-card-popup-note">La carte flotte ici. En gratuit, elle reste consultable ; “Enregistrer” sert à déverrouiller la mémoire HTML autonome.</p>
        </div>
      </div>
      <div class="memory-card-modal-footer">
        <button type="button" class="secondary" id="memory-card-close-btn">Fermer</button>
        <div class="memory-card-inline-actions">
          <span id="memory-card-more-options-note" class="memory-card-more-options-note">options dans le HTML autonome</span>
          <button type="button" class="secondary" id="memory-card-open-ar-btn">Ouvrir AR/XR</button>
          <button type="button" id="memory-card-download-btn">Enregistrer</button>
        </div>
      </div>
    </div>
  </div>

  <button id="premium-cta">
    Jouer contre vents et marées
    <small>débloquer l’expérience</small>
  </button>
  <div id="cycle-modal" aria-hidden="true">
    <div class="cycle-card">
      <h2>Ton arbre a accompli son premier cycle.</h2>
      <p>
        365 jours : une année complète de croissance. Tu peux conserver cet arbre,
        créer une card visuelle gratuite, repartir d’une nouvelle graine, ou garder une vraie
        carte mémoire premium pour lui redonner vie plus tard.
      </p>
      <div class="cycle-actions">
        <button id="cycle-keep-btn">Conserver l’arbre<br><small>le garder comme page d’accueil</small></button>
        <button id="cycle-card-btn" class="secondary">Créer une card HTML vivante<br><small>page autonome partageable</small></button>
        <button id="cycle-cut-btn" class="secondary">Couper et recommencer<br><small>nouvelle graine</small></button>
        <button id="cycle-premium-btn" class="premium">Continuer à vie · 19,99€<br><small>carte mémoire illimitée + partage avancé</small></button>
      </div>
    </div>
  </div>


  <div id="premium-panel">
    <div class="premium-title">Jouer contre vents et marées</div>
    <div class="premium-copy" id="premium-copy-main">
      Deux modes de jeu existent : gratuit ou à vie. L’extension “vents & marées” est un supplément séparé,
      activable aussi bien sur le mode gratuit que sur le mode à vie. Elle débloque des outils, artefacts,
      entretiens supplémentaires et le mode accueil plein écran sans HUD.
    </div>
    <div class="premium-list">
      <span id="premium-line-free">Gratuit : 1 clic / jour pendant 365 jours</span>
      <span id="premium-line-life">À vie : arbre éternel + mémoire illimitée</span>
      <span id="premium-line-addon">Add-on “vents & marées” : 4,99€ sur gratuit ou à vie</span>
      <span id="premium-line-addon-features">Débloque 5 clics/jour au départ + outils + artefacts + mode accueil sans HUD</span>
      <span id="premium-line-cards">Cartes : 2D originales en gratuit · version glass collector pour les arbres à vie</span>
    </div>
    <div class="premium-action-group" aria-label="Action premium disponible">
      <div class="premium-action-label">Mode accueil</div>
      <button class="premium-action-btn" id="presentation-fullscreen-btn">
        Plein écran sans HUD
        <small>restaurant · accueil entreprise · sensibilisation “un clic par jour”</small>
      </button>
    </div>
  </div>


  <script type="module">
    import * as THREE from 'three/webgpu';
    import {
      Fn, uniform, float, int, vec3, vec4,
      instancedArray, instanceIndex, positionGeometry, positionWorld,
      // Import additional TSL helpers used in this scene
      uv,
      sin, cos, pow, sqrt, abs, floor, fract, max, max as tslMax, exp,
      smoothstep, mix, select, dot, normalize, time,
      Loop, cameraPosition, hash, atan, clamp, vec2,
      mx_noise_float, deltaTime, PI
    } from 'three/tsl';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    import { VRButton } from 'three/addons/webxr/VRButton.js';
    import { PLYLoader } from 'three/addons/loaders/PLYLoader.js';
    import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
    import { SkyMesh } from 'three/addons/objects/SkyMesh.js';
    import Stats from 'stats-gl';

    document.body.classList.add('microcosm-immersive-page');

    // Vérifier le support WebGPU
    if (!navigator.gpu) {
      document.getElementById('no-webgpu').style.display = 'flex';
      throw new Error('WebGPU not supported');
    }

    // ───── Budget de performance runtime ─────
    // V8.25 : la densité herbe peut réellement monter jusqu'à ×10 sur desktop.
    // Sur mobile, on garde le slider de dev mais le pool GPU est plafonné pour éviter un gel immédiat.
    const MICRO_GRASS_BASE_BLADE_INSTANCES = 65536;
    const microcosmMobileAutoBudget = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
    const microcosmGrassMaxMultiplier = microcosmMobileAutoBudget ? 3 : 10;
    const MICROCOSM_RUNTIME_PERF = {
      maxPixelRatio: 1.35,
      vrComfortPixelRatio: 1.0,
      grassBaseBladeInstances: MICRO_GRASS_BASE_BLADE_INSTANCES,
      grassMaxDensityMultiplier: microcosmGrassMaxMultiplier,
      grassBladeInstances: MICRO_GRASS_BASE_BLADE_INSTANCES * microcosmGrassMaxMultiplier,
      grassGrid: Math.ceil(Math.sqrt(MICRO_GRASS_BASE_BLADE_INSTANCES * microcosmGrassMaxMultiplier)),
      grassUpdateEvery: microcosmGrassMaxMultiplier >= 10 ? 3 : 2,
      oceanFineUpdateEvery: 2,
      statsUpdateEvery: 30,
      hudUpdateMs: 1400,
      mobileAutoBudget: microcosmMobileAutoBudget
    };
    if (MICROCOSM_RUNTIME_PERF.mobileAutoBudget) {
      MICROCOSM_RUNTIME_PERF.maxPixelRatio = 1.0;
      MICROCOSM_RUNTIME_PERF.grassUpdateEvery = 4;
      MICROCOSM_RUNTIME_PERF.oceanFineUpdateEvery = 3;
      MICROCOSM_RUNTIME_PERF.hudUpdateMs = 1800;
    }

    // ───── Initialisation de la scène, de la caméra et du renderer ─────
    const microcosmMount = document.getElementById('microcosm-app-root') || document.body;
    const scene = new THREE.Scene();
    scene.background = new THREE.Color('#9edcff');
    // Increase the field of view for a wider initial perspective
    const camera = new THREE.PerspectiveCamera(78, window.innerWidth / window.innerHeight, 0.01, 1000000);
    camera.position.set(0, 4.5, 14.0);
    const renderer = new THREE.WebGPURenderer({ antialias: true, sampleCount: 2, alpha: false });
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, MICROCOSM_RUNTIME_PERF.maxPixelRatio));
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 0.8;
    renderer.setClearColor(0x9edcff, 1);
    renderer.domElement.style.background = '#9edcff';
    microcosmMount.appendChild(renderer.domElement);
    await renderer.init();
    renderer.xr.enabled = true;
    if ('xr' in navigator) {
      microcosmMount.appendChild(VRButton.createButton(renderer));
    }

    function setMicrocosmHudVisibilityForVR(active) {
      document.body.classList.toggle('microcosm-xr-active', active && vrRuntimeState.hideHudInVR);
      document.body.classList.toggle('microcosm-xr-preparing', false);
    }

    function applyVRRuntimeMode(active) {
      vrRuntimeState.active = active;
      setMicrocosmHudVisibilityForVR(active);

      if (active) {
        vrRuntimeState.saved.grassDensity = grassDensity?.value ?? null;
        vrRuntimeState.saved.grassDensityMultiplier = grassDensityMultiplier?.value ?? null;
        vrRuntimeState.saved.shoreOpacity = shoreFadeState.opacity;
        vrRuntimeState.saved.foamIntensity = foamIntensity?.value ?? null;
        vrRuntimeState.saved.heightScale = heightScale?.value ?? null;
        vrRuntimeState.saved.choppiness = choppiness?.value ?? null;

        renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, vrRuntimeState.comfortPixelRatio));

        if (vrRuntimeState.reduceParticles) {
          grassDensity.value = Math.min(grassDensity.value, 1.05);
          grassDensityMultiplier.value = Math.min(grassDensityMultiplier.value, MICROCOSM_RUNTIME_PERF.mobileAutoBudget ? 1.15 : 1.75);
          shoreFadeState.opacity *= 0.68;
          if (shoreFadePoints) shoreFadePoints.material.opacity = shoreFadeState.enabled ? shoreFadeState.opacity : 0;
        }
        if (vrRuntimeState.reduceOceanWhileVR) {
          heightScale.value = Math.min(heightScale.value, 0.62);
          choppiness.value = Math.min(choppiness.value, 0.82);
          foamIntensity.value = Math.min(foamIntensity.value, 1.05);
        }
      } else {
        renderer.setPixelRatio(vrRuntimeState.normalPixelRatio);

        if (vrRuntimeState.saved.grassDensity !== null) grassDensity.value = vrRuntimeState.saved.grassDensity;
        if (vrRuntimeState.saved.grassDensityMultiplier !== null) grassDensityMultiplier.value = vrRuntimeState.saved.grassDensityMultiplier;
        if (vrRuntimeState.saved.shoreOpacity !== null) {
          shoreFadeState.opacity = vrRuntimeState.saved.shoreOpacity;
          if (shoreFadePoints) shoreFadePoints.material.opacity = shoreFadeState.enabled ? shoreFadeState.opacity : 0;
        }
        if (vrRuntimeState.saved.foamIntensity !== null) foamIntensity.value = vrRuntimeState.saved.foamIntensity;
        if (vrRuntimeState.saved.heightScale !== null) heightScale.value = vrRuntimeState.saved.heightScale;
        if (vrRuntimeState.saved.choppiness !== null) choppiness.value = vrRuntimeState.saved.choppiness;
        vrRuntimeState.saved.grassDensity = null;
        vrRuntimeState.saved.grassDensityMultiplier = null;
        vrRuntimeState.saved.shoreOpacity = null;
        vrRuntimeState.saved.foamIntensity = null;
        vrRuntimeState.saved.heightScale = null;
        vrRuntimeState.saved.choppiness = null;
      }
    }

    renderer.xr.addEventListener('sessionstart', () => {
      document.body.classList.add('microcosm-xr-preparing');
      setTimeout(() => applyVRRuntimeMode(true), 120);
    });
    renderer.xr.addEventListener('sessionend', () => {
      applyVRRuntimeMode(false);
    });

    // ───── Contrôles orbitaux unifiés ─────
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.target.set(0, 0.9, 0);
    camera.lookAt(controls.target);

    // Caméra libre : plus de verrouillage d'angle, de distance ou d'azimut.
    // L'auto-follow de l'icosaèdre se désactive dès que l'utilisateur manipule la caméra.
    controls.enableDamping = true;
    controls.dampingFactor = 0.055;
    controls.enableZoom = true;
    controls.enablePan = true;
    controls.enableRotate = true;
    controls.minDistance = 0.01;
    controls.maxDistance = Infinity;
    controls.minPolarAngle = 0;
    controls.maxPolarAngle = Math.PI;
    controls.minAzimuthAngle = -Infinity;
    controls.maxAzimuthAngle = Infinity;
    controls.zoomSpeed = 1.35;
    controls.rotateSpeed = 0.85;
    controls.panSpeed = 0.9;
    controls.screenSpacePanning = true;
    if ('zoomToCursor' in controls) controls.zoomToCursor = true;

    let cameraAutoFollow = true;
    // Dès que l'utilisateur zoome/pivote/pan, la caméra devient volontairement manuelle.
    // Un clic de croissance ne doit plus jamais provoquer de dézoom/recentrage automatique.
    let userCameraLocked = false;
    let openingGrassSplashActive = true;
    const disableCameraAutoFollow = () => {
      openingGrassSplashActive = false;
      cameraAutoFollow = false;
      userCameraLocked = true;
    };
    const restoreCameraAutoFollow = ({ clearUserLock = false } = {}) => {
      if (clearUserLock) userCameraLocked = false;
      if (!userCameraLocked) cameraAutoFollow = true;
    };

    // Splashscreen d'ouverture uniquement : caméra basse dans l'herbe, un peu moins zoomée
    // que la version précédente. Dès le premier clic, on reprend la caméra de croissance validée.
    const openingGrassCameraPos = new THREE.Vector3(0.0, 0.88, 11.5);
    const openingGrassCameraLook = new THREE.Vector3(0.0, 0.96, -48.0);
    function setOpeningGrassSplashCamera() {
      camera.position.copy(openingGrassCameraPos);
      controls.target.copy(openingGrassCameraLook);
      camera.lookAt(openingGrassCameraLook);
      camera.updateProjectionMatrix();
      controls.update();
    }
    function exitOpeningGrassSplash() {
      if (!openingGrassSplashActive) return;
      openingGrassSplashActive = false;
      restoreCameraAutoFollow({ clearUserLock: true });
    }
    const disableCameraAutoFollowAfterSplash = () => {
      if (openingGrassSplashActive) return;
      disableCameraAutoFollow();
    };
    controls.addEventListener('start', disableCameraAutoFollow);
    renderer.domElement.addEventListener('wheel', disableCameraAutoFollow, { passive: true });
    renderer.domElement.addEventListener('pointerdown', disableCameraAutoFollowAfterSplash, { passive: true });
    renderer.domElement.addEventListener('touchstart', disableCameraAutoFollowAfterSplash, { passive: true });
    window.addEventListener('keydown', (e) => {
      if (e.code === 'KeyC') {
        openingGrassSplashActive = false;
        userCameraLocked = cameraAutoFollow;
        cameraAutoFollow = !cameraAutoFollow;
      }
    });

    // ───── Statistiques GPU/CPU (optionnel) ─────
    const stats = new Stats({ trackGPU: false });
    stats.init(renderer);
    // document.body.appendChild(stats.dom);

    // ───── Éclairage global ─────
    const hemiLight = new THREE.HemisphereLight(0x87ceeb, 0x1a3a5c, 0.3);
    scene.add(hemiLight);
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    scene.add(ambientLight);
    // Soleil pour l’océan et le ciel
    const sunLight = new THREE.DirectionalLight(0xffecd0, 4.5);
    scene.add(sunLight);
    // Lumière supplémentaire pour l’arbre
    const treeDirLight = new THREE.DirectionalLight(0xfff8f0, 1.8);
    treeDirLight.position.set(10, 20, 8);
    treeDirLight.castShadow = true;
    treeDirLight.shadow.mapSize.set(2048, 2048);
    treeDirLight.shadow.camera.left = -20;
    treeDirLight.shadow.camera.right = 20;
    treeDirLight.shadow.camera.top = 20;
    treeDirLight.shadow.camera.bottom = -20;
    treeDirLight.shadow.camera.near = 1;
    treeDirLight.shadow.camera.far = 50;
    treeDirLight.shadow.bias = -0.001;
    scene.add(treeDirLight);
    const rimLight = new THREE.DirectionalLight(0xffd4c4, 0.5);
    rimLight.position.set(-8, 10, -10);
    scene.add(rimLight);
    const fillLight = new THREE.DirectionalLight(0xffe8e0, 0.3);
    fillLight.position.set(-5, 5, 10);
    scene.add(fillLight);
    const pointLight = new THREE.PointLight(0xffccbb, 0.4, 30);
    pointLight.position.set(5, 3, 5);
    scene.add(pointLight);

    // ───── Sky and Clouds (océan) ─────
    const sunDir = uniform(new THREE.Vector3());

    // Ciel principal robuste WebGPU + WebXR.
    // Correction réelle du noir : le dôme précédent avait un rayon trop grand / matériau node fragile.
    // Ici : sphère complète, rayon inférieur au camera.far, MeshBasicMaterial + vertex colors.
    const skyDomeGeo = new THREE.SphereGeometry(18000, 96, 48);
    const skyColors = [];
    const skyPosAttr = skyDomeGeo.getAttribute('position');
    const cHorizon = new THREE.Color(0xffdfa3);
    const cLow = new THREE.Color(0xcff2ff);
    const cMid = new THREE.Color(0x9edcff);
    const cHigh = new THREE.Color(0x74bff2);
    const cTmp = new THREE.Color();
    for (let i = 0; i < skyPosAttr.count; i++) {
      const y = skyPosAttr.getY(i) / 18000;
      const t = THREE.MathUtils.clamp((y + 0.18) / 1.18, 0, 1);
      if (t < 0.32) {
        cTmp.copy(cHorizon).lerp(cLow, t / 0.32);
      } else if (t < 0.72) {
        cTmp.copy(cLow).lerp(cMid, (t - 0.32) / 0.40);
      } else {
        cTmp.copy(cMid).lerp(cHigh, (t - 0.72) / 0.28);
      }
      skyColors.push(cTmp.r, cTmp.g, cTmp.b);
    }
    skyDomeGeo.setAttribute('color', new THREE.Float32BufferAttribute(skyColors, 3));
    const skyDomeMat = new THREE.MeshBasicMaterial({
      side: THREE.DoubleSide,
      vertexColors: true,
      depthWrite: false,
      depthTest: false,
      fog: false,
      toneMapped: false
    });
    const skyDome = new THREE.Mesh(skyDomeGeo, skyDomeMat);
    skyDome.renderOrder = -1000000;
    skyDome.frustumCulled = false;
    const _skyFollowPos = new THREE.Vector3();
    skyDome.onBeforeRender = (_renderer, _scene, activeCamera) => {
      activeCamera.getWorldPosition(_skyFollowPos);
      skyDome.position.copy(_skyFollowPos);
      skyDome.updateMatrixWorld(true);
    };
    scene.add(skyDome);

    // Skybox PLY externe : activable depuis les réglages, mais désactivée automatiquement en XR.
    const PLY_SKYBOX_URL = 'https://presentcomposedesign.fr/wp-content/uploads/2026/05/Reflective_Sky_and_Water.compressed.ply';
    let plySkybox = null;
    let plySkyboxWanted = false;
    const plySkyboxState = { loaded: false, error: false, loading: true };
    function applyPlySkyboxVisibility() {
      if (!plySkybox) return;
      const inXR = renderer.xr.isPresenting === true;
      plySkybox.visible = plySkyboxWanted && !inXR;
      skyDome.visible = !plySkybox.visible;
    }
    renderer.xr.addEventListener('sessionstart', applyPlySkyboxVisibility);
    renderer.xr.addEventListener('sessionend', applyPlySkyboxVisibility);

    const plyLoader = new PLYLoader();
    plyLoader.load(PLY_SKYBOX_URL, (geometry) => {
      geometry.computeBoundingBox();
      geometry.computeVertexNormals?.();
      const box = geometry.boundingBox;
      const center = new THREE.Vector3();
      const size = new THREE.Vector3();
      box.getCenter(center);
      box.getSize(size);
      geometry.translate(-center.x, -center.y, -center.z);

      const hasVertexColors = !!geometry.getAttribute('color');
      const skyMat = new THREE.MeshBasicMaterial({
        side: THREE.DoubleSide,
        vertexColors: hasVertexColors,
        color: hasVertexColors ? 0xffffff : 0x9edcff,
        depthWrite: false,
        depthTest: false,
        fog: false,
        toneMapped: false
      });
      plySkybox = new THREE.Mesh(geometry, skyMat);
      const maxDim = Math.max(size.x, size.y, size.z) || 1;
      plySkybox.scale.setScalar(26000 / maxDim);
      plySkybox.renderOrder = -999999;
      plySkybox.frustumCulled = false;
      plySkybox.onBeforeRender = (_renderer, _scene, activeCamera) => {
        activeCamera.getWorldPosition(_skyFollowPos);
        plySkybox.position.copy(_skyFollowPos);
        plySkybox.updateMatrixWorld(true);
      };
      scene.add(plySkybox);
      plySkyboxState.loaded = true;
      plySkyboxState.loading = false;
      plySkyboxWanted = false;
      applyPlySkyboxVisibility();
    }, undefined, (err) => {
      console.warn('Impossible de charger la skybox PLY :', err);
      plySkyboxState.error = true;
      plySkyboxState.loading = false;
      skyDome.visible = true;
    });

    // Fallback non-XR : même si un navigateur ignore le dôme, le clear + fond CSS ne redeviennent jamais noirs.
    // SkyMesh conservé seulement comme référence de paramètres, mais jamais ajouté à la scène.
    const sky = new SkyMesh();
    sky.visible = false;

    // Couche de nuages
    const cloudCoverage = uniform(0.45);
    const cloudDensity = uniform(0.8);
    const cloudSpeed = uniform(0.006);
    function createCloudLayer(radius, yPos, scaleUV, alpha, geo) {
      const mat = new THREE.MeshBasicNodeMaterial({ side: THREE.BackSide, transparent: true, depthWrite: false, depthTest: false });
      mat.colorNode = Fn(() => {
        const wPos = positionWorld;
        const uv = vec2(wPos.x, wPos.z).mul(scaleUV);
        const t = time.mul(cloudSpeed);
        const warpN = mx_noise_float(uv.mul(0.7).add(vec2(t.mul(0.5), t.mul(-0.3))));
        const warpedUV = uv.add(vec2(warpN.mul(0.15), warpN.mul(0.12)));
        const n1 = mx_noise_float(warpedUV.mul(1.0).add(vec2(t, t.mul(0.3))));
        const n2 = mx_noise_float(warpedUV.mul(2.2).add(vec2(t.mul(-0.6), t.mul(0.8))));
        const n3 = mx_noise_float(warpedUV.mul(5.0).add(vec2(t.mul(0.9), t.mul(-0.5))));
        const n4 = mx_noise_float(warpedUV.mul(10.0).add(vec2(t.mul(-0.4), t.mul(1.1))));
        const n5 = mx_noise_float(warpedUV.mul(20.0).add(vec2(t.mul(1.2), t.mul(0.4))));
        const cloud = n1.mul(0.4).add(n2.mul(0.25)).add(n3.mul(0.18)).add(n4.mul(0.1)).add(n5.mul(0.07));
        const coverageThresh = float(1.0).sub(cloudCoverage);
        const cloudMask = smoothstep(coverageThresh, coverageThresh.add(0.2), cloud);
        const densityMask = pow(cloudMask, float(0.6)).mul(cloudDensity);
        const L = normalize(sunDir);
        const cloudNormalApprox = normalize(vec3(
          mx_noise_float(warpedUV.add(vec2(0.001, 0.0))).sub(n1),
          float(0.08),
          mx_noise_float(warpedUV.add(vec2(0.0, 0.001))).sub(n1)
        ));
        const NdotL = max(dot(cloudNormalApprox, L), float(0.0));
        const sunColorWarm = vec3(1.0, 0.92, 0.8);
        const sunColorCool = vec3(0.95, 0.97, 1.0);
        const sunTint = mix(sunColorWarm, sunColorCool, smoothstep(float(0.0), float(30.0), float(params.sunElevation)));
        const brightColor = sunTint;
        const midColor = vec3(0.78, 0.8, 0.85);
        const shadowColor = vec3(0.4, 0.42, 0.52);
        const darkEdge = vec3(0.28, 0.3, 0.38);
        const lightMix = NdotL.mul(0.6).add(cloud.mul(0.4));
        const col1 = mix(darkEdge, shadowColor, smoothstep(float(0.0), float(0.3), lightMix));
        const col2 = mix(col1, midColor, smoothstep(float(0.3), float(0.55), lightMix));
        const cloudCol = mix(col2, brightColor, smoothstep(float(0.55), float(0.85), lightMix));
        const edgeMask = smoothstep(float(0.0), float(0.15), cloudMask).mul(smoothstep(float(0.5), float(0.15), cloudMask));
        const silverLining = edgeMask.mul(0.35).mul(max(NdotL, float(0.2)));
        const finalCol = cloudCol.add(vec3(silverLining));
        const horizonFade = smoothstep(float(0.0), float(0.15), normalize(wPos).y);
        return vec4(finalCol, densityMask.mul(alpha).mul(horizonFade));
      })();
      const mesh = new THREE.Mesh(geo, mat);
      mesh.position.y = yPos;
      mesh.renderOrder = -999000;
      mesh.frustumCulled = false;
      return mesh;
    }
    const cloudGeo1 = new THREE.SphereGeometry(13000, 48, 24, 0, Math.PI * 2, 0, Math.PI * 0.38);
    const cloudLayer1 = createCloudLayer(13000, 0, 0.00008, 0.75, cloudGeo1);
    // Désactivé : ces calottes transparentes WebGPU rendaient parfois une grande zone noire.
    cloudLayer1.visible = false;
    const cloudGeo2 = new THREE.SphereGeometry(13500, 40, 20, 0, Math.PI * 2, 0, Math.PI * 0.32);
    const cloudLayer2 = createCloudLayer(13500, 200, 0.00005, 0.35, cloudGeo2);
    // Désactivé pour garder un ciel jour complet en desktop et en XR.
    cloudLayer2.visible = false;

    // ───── Paramètres globaux pour l’océan ─────
    const params = {
      windSpeed: 9.5,
      choppiness: 0.62,
      heightScale: 0.58,
  // Lighten the base water color so the ocean is visible by default
  waterColor: '#042c4e',
      sssColor: '#0a5c5a',
      sssIntensity: 0.9,
      roughness: 0.08,
      fresnelPower: 4.5,
      fresnelStrength: 0.75,
      skyColor: '#4a6d8c',
      sunElevation: 2,
      sunAzimuth: 187,
      foamColor: '#ffffff',
      foamThreshold: 0.72,
      foamPower: 1.75,
      foamIntensity: 0.92,
      fbmNoiseFreq: 0.8,
      fbmNoiseAmp: 0.35,
      depthBlend: 0.55,
      reflectionStrength: 0.46,
      reflectionDistortion: 0.04,
      bumpStrength: 0.135,
      bumpFrequency: 0.42,
      fogDensity: 0.5,
      fogNear: 100,
      fogFar: 1800,
      fogColor: '#47616d'
    };
    // Uniforms océan
    const windSpeedU = uniform(params.windSpeed);
    const windDir = uniform(Math.PI * 0.25);
    const choppiness = uniform(params.choppiness);
    const heightScale = uniform(params.heightScale);
    const waterColor = uniform(new THREE.Color(params.waterColor));
    const sssColor = uniform(new THREE.Color(params.sssColor));
    const sssIntensity = uniform(params.sssIntensity);
    const roughness_u = uniform(params.roughness);
    const fresnelPower = uniform(params.fresnelPower);
    const fresnelStrength = uniform(params.fresnelStrength);
    const skyColor = uniform(new THREE.Color(params.skyColor));
    const foamColorU = uniform(new THREE.Color(params.foamColor));
    const foamThreshold = uniform(params.foamThreshold);
    const foamPower = uniform(params.foamPower);
    const foamIntensity = uniform(params.foamIntensity);
    // V8.27 · Le rivage et la mousse suivent enfin le même path organique que la terre.
    // On projette la distance océan sur une distance radiale déformée par la même signature de forme.
    const oceanIslandShapePhaseA = uniform(0.0);
    const oceanIslandShapePhaseB = uniform(0.0);
    const oceanIslandShapePhaseC = uniform(0.0);
    const oceanIslandShapeRoundness = uniform(0.16);
    const fbmNoiseFreqU = uniform(params.fbmNoiseFreq);
    const fbmNoiseAmpU = uniform(params.fbmNoiseAmp);
    const depthBlend = uniform(params.depthBlend);
    const reflectionStrength = uniform(params.reflectionStrength);
    const reflectionDistortion = uniform(params.reflectionDistortion);
    const bumpStrengthU = uniform(params.bumpStrength);
    const bumpNoiseType = uniform(5);
    const bumpFrequencyU = uniform(params.bumpFrequency);
    const fogDensity = uniform(params.fogDensity);
    const fogNear = uniform(params.fogNear);
    const fogFar = uniform(params.fogFar);
    const fogColorU = uniform(new THREE.Color(params.fogColor));
    // Courbure commune kokedama : même logique pour eau, terre et herbe.
    // L'eau est courbée dans le shader pour éviter un horizon plat, mais reste dissociée du sol fixe.
    const oceanPlanetCurveStrength = uniform(0.00024);
    const oceanPlanetCurveLimit = uniform(-38.0);
    // L'océan Alejandro est directement sphérisé : la courbure est appliquée au maillage dynamique,
    // pas à une sphère décorative ajoutée sous la scène.
    const oceanCurveAmount = uniform(1.0);
    const oceanSphereRadiusU = uniform(4800.0);
    const oceanIslandCutRadius = uniform(14.0);
    const oceanIslandCutFeather = uniform(2.2);
    // Zone de rivage : elle neutralise physiquement la houle près de l'îlot,
    // afin que les vagues ne traversent pas la terre/herbe.
    const oceanShoreCalmDistance = uniform(6.2);
    const oceanShoreMinimumWave = uniform(0.035);
    const oceanShoreFoamStrength = uniform(0.68);
    const oceanMatchedCurveStrength = uniform(0.0026);
    const foamNoiseType = uniform(5);
    const heightColorLow = uniform(new THREE.Color('#021a35'));
    const heightColorMid = uniform(new THREE.Color('#064273'));
    const heightColorHigh = uniform(new THREE.Color('#1a8a7d'));
    const heightColorPeak = uniform(new THREE.Color('#5ec4b0'));
    const heightColorIntensity = uniform(0.6);
    // Ocean compute constants
    const G = 9.81;
    const FFT_SIZE = 256;
    const FFT_SIZE_SQ = FFT_SIZE * FFT_SIZE;
    const GRID_K = 16;
    const WAVES_PER_CASCADE = GRID_K * GRID_K;
    const TOTAL_WAVES = WAVES_PER_CASCADE * 2;
    const PATCH_COARSE = 250;
    const PATCH_FINE = 37;
    // GPU buffers for ocean
    const waveData = instancedArray(TOTAL_WAVES, 'vec4');
    const waveExtra = instancedArray(TOTAL_WAVES, 'vec4');
    const dispGridCoarse = instancedArray(FFT_SIZE_SQ, 'vec4');
    const dispGridFine = instancedArray(FFT_SIZE_SQ, 'vec4');
    const slopeGridCoarse = instancedArray(FFT_SIZE_SQ, 'vec4');
    const slopeGridFine = instancedArray(FFT_SIZE_SQ, 'vec4');
    // Init JONSWAP spectrum
    const TWO_PI = float(Math.PI * 2);
    const initWaves = Fn(() => {
      const i = instanceIndex;
      const cascadeIdx = i.div(WAVES_PER_CASCADE);
      const localIdx = i.mod(WAVES_PER_CASCADE);
      const nx = localIdx.mod(GRID_K).sub(GRID_K / 2).toFloat();
      const nz = localIdx.div(GRID_K).sub(GRID_K / 2).toFloat();
      const patchSize = select(cascadeIdx.equal(0), float(PATCH_COARSE), float(PATCH_FINE));
      const dk = TWO_PI.div(patchSize);
      const kx = nx.mul(dk);
      const kz = nz.mul(dk);
      const k = sqrt(kx.mul(kx).add(kz.mul(kz))).max(0.0001);
      const omega = sqrt(float(G).mul(k));
      const dirX = kx.div(k);
      const dirZ = kz.div(k);
      const wSpeed = tslMax(windSpeedU, float(0.5));
      const L = wSpeed.mul(wSpeed).div(float(G));
      const kL = k.mul(L);
      const k4 = k.mul(k).mul(k).mul(k);
      const phillips = float(0.01).div(k4).mul(exp(float(-1.0).div(kL.mul(kL))));
      const omegaP = float(G).mul(0.87).div(wSpeed);
      const sigma = select(omega.lessThanEqual(omegaP), float(0.07), float(0.09));
      const rArg = omega.sub(omegaP).mul(omega.sub(omegaP)).negate().div(float(2.0).mul(sigma).mul(sigma).mul(omegaP).mul(omegaP));
      const jonswap = pow(float(3.3), exp(rArg));
      const waveAngle = atan(kz, kx);
      const cosA = cos(waveAngle.sub(windDir));
      const directional = pow(tslMax(cosA, float(0)), 2);
      const suppress = exp(k.mul(k).mul(-0.0001));
      const S = phillips.mul(jonswap).mul(directional).mul(suppress);
      const isCenter = abs(nx).lessThan(0.5).and(abs(nz).lessThan(0.5));
      const amp = select(isCenter, float(0), sqrt(S).mul(dk).mul(heightScale));
      const phase = hash(i.add(12345)).mul(TWO_PI);
      waveData.element(i).assign(vec4(dirX, dirZ, omega, amp));
      waveExtra.element(i).assign(vec4(k, phase, float(0), float(0)));
    })().compute(TOTAL_WAVES);
    // Compute ocean displacement per cascade
    function makeOceanCompute(dispGrid, slopeGrid, waveStart, waveCount, patchSizeVal) {
      const ps = float(patchSizeVal);
      return Fn(() => {
        const idx = instanceIndex;
        const gx = idx.mod(FFT_SIZE);
        const gz = idx.div(FFT_SIZE);
        const wx = gx.toFloat().div(float(FFT_SIZE)).mul(ps);
        const wz = gz.toFloat().div(float(FFT_SIZE)).mul(ps);
        const dy = float(0).toVar();
        const dx = float(0).toVar();
        const dz = float(0).toVar();
        const slopeX = float(0).toVar();
        const slopeZ = float(0).toVar();
        const Jxx = float(0).toVar();
        const Jzz = float(0).toVar();
        const Jxz = float(0).toVar();
        Loop(waveCount, ({ i }) => {
          const wi = i.add(waveStart);
          const wd = waveData.element(wi);
          const we = waveExtra.element(wi);
          const dX = wd.x;
          const dZ = wd.y;
          const omega = wd.z;
          const amp_w = wd.w;
          const k = we.x;
          const phase = we.y;
          const theta = k.mul(dX.mul(wx).add(dZ.mul(wz))).sub(omega.mul(time)).add(phase);
          const c = cos(theta);
          const s = sin(theta);
          dy.addAssign(amp_w.mul(c));
          dx.subAssign(amp_w.mul(dX).mul(s));
          dz.subAssign(amp_w.mul(dZ).mul(s));
          slopeX.subAssign(amp_w.mul(k).mul(dX).mul(s));
          slopeZ.subAssign(amp_w.mul(k).mul(dZ).mul(s));
          Jxx.subAssign(amp_w.mul(k).mul(dX).mul(dX).mul(c));
          Jzz.subAssign(amp_w.mul(k).mul(dZ).mul(dZ).mul(c));
          Jxz.subAssign(amp_w.mul(k).mul(dX).mul(dZ).mul(c));
        });
        dispGrid.element(idx).assign(vec4(dx.mul(choppiness), dy, dz.mul(choppiness), Jxz.mul(choppiness)));
        slopeGrid.element(idx).assign(vec4(slopeX, slopeZ, Jxx.mul(choppiness), Jzz.mul(choppiness)));
      })().compute(FFT_SIZE_SQ);
    }
    const computeOceanCoarse = makeOceanCompute(dispGridCoarse, slopeGridCoarse, 0, WAVES_PER_CASCADE, PATCH_COARSE);
    const computeOceanFine = makeOceanCompute(dispGridFine, slopeGridFine, WAVES_PER_CASCADE, WAVES_PER_CASCADE, PATCH_FINE);
    function sampleGrid(grid, wx, wz, patchSizeVal) {
      const ps = float(patchSizeVal);
      const u = fract(wx.div(ps)).mul(float(FFT_SIZE));
      const v = fract(wz.div(ps)).mul(float(FFT_SIZE));
      const ix = floor(u).toInt();
      const iz = floor(v).toInt();
      const fx = fract(u);
      const fz = fract(v);
      const N = int(FFT_SIZE);
      const i00 = iz.mul(N).add(ix);
      const i10 = iz.mul(N).add(ix.add(1).mod(N));
      const i01 = iz.add(1).mod(N).mul(N).add(ix);
      const i11 = iz.add(1).mod(N).mul(N).add(ix.add(1).mod(N));
      return mix(
        mix(grid.element(i00), grid.element(i10), fx),
        mix(grid.element(i01), grid.element(i11), fx),
        fz
      );
    }
    // Gerstner swells
    const SWELLS = [
      { dx: 0.9, dz: 0.44, wavelength: 120, steepness: 0.18 },
      { dx: -0.3, dz: 0.95, wavelength: 80, steepness: 0.13 },
      { dx: 0.6, dz: -0.8, wavelength: 200, steepness: 0.10 },
      { dx: 0.7, dz: 0.7, wavelength: 400, steepness: 0.06 },
      { dx: -0.5, dz: 0.86, wavelength: 600, steepness: 0.04 },
      { dx: 0.4, dz: 0.92, wavelength: 55, steepness: 0.12 }
    ];
    const SWELL_PARAMS = SWELLS.map((sw) => {
      const len = Math.sqrt(sw.dx * sw.dx + sw.dz * sw.dz);
      const ndx = sw.dx / len;
      const ndz = sw.dz / len;
      const k = (2 * Math.PI) / sw.wavelength;
      const omega = Math.sqrt(G * k);
      const amp = sw.steepness / k;
      return { ndx, ndz, k, omega, amp };
    });
    function buildGerstnerSwells(wx, wz) {
      let dy = float(0);
      let dx = float(0);
      let dz = float(0);
      for (const p of SWELL_PARAMS) {
        const theta = float(p.k).mul(float(p.ndx).mul(wx).add(float(p.ndz).mul(wz))).sub(float(p.omega).mul(time));
        const c = cos(theta);
        const s = sin(theta);
        dy = dy.add(float(p.amp).mul(c));
        dx = dx.sub(float(p.amp * p.ndx).mul(s));
        dz = dz.sub(float(p.amp * p.ndz).mul(s));
      }
      return { dx, dy, dz };
    }
    function buildGerstnerSwellJacobian(wx, wz) {
      let Jxx = float(0);
      let Jzz = float(0);
      let Jxz = float(0);
      for (const p of SWELL_PARAMS) {
        const theta = float(p.k).mul(float(p.ndx).mul(wx).add(float(p.ndz).mul(wz))).sub(float(p.omega).mul(time));
        const c = cos(theta);
        Jxx = Jxx.sub(float(p.amp * p.k * p.ndx * p.ndx).mul(c));
        Jzz = Jzz.sub(float(p.amp * p.k * p.ndz * p.ndz).mul(c));
        Jxz = Jxz.sub(float(p.amp * p.k * p.ndx * p.ndz).mul(c));
      }
      return { Jxx, Jzz, Jxz };
    }
    // Géométrie réelle de l'océan : disque radial, puis sphérisation dans le shader.
    // On n'utilise plus de plaque carrée ni de sphère décorative ajoutée.
    // Le shader Alejandro reste appliqué sur ce maillage circulaire et courbé.
    const OCEAN_DISC_RADIUS = 1350.0;
    const OCEAN_EDGE_FADE_START = 1080.0;
    const OCEAN_EDGE_FADE_END = 1340.0;
    function buildSphericalOceanDiscGeometry(radius = OCEAN_DISC_RADIUS, radialSteps = 192, segments = 512) {
      const positions = [];
      const normals = [];
      const uvs = [];
      const indices = [];
      for (let r = 0; r <= radialSteps; r++) {
        const rn = r / radialSteps;
        const rr = radius * Math.pow(rn, 0.92);
        for (let s = 0; s < segments; s++) {
          const a = (s / segments) * Math.PI * 2;
          const x = Math.cos(a) * rr;
          const z = Math.sin(a) * rr;
          positions.push(x, 0, z);
          normals.push(0, 1, 0);
          uvs.push(0.5 + (x / radius) * 0.5, 0.5 + (z / radius) * 0.5);
        }
      }
      for (let r = 0; r < radialSteps; r++) {
        for (let s = 0; s < segments; s++) {
          const a = r * segments + s;
          const b = r * segments + ((s + 1) % segments);
          const c = (r + 1) * segments + s;
          const d = (r + 1) * segments + ((s + 1) % segments);
          indices.push(a, c, b, b, c, d);
        }
      }
      const geo = new THREE.BufferGeometry();
      geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
      geo.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
      geo.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
      geo.setIndex(indices);
      geo.computeVertexNormals();
      return geo;
    }
    const oceanGeo = buildSphericalOceanDiscGeometry();
    const oceanMat = new THREE.MeshBasicNodeMaterial({ side: THREE.DoubleSide });
    // Node for position
    oceanMat.positionNode = Fn(() => {
      const wx = positionGeometry.x;
      const wz = positionGeometry.z;
      const dCoarse = sampleGrid(dispGridCoarse, wx, wz, PATCH_COARSE);
      const dFine = sampleGrid(dispGridFine, wx, wz, PATCH_FINE);
      const swell = buildGerstnerSwells(wx, wz);
      const baseDist = sqrt(wx.mul(wx).add(wz.mul(wz)));
      const shoreAngle = atan(wz, wx);
      const shoreLobeA = sin(shoreAngle.mul(3.0).add(oceanIslandShapePhaseA));
      const shoreLobeB = sin(shoreAngle.mul(5.0).add(oceanIslandShapePhaseB)).mul(0.55);
      const shoreLobeC = sin(shoreAngle.mul(7.0).add(oceanIslandShapePhaseC)).mul(0.20);
      const shoreShapeScale = tslMax(float(0.72), float(1.0).add(shoreLobeA.add(shoreLobeB).mul(oceanIslandShapeRoundness)).add(shoreLobeC.mul(0.07)));
      const islandOrganicDist = baseDist.div(shoreShapeScale);
      // Collision visuelle : la houle est étouffée au plus près de la vraie silhouette organique de l'îlot.
      // La zone calme n'est donc plus une simple couronne circulaire.
      const shoreEnergy = mix(
        oceanShoreMinimumWave,
        float(1.0),
        smoothstep(oceanIslandCutRadius, oceanIslandCutRadius.add(oceanShoreCalmDistance), islandOrganicDist)
      );
      const totalDx = dCoarse.x.add(dFine.x).add(swell.dx).mul(shoreEnergy);
      const totalDy = dCoarse.y.add(dFine.y).add(swell.dy).mul(shoreEnergy);
      const totalDz = dCoarse.z.add(dFine.z).add(swell.dz).mul(shoreEnergy);
      const oceanX = wx.add(totalDx);
      const oceanZ = wz.add(totalDz);
      const oceanR2 = oceanX.mul(oceanX).add(oceanZ.mul(oceanZ));
      // Courbure par défaut accordée à celle de l'îlot : même coefficient local près du micro-monde.
      // Le clamp évite que la calotte tombe trop bas aux très grandes distances.
      const matchedIslandSag = oceanR2.mul(oceanMatchedCurveStrength).negate().max(oceanPlanetCurveLimit);
      const sphericalCapSag = sqrt(oceanSphereRadiusU.mul(oceanSphereRadiusU).sub(oceanR2).max(float(1.0)))
        .sub(oceanSphereRadiusU)
        .max(oceanPlanetCurveLimit);
      const sphericalSag = mix(sphericalCapSag, matchedIslandSag, float(0.92)).mul(oceanCurveAmount);
      return vec3(oceanX, totalDy.add(sphericalSag), oceanZ);
    })();
    oceanMat.colorNode = Fn(() => {
      const wx = positionGeometry.x;
      const wz = positionGeometry.z;

      const slCoarse = sampleGrid(slopeGridCoarse, wx, wz, PATCH_COARSE);
      const slFine = sampleGrid(slopeGridFine, wx, wz, PATCH_FINE);
      const dCoarse = sampleGrid(dispGridCoarse, wx, wz, PATCH_COARSE);
      const dFine = sampleGrid(dispGridFine, wx, wz, PATCH_FINE);

      const gridJxx = slCoarse.z.add(slFine.z);
      const gridJzz = slCoarse.w.add(slFine.w);
      const gridJxz = dCoarse.w.add(dFine.w);

      const swellJ = buildGerstnerSwellJacobian(wx, wz);
      const totalJxx = gridJxx.add(swellJ.Jxx);
      const totalJzz = gridJzz.add(swellJ.Jzz);
      const totalJxz = gridJxz.add(swellJ.Jxz);

      const J = float(1).add(totalJxx).mul(float(1).add(totalJzz)).sub(totalJxz.mul(totalJxz));

      const foamEdgeUV = vec2(wx, wz);
      const ft = time.mul(0.15);

      // === Shared noise samples at multiple octaves ===
      // Shared noise samples (3 octaves instead of 5 for perf)
      const warpScale = fbmNoiseFreqU.mul(0.3);
      const warpX = mx_noise_float(foamEdgeUV.mul(warpScale).add(vec2(ft.mul(0.2), float(0)))).mul(1.8);
      const warpZ = mx_noise_float(foamEdgeUV.mul(warpScale).add(vec2(float(0), ft.mul(-0.15)))).mul(1.8);
      const warpedUV = foamEdgeUV.add(vec2(warpX, warpZ));

      const n1f = mx_noise_float(warpedUV.mul(fbmNoiseFreqU).add(vec2(ft.mul(0.35), ft.mul(-0.2))));
      const n2f = mx_noise_float(warpedUV.mul(fbmNoiseFreqU.mul(2.3)).add(vec2(ft.mul(-0.25), ft.mul(0.4))));
      const n3f = mx_noise_float(warpedUV.mul(fbmNoiseFreqU.mul(5.7)).add(vec2(ft.mul(0.5), ft.mul(0.15))));

      // Unified 3-octave FBM used as base for all foam noise types
      const baseFbm = n1f.mul(0.5).add(n2f.mul(0.3)).add(n3f.mul(0.2));

      // Derive all noise variants from shared samples (no extra texture fetches)
      // --- Type 0: Domain Warp ---
      const ridgedDW = float(1.0).sub(abs(baseFbm.mul(2.0).sub(1.0)));
      const noiseDomainWarp = mix(baseFbm, ridgedDW, float(0.4));

      // --- Type 1: Ridged Multifractal ---
      const r1 = float(1.0).sub(abs(n1f.mul(2.0).sub(1.0)));
      const r2 = float(1.0).sub(abs(n2f.mul(2.0).sub(1.0)));
      const r3 = float(1.0).sub(abs(n3f.mul(2.0).sub(1.0)));
      const noiseRidged = r1.mul(0.5).add(r2.mul(r1).mul(0.3)).add(r3.mul(r2).mul(0.2));

      // --- Type 2: Cellular / Voronoi-like (2 offset samples instead of 4) ---
      const cUV = foamEdgeUV.mul(fbmNoiseFreqU.mul(0.8)).add(vec2(ft.mul(0.2), ft.mul(-0.15)));
      const c1 = mx_noise_float(cUV);
      const c2 = mx_noise_float(cUV.add(vec2(0.33, 0.77)));
      const noiseCellular = float(1.0).sub(abs(c1.sub(c2))).mul(1.5);

      // --- Type 3: Billow ---
      const b1 = abs(n1f.mul(2.0).sub(1.0));
      const b2 = abs(n2f.mul(2.0).sub(1.0));
      const b3 = abs(n3f.mul(2.0).sub(1.0));
      const noiseBillow = b1.mul(0.5).add(b2.mul(0.3)).add(b3.mul(0.2));

      // --- Type 4: Swiss (2 octaves instead of 3) ---
      const sw1 = float(1.0).sub(abs(n1f.mul(2.0).sub(1.0)));
      const sw1sq = sw1.mul(sw1);
      const swWarp = warpedUV.add(vec2(sw1.mul(0.3), sw1.mul(-0.25)));
      const sw2raw = mx_noise_float(swWarp.mul(fbmNoiseFreqU.mul(2.5)).add(vec2(ft.mul(-0.3), ft.mul(0.2))));
      const sw2 = float(1.0).sub(abs(sw2raw.mul(2.0).sub(1.0))).mul(sw1sq);
      const noiseSwiss = sw1.mul(0.6).add(sw2.mul(0.4));

      // --- Type 5: Turbulent (3 octaves instead of 4) ---
      const t1 = abs(n1f.mul(2.0).sub(1.0));
      const t2 = abs(n2f.mul(2.0).sub(1.0));
      const t3 = abs(n3f.mul(2.0).sub(1.0));
      const noiseTurbulent = float(1.0).sub(t1.mul(0.45).add(t2.mul(0.35)).add(t3.mul(0.2)));

      // Select noise type via step functions
      const isType0 = smoothstep(float(-0.5), float(0.5), foamNoiseType.negate());
      const isType1 = smoothstep(float(0.5), float(1.5), foamNoiseType).mul(smoothstep(float(1.5), float(0.5), foamNoiseType));
      const isType2 = smoothstep(float(1.5), float(2.5), foamNoiseType).mul(smoothstep(float(2.5), float(1.5), foamNoiseType));
      const isType3 = smoothstep(float(2.5), float(3.5), foamNoiseType).mul(smoothstep(float(3.5), float(2.5), foamNoiseType));
      const isType4 = smoothstep(float(3.5), float(4.5), foamNoiseType).mul(smoothstep(float(4.5), float(3.5), foamNoiseType));
      const isType5 = smoothstep(float(4.5), float(5.5), foamNoiseType);

      const foamNoise = noiseDomainWarp.mul(isType0)
        .add(noiseRidged.mul(isType1))
        .add(noiseCellular.mul(isType2))
        .add(noiseBillow.mul(isType3))
        .add(noiseSwiss.mul(isType4))
        .add(noiseTurbulent.mul(isType5))
        .mul(fbmNoiseAmpU);
  
      const foamEdge = foamThreshold.add(foamNoise);
      const foamRaw = pow(smoothstep(foamEdge, foamEdge.sub(0.6), J), foamPower);
      const islandDistForFoamBase = sqrt(wx.mul(wx).add(wz.mul(wz)));
      const shoreAngle = atan(wz, wx);
      const shoreLobeA = sin(shoreAngle.mul(3.0).add(oceanIslandShapePhaseA));
      const shoreLobeB = sin(shoreAngle.mul(5.0).add(oceanIslandShapePhaseB)).mul(0.55);
      const shoreLobeC = sin(shoreAngle.mul(7.0).add(oceanIslandShapePhaseC)).mul(0.20);
      const shoreShapeScale = tslMax(float(0.72), float(1.0).add(shoreLobeA.add(shoreLobeB).mul(oceanIslandShapeRoundness)).add(shoreLobeC.mul(0.07)));
      const islandDistForFoam = islandDistForFoamBase.div(shoreShapeScale);
      const shoreEnergyColor = mix(
        oceanShoreMinimumWave,
        float(1.0),
        smoothstep(oceanIslandCutRadius, oceanIslandCutRadius.add(oceanShoreCalmDistance), islandDistForFoam)
      );
      const shoreInner = smoothstep(oceanIslandCutRadius.sub(oceanIslandCutFeather.mul(0.42)), oceanIslandCutRadius, islandDistForFoam);
      const shoreOuter = float(1.0).sub(smoothstep(oceanIslandCutRadius, oceanIslandCutRadius.add(oceanIslandCutFeather.mul(1.55)), islandDistForFoam));
      const shoreFoam = shoreInner.mul(shoreOuter).mul(oceanShoreFoamStrength);
      // La mousse de pleine mer est calmée près de l'île ; seul le liseré de rivage reste marqué.
      const foamMask = tslMax(clamp(foamRaw.mul(foamIntensity).mul(shoreEnergyColor), 0, 1), shoreFoam);

      const sX = slCoarse.x.add(slFine.x).mul(shoreEnergyColor);
      const sZ = slCoarse.y.add(slFine.y).mul(shoreEnergyColor);

      // Procedural normal-map detail via multi-octave noise derivatives
      const detailUV = vec2(wx, wz);
      const t = time.mul(0.3);
      const eps = float(0.01);
      const epsF = float(0.005);
      const epsVF = float(0.002);

      // ── Bump noise: 2 octaves with distance LOD ──
      const nFreq1 = float(0.8).mul(bumpFrequencyU);
      const nFreq2 = float(3.2).mul(bumpFrequencyU);
      const tOff1 = vec2(t.mul(0.4), t.mul(0.2));
      const tOff2 = vec2(t.mul(-0.6), t.mul(0.35));

      // Shared base noise samples (reused across all bump types)
      const bn1 = mx_noise_float(detailUV.mul(nFreq1).add(tOff1));
      const bn1px = mx_noise_float(detailUV.mul(nFreq1).add(vec2(eps, float(0))).add(tOff1));
      const bn1pz = mx_noise_float(detailUV.mul(nFreq1).add(vec2(float(0), eps)).add(tOff1));
      const bn2 = mx_noise_float(detailUV.mul(nFreq2).add(tOff2));
      const bn2px = mx_noise_float(detailUV.mul(nFreq2).add(vec2(epsF, float(0))).add(tOff2));
      const bn2pz = mx_noise_float(detailUV.mul(nFreq2).add(vec2(float(0), epsF)).add(tOff2));

      // Finite-difference derivatives from shared samples
      const d1x = bn1px.sub(bn1).mul(100.0);
      const d1z = bn1pz.sub(bn1).mul(100.0);
      const d2x = bn2px.sub(bn2).mul(200.0);
      const d2z = bn2pz.sub(bn2).mul(200.0);

      // === Type 0: Standard Perlin ===
      const perlinBumpX = d1x.mul(0.6).add(d2x.mul(0.4));
      const perlinBumpZ = d1z.mul(0.6).add(d2z.mul(0.4));

      // === Type 1: Ridged ===
      const rbn1 = float(1.0).sub(abs(bn1.mul(2.0).sub(1.0)));
      const rbn1px = float(1.0).sub(abs(bn1px.mul(2.0).sub(1.0)));
      const rbn1pz = float(1.0).sub(abs(bn1pz.mul(2.0).sub(1.0)));
      const rbn2 = float(1.0).sub(abs(bn2.mul(2.0).sub(1.0)));
      const rbn2px = float(1.0).sub(abs(bn2px.mul(2.0).sub(1.0)));
      const rbn2pz = float(1.0).sub(abs(bn2pz.mul(2.0).sub(1.0)));
      const ridgedBumpX = rbn1px.sub(rbn1).mul(100.0).mul(0.6).add(rbn2px.sub(rbn2).mul(200.0).mul(0.4));
      const ridgedBumpZ = rbn1pz.sub(rbn1).mul(100.0).mul(0.6).add(rbn2pz.sub(rbn2).mul(200.0).mul(0.4));

      // === Type 2: Domain Warped ===
      const dwWarpN = mx_noise_float(detailUV.mul(0.4).add(vec2(t.mul(0.15), t.mul(-0.1))));
      const dwOff = vec2(dwWarpN.mul(0.3), dwWarpN.mul(-0.25));
      const dwUV = detailUV.add(dwOff);
      const dw1 = mx_noise_float(dwUV.mul(nFreq1).add(tOff1));
      const dw1dx = mx_noise_float(dwUV.mul(nFreq1).add(vec2(eps, float(0))).add(tOff1)).sub(dw1).mul(100.0);
      const dw1dz = mx_noise_float(dwUV.mul(nFreq1).add(vec2(float(0), eps)).add(tOff1)).sub(dw1).mul(100.0);
      const dwBumpX = dw1dx;
      const dwBumpZ = dw1dz;

      // === Type 3: Billow ===
      const bbn1 = abs(bn1.mul(2.0).sub(1.0));
      const bbn1px = abs(bn1px.mul(2.0).sub(1.0));
      const bbn1pz = abs(bn1pz.mul(2.0).sub(1.0));
      const bbn2 = abs(bn2.mul(2.0).sub(1.0));
      const bbn2px = abs(bn2px.mul(2.0).sub(1.0));
      const bbn2pz = abs(bn2pz.mul(2.0).sub(1.0));
      const billowBumpX = bbn1px.sub(bbn1).mul(100.0).mul(0.6).add(bbn2px.sub(bbn2).mul(200.0).mul(0.4));
      const billowBumpZ = bbn1pz.sub(bbn1).mul(100.0).mul(0.6).add(bbn2pz.sub(bbn2).mul(200.0).mul(0.4));

      // === Type 4: Crosshatch ===
      const chFreq1 = nFreq1.mul(1.2);
      const chA1 = mx_noise_float(vec2(detailUV.x.mul(chFreq1).add(detailUV.y.mul(0.3)), detailUV.y.mul(chFreq1).sub(detailUV.x.mul(0.3))).add(tOff1));
      const chB1 = mx_noise_float(vec2(detailUV.x.mul(chFreq1).sub(detailUV.y.mul(0.5)), detailUV.y.mul(chFreq1).add(detailUV.x.mul(0.5))).add(tOff1.mul(-0.7)));
      const ch1 = chA1.mul(0.5).add(chB1.mul(0.5));
      const chA1px = mx_noise_float(vec2(detailUV.x.add(eps).mul(chFreq1).add(detailUV.y.mul(0.3)), detailUV.y.mul(chFreq1).sub(detailUV.x.add(eps).mul(0.3))).add(tOff1));
      const chB1px = mx_noise_float(vec2(detailUV.x.add(eps).mul(chFreq1).sub(detailUV.y.mul(0.5)), detailUV.y.mul(chFreq1).add(detailUV.x.add(eps).mul(0.5))).add(tOff1.mul(-0.7)));
      const ch1px = chA1px.mul(0.5).add(chB1px.mul(0.5));
      const chA1pz = mx_noise_float(vec2(detailUV.x.mul(chFreq1).add(detailUV.y.add(eps).mul(0.3)), detailUV.y.add(eps).mul(chFreq1).sub(detailUV.x.mul(0.3))).add(tOff1));
      const chB1pz = mx_noise_float(vec2(detailUV.x.mul(chFreq1).sub(detailUV.y.add(eps).mul(0.5)), detailUV.y.add(eps).mul(chFreq1).add(detailUV.x.mul(0.5))).add(tOff1.mul(-0.7)));
      const ch1pz = chA1pz.mul(0.5).add(chB1pz.mul(0.5));
      const crossBumpX = ch1px.sub(ch1).mul(100.0);
      const crossBumpZ = ch1pz.sub(ch1).mul(100.0);

      // === Type 5: FBM (4 octaves, distance-culled) ===
      const fbmFreqs = [nFreq1, nFreq1.mul(2.0), nFreq1.mul(4.0), nFreq1.mul(8.0)];
      const fbmWeights = [0.4, 0.3, 0.2, 0.1];
      const fbmTimeOffsets = [tOff1, tOff2, vec2(t.mul(0.55), t.mul(-0.3)), vec2(t.mul(-0.25), t.mul(0.6))];
      const fbmEpsArr = [eps, epsF, epsF, epsVF];
      const fbmDerScale = [100.0, 200.0, 200.0, 500.0];

      let fbmBumpXAcc = float(0);
      let fbmBumpZAcc = float(0);
      for (let oi = 0; oi < 4; oi++) {
        const fn0 = mx_noise_float(detailUV.mul(fbmFreqs[oi]).add(fbmTimeOffsets[oi]));
        const fnpx = mx_noise_float(detailUV.mul(fbmFreqs[oi]).add(vec2(fbmEpsArr[oi], float(0))).add(fbmTimeOffsets[oi]));
        const fnpz = mx_noise_float(detailUV.mul(fbmFreqs[oi]).add(vec2(float(0), fbmEpsArr[oi])).add(fbmTimeOffsets[oi]));
        fbmBumpXAcc = fbmBumpXAcc.add(fnpx.sub(fn0).mul(fbmDerScale[oi]).mul(fbmWeights[oi]));
        fbmBumpZAcc = fbmBumpZAcc.add(fnpz.sub(fn0).mul(fbmDerScale[oi]).mul(fbmWeights[oi]));
      }
      const fbmBumpX = fbmBumpXAcc;
      const fbmBumpZ = fbmBumpZAcc;

      // Select bump noise type
      const bIsT0 = smoothstep(float(-0.5), float(0.5), bumpNoiseType.negate());
      const bIsT1 = smoothstep(float(0.5), float(1.5), bumpNoiseType).mul(smoothstep(float(1.5), float(0.5), bumpNoiseType));
      const bIsT2 = smoothstep(float(1.5), float(2.5), bumpNoiseType).mul(smoothstep(float(2.5), float(1.5), bumpNoiseType));
      const bIsT3 = smoothstep(float(2.5), float(3.5), bumpNoiseType).mul(smoothstep(float(3.5), float(2.5), bumpNoiseType));
      const bIsT4 = smoothstep(float(3.5), float(4.5), bumpNoiseType).mul(smoothstep(float(4.5), float(3.5), bumpNoiseType));
      const bIsT5 = smoothstep(float(4.5), float(5.5), bumpNoiseType);

      const selectedBumpX = perlinBumpX.mul(bIsT0)
        .add(ridgedBumpX.mul(bIsT1))
        .add(dwBumpX.mul(bIsT2))
        .add(billowBumpX.mul(bIsT3))
        .add(crossBumpX.mul(bIsT4))
        .add(fbmBumpX.mul(bIsT5));
      const selectedBumpZ = perlinBumpZ.mul(bIsT0)
        .add(ridgedBumpZ.mul(bIsT1))
        .add(dwBumpZ.mul(bIsT2))
        .add(billowBumpZ.mul(bIsT3))
        .add(crossBumpZ.mul(bIsT4))
        .add(fbmBumpZ.mul(bIsT5));

      // Blend noise normals - attenuate with distance for performance perception  
      const distFade = smoothstep(float(800.0), float(50.0), positionWorld.sub(cameraPosition).length());
      const bumpStrength = bumpStrengthU.mul(distFade).mul(shoreEnergyColor);
      const bumpX = selectedBumpX.mul(bumpStrength);
      const bumpZ = selectedBumpZ.mul(bumpStrength);

      const N_vec = normalize(vec3(sX.negate().add(bumpX), float(1), sZ.negate().add(bumpZ)));

      const waveHeight = dCoarse.y.add(dFine.y);

      const V = normalize(cameraPosition.sub(positionWorld));
      const L = normalize(sunDir);
      const NdotL = abs(dot(N_vec, L)).max(float(0.15));
      const NdotV = abs(dot(N_vec, V)).max(float(0.05));

      const F = pow(float(1.0).sub(NdotV), fresnelPower)
        .mul(fresnelStrength)
        .mul(float(1).sub(foamMask.mul(0.8)));

      const deepColor = vec3(0.0, 0.03, 0.12);
      const shallowColor = vec3(0.0, 0.08, 0.18);
      const elevationMask = smoothstep(float(-4.0), float(6.0), waveHeight);
      const baseAlbedo = mix(deepColor, waterColor, elevationMask);
      const depthTint = mix(shallowColor, baseAlbedo, smoothstep(float(-2.0), float(3.0), waveHeight));

      // Height-based color gradient (4-stop)
      const hNorm = smoothstep(float(-5.0), float(8.0), waveHeight);
      const hColor1 = mix(heightColorLow, heightColorMid, smoothstep(float(0.0), float(0.33), hNorm));
      const hColor2 = mix(hColor1, heightColorHigh, smoothstep(float(0.33), float(0.66), hNorm));
      const hColor3 = mix(hColor2, heightColorPeak, smoothstep(float(0.66), float(1.0), hNorm));
      const albedo = mix(depthTint, hColor3, heightColorIntensity);

      const H = normalize(L.add(V));
      const NdotH = tslMax(dot(N_vec, H), float(0));
      const shininess = mix(float(800), float(4), roughness_u);
      const spec = pow(NdotH, shininess).mul(1.2).mul(float(1).sub(foamMask));

      // Sun disc reflection on water
      const R = normalize(L.sub(N_vec.mul(dot(L, N_vec).mul(2.0))));
      const sunReflect = pow(tslMax(dot(V, R), float(0)), float(512)).mul(3.0).mul(float(1).sub(foamMask));

      // ── Advanced Subsurface Scattering ──
      // 1. Forward-scatter through wave crests (backlit translucency)
      const sssForwardDir = normalize(L.add(N_vec.mul(0.4)));
      const sssForward = pow(tslMax(dot(V, sssForwardDir.negate()), float(0)), float(5)).mul(0.8);
  
      // 2. View-dependent SSS at grazing angles (subtle)
      const sssViewGraze = pow(float(1.0).sub(NdotV), float(4)).mul(0.2);
  
      // 3. Height-based translucency - thin wave crests scatter more light
      const sssCrestMask = smoothstep(float(0.0), float(6.0), waveHeight);
      const sssThickness = smoothstep(float(2.0), float(7.0), waveHeight).mul(0.5);
  
      // 4. Ambient SSS - very subtle volume contribution
      const sssAmbient = float(0.06).mul(sssCrestMask);
  
      // 5. Backlit SSS - reduced for realism
      const sssBacklit = pow(tslMax(dot(V, L.negate()), float(0)), float(4)).mul(0.5);
  
      // 6. Wave-curvature enhanced SSS - tighter range
      const sssCurvature = smoothstep(float(0.4), float(-0.3), J).mul(0.3);
  
      // Combine all SSS components with reduced overall contribution
      const sssTotal = sssForward.add(sssViewGraze).add(sssBacklit).add(sssAmbient).add(sssCurvature);
      const sssModulated = sssTotal.mul(sssCrestMask.mul(0.7).add(sssThickness)).mul(sssIntensity);
  
      // SSS color: more blue-teal, less vivid green; deeper blue dominates
      const sssShallowTint = sssColor;
      const sssDeepColor = vec3(0.01, 0.04, 0.14);
      const sssDepthBlend = smoothstep(float(-1.0), float(5.0), waveHeight);
      const sssMixedColor = mix(sssDeepColor, sssShallowTint, sssDepthBlend.mul(depthBlend));
      const sssFinalColor = sssMixedColor.mul(sssModulated);
  
      const sss = sssFinalColor;

      // Atmospheric fog / distance fade
      const distToCamera = positionWorld.sub(cameraPosition).length();
      const fogFactor = smoothstep(fogNear, fogFar, distToCamera).mul(fogDensity);
      const fogColor = fogColorU;

      const foamLit = foamColorU.mul(NdotL.mul(0.4).add(0.7));
      const foamDetailNoise = mx_noise_float(vec2(wx, wz).mul(8.0)).mul(0.1).add(0.9);
      const foamFinal = foamLit.mul(foamDetailNoise);

      // ── PBR Reflection via procedural sky ──
      // Reflect view vector around perturbed normal
      const reflectDir = V.negate().add(N_vec.mul(dot(V, N_vec).mul(2.0)));
      // Add bump-based distortion for wavy reflections
      const distortedReflect = normalize(reflectDir.add(
        vec3(bumpX.mul(reflectionDistortion.mul(10.0)), float(0), bumpZ.mul(reflectionDistortion.mul(10.0)))
      ));

      // Procedural sky reflection matching the atmospheric scattering sky
      const reflY = distortedReflect.y;
      const reflYClamped = tslMax(reflY, float(0.0));

      // Rayleigh-like sky gradient for reflection
      const sunDotRefl = tslMax(dot(distortedReflect, L), float(0));

      // Sky zenith to horizon gradient
      const skyZenith = vec3(0.15, 0.35, 0.75);  // deep blue zenith
      const skyHorizon = vec3(0.55, 0.70, 0.90);  // pale blue horizon
      const skyDawn = vec3(0.85, 0.55, 0.35);     // warm horizon at low sun

      // Sun elevation factor (low sun = warmer horizon)
      const sunElFactor = smoothstep(float(0.0), float(0.25), L.y);
      const horizonColor = mix(skyDawn, skyHorizon, sunElFactor);

      // Base sky color based on reflection elevation
      const skyGrad = mix(horizonColor, skyZenith, smoothstep(float(0.0), float(0.6), reflYClamped));

      // Mie-like sun glow around sun direction
      const mieGlow = pow(sunDotRefl, float(64)).mul(1.2);
      const mieHalo = pow(sunDotRefl, float(8)).mul(0.25);
      const sunWarmColor = vec3(1.0, 0.85, 0.55);
      const sunWhiteColor = vec3(1.0, 0.95, 0.85);
      const mieContrib = sunWarmColor.mul(mieHalo).add(sunWhiteColor.mul(mieGlow));

      // Sun disc specular (sharp, bright)
      const sunDiscGlow = pow(sunDotRefl, float(512)).mul(5.0).mul(float(1).sub(roughness_u));
      const sunDiscHalo = pow(sunDotRefl, float(128)).mul(1.5).mul(float(1).sub(roughness_u.mul(0.5)));
      const sunReflColor = vec3(1.0, 0.92, 0.75);

      // Below-horizon: use deep water color instead of near-black
      const belowHorizonColor = mix(
        vec3(0.04, 0.08, 0.18),
        vec3(0.08, 0.15, 0.28),
        smoothstep(float(-0.5), float(0.0), reflY)
      );
      // Wider, softer horizon transition to avoid harsh black band
      const horizonBlend = smoothstep(float(-0.25), float(0.12), reflY);

      // Combine sky reflection components
      const skyReflection = mix(belowHorizonColor, skyGrad.add(mieContrib), horizonBlend);

      // Cloud reflection: single noise sample for approximate cloud reflections
      const cloudReflUV = vec2(distortedReflect.x, distortedReflect.z).mul(0.5).div(tslMax(reflYClamped, float(0.05)));
      const cloudReflScale = float(0.00015);
      const crt = time.mul(cloudSpeed);
      const cloudReflNoise = mx_noise_float(cloudReflUV.mul(cloudReflScale.mul(10000.0)).add(vec2(crt, crt.mul(0.3))));
      const cloudReflThresh = float(1.0).sub(cloudCoverage);
      const cloudReflMask = smoothstep(cloudReflThresh, cloudReflThresh.add(0.2), cloudReflNoise).mul(0.35);
      const cloudReflColor = vec3(0.85, 0.88, 0.92);

      // Mix cloud reflection into sky reflection (only above horizon)
      const envReflection = mix(skyReflection, cloudReflColor, cloudReflMask.mul(horizonBlend))
        .add(sunReflColor.mul(sunDiscGlow.add(sunDiscHalo)));

      // PBR roughness-based reflection fade - ensure minimum reflection brightness
      const roughFade = float(1.0).sub(roughness_u.mul(0.5));
      // Add a small ambient reflection floor to prevent pure black
      const ambientReflection = mix(
        vec3(0.03, 0.06, 0.12),
        vec3(0.06, 0.10, 0.18),
        smoothstep(float(-0.3), float(0.2), reflY)
      );
      const finalReflection = tslMax(envReflection.mul(roughFade), ambientReflection);

      // Schlick Fresnel with F0 for water (IOR ~1.33 -> F0 ~0.02)
      // Slightly higher F0 to prevent dark patches at steep angles
      const f0 = float(0.04);
      const fresnelSchlick = f0.add(float(1.0).sub(f0).mul(pow(float(1.0).sub(NdotV), float(5.0))));
      const reflectionMix = fresnelSchlick.mul(reflectionStrength).mul(float(1).sub(foamMask.mul(0.7)));
  
      // PBR energy conservation: reduce diffuse as reflections increase
      const diffuseWeight = float(1.0).sub(reflectionMix);
      const diffuseColor = albedo.mul(NdotL.mul(0.8).add(0.2)).mul(diffuseWeight);
  
      // Combine diffuse + reflection + specular + SSS
      const oceanColor = diffuseColor
        .add(finalReflection.mul(reflectionMix))
        .add(vec3(spec).mul(float(1).sub(roughness_u)))
        .add(sss);

      const litOcean = mix(oceanColor, foamFinal, foamMask);
      return mix(litOcean, fogColor, fogFactor);
    })();


    // Découpe douce sous l'îlot : l'océan ne traverse plus visuellement la terre/herbe.
    // Le liseré de mousse est ajouté dans colorNode autour de cette zone.
    oceanMat.opacityNode = Fn(() => {
      const wx = positionGeometry.x;
      const wz = positionGeometry.z;
      const d = sqrt(wx.mul(wx).add(wz.mul(wz)));
      const shoreAngle = atan(wz, wx);
      const shoreLobeA = sin(shoreAngle.mul(3.0).add(oceanIslandShapePhaseA));
      const shoreLobeB = sin(shoreAngle.mul(5.0).add(oceanIslandShapePhaseB)).mul(0.55);
      const shoreLobeC = sin(shoreAngle.mul(7.0).add(oceanIslandShapePhaseC)).mul(0.20);
      const shoreShapeScale = tslMax(float(0.72), float(1.0).add(shoreLobeA.add(shoreLobeB).mul(oceanIslandShapeRoundness)).add(shoreLobeC.mul(0.07)));
      const organicDist = d.div(shoreShapeScale);
      const islandMask = smoothstep(oceanIslandCutRadius.sub(oceanIslandCutFeather.mul(0.42)), oceanIslandCutRadius.add(oceanIslandCutFeather.mul(0.95)), organicDist);
      // Fait disparaître progressivement le bord sphérisé tout en suivant le path organique du sol.
      const edgeFade = float(1.0).sub(smoothstep(float(OCEAN_EDGE_FADE_START), float(OCEAN_EDGE_FADE_END), d));
      return islandMask.mul(edgeFade);
    })();
    oceanMat.transparent = true;
    oceanMat.depthWrite = false;
    oceanMat.depthTest = true;
    const oceanMesh = new THREE.Mesh(oceanGeo, oceanMat);
    oceanMesh.frustumCulled = false;
    // Eau rapprochée de la terre : l'écart vertical est réduit sans toucher à la croissance.
    oceanMesh.position.y = -0.92;
    scene.add(oceanMesh);
    // Ancien fond sphérique supprimé : l'océan est maintenant le maillage Alejandro lui-même,
    // sphérisé en shader. Aucun volume bleu supplémentaire n'est ajouté sous la scène.
    const oceanPlanet = new THREE.Group();
    oceanPlanet.visible = false;
    oceanPlanet.frustumCulled = false;
    const oceanPlanetMat = { color: { set: () => {} } };

    // Flat floor optionnel : remplace la grande couche noire/"autre océan" par un sol plat contrôlable.
    // Il reste sous l'île et sous l'eau pour ne pas masquer les couches courbées.
    const flatFloorState = { enabled: false, color: '#02050c', y: -12.0, size: 3600 };
    const flatFloorGeo = new THREE.PlaneGeometry(flatFloorState.size, flatFloorState.size, 1, 1);
    flatFloorGeo.rotateX(-Math.PI / 2);
    const flatFloorMat = new THREE.MeshBasicMaterial({
      color: new THREE.Color(flatFloorState.color),
      side: THREE.DoubleSide,
      depthWrite: false,
      depthTest: true,
      toneMapped: false
    });
    const flatFloor = new THREE.Mesh(flatFloorGeo, flatFloorMat);
    flatFloor.position.y = flatFloorState.y;
    flatFloor.visible = flatFloorState.enabled;
    flatFloor.renderOrder = -20;
    flatFloor.frustumCulled = false;
    scene.add(flatFloor);

    // Cycles naturels : marée douce pilotée par le temps + progression Fibonacci,
    // et synchronisée à l'énergie du vent. Elle anime le niveau d'eau sans toucher à la croissance de l'arbre.
    const PHI = (1 + Math.sqrt(5)) / 2;
    const oceanCycleState = { tideEnabled: true, baseLevel: oceanMesh.position.y, tideAmplitude: 0.045, tideSpeed: 0.24, manualTideControls: false };
    const oceanManualOverrides = {
      tideAmplitude: false,
      shoreCalmDistance: false,
      shoreFoamStrength: false,
      waterLevel: false,
      shoreFade: false
    };

    // ───── Microclimat global ─────
    // Une seule source de vérité pour le vent, l'air, la lumière, la marée,
    // l'océan, l'herbe, les feuilles et les particules de rivage.
    // Par défaut, le microclimat suit l'horloge locale du navigateur.
    const microClimate = {
      autoBrowserClock: true,
      liveWeatherEnabled: false,
      liveWeatherStatus: 'horloge navigateur',
      liveWeatherError: '',
      liveWeatherTime: '',
      liveWeatherCode: null,
      liveWeatherApparent: null,
      liveWeatherIsDay: null,
      locationLabel: '',
      latitude: null,
      longitude: null,
      windSpeed: params.windSpeed,
      windDirection: Math.PI * 0.25,
      windForce: 0.22,
      airTemperature: 18,
      humidity: 62,
      pressure: 1013,
      cloudCover: 0.45,
      rain: 0.0,
      daylight: 1.0,
      season01: 0.0,
      time01: 0.5,
      tide01: 0.0,
      lastSpectrumUpdate: 0,
      lastLiveWeatherFetch: 0
    };

    const vrPlayableState = {
      // Désactivé par défaut sur desktop, mobile et tablette.
      // La limite réelle 18 m² reste disponible pour une vraie session VR.
      enabled: false,
      visible: false,
      radius: Math.sqrt(18 / Math.PI)
    };

    const vrRuntimeState = {
      active: false,
      hideHudInVR: true,
      viewMode: '3e personne',
      comfortPixelRatio: MICROCOSM_RUNTIME_PERF.vrComfortPixelRatio,
      normalPixelRatio: Math.min(window.devicePixelRatio || 1, MICROCOSM_RUNTIME_PERF.maxPixelRatio),
      vrScale: 1.0,
      reduceParticles: true,
      reduceOceanWhileVR: true,
      saved: {
        grassDensity: null,
        grassDensityMultiplier: null,
        shoreOpacity: null,
        foamIntensity: null,
        heightScale: null,
        choppiness: null
      }
    };

    const shoreFadeState = {
      enabled: true,
      density: 1650,
      width: 1.18,
      opacity: 0.46,
      color: '#e9f0d2'
    };

    const premiumProductState = {
      localTestingOverride: false,
      maxFreeClicksPerDay: 1,
      windsUnlocked: false,
      lifetimeUnlocked: false,
      windsClicksPerDay: 5
    };

    // Racine vivante de l'îlot : terre + herbe + arbre + graine + petites interactions.
    // La croissance de l'arbre reste inchangée ; on anime uniquement le micro-monde qui le porte.
    const islandRoot = new THREE.Group();
    islandRoot.name = 'MicrocosmFloatingIslandRoot';
    scene.add(islandRoot);
    const islandMotionState = {
      // Désactivé par défaut : plus de flottement séparé qui donne l'impression
      // que l'îlot dérive hors du système. Si on l'active, il utilise le microclimat global.
      enabled: false,
      bobAmplitude: 0.0,
      tiltAmplitude: 0.0,
      driftAmplitude: 0.0,
      vrComfort: true,
      lastTide: 0
    };
    function updateFloatingIslandMotion(elapsed, windRT) {
      if (!islandMotionState.enabled) {
        islandRoot.position.set(0, 0, 0);
        islandRoot.rotation.set(0, 0, 0);
        islandRoot.updateMatrixWorld(true);
        return;
      }
      const p = THREE.MathUtils.clamp(growthProgress, 0, 1);
      const dayPhase = (growthClicksTree / FIB_GROWTH_TOTAL_CLICKS_TREE) * Math.PI * 2 * PHI;
      const wind01 = THREE.MathUtils.clamp(globalWind.speed / 50, 0, 1);
      const comfort = islandMotionState.vrComfort || renderer.xr.isPresenting ? 0.42 : 1.0;
      const ageInertia = THREE.MathUtils.lerp(0.72, 1.0, p);
      const phaseA = elapsed * oceanCycleState.tideSpeed * PHI + dayPhase;
      const phaseB = elapsed * oceanCycleState.tideSpeed * (PHI * 0.63) + dayPhase * 0.37;
      const waveEnergy = (0.70 + wind01 * 0.55) * ageInertia;

      const bob = Math.sin(phaseA + 0.55) * islandMotionState.bobAmplitude * waveEnergy * comfort
        + islandMotionState.lastTide * 0.22;
      islandRoot.position.y = bob;
      islandRoot.position.x = Math.sin(phaseB) * islandMotionState.driftAmplitude * windRT.dirX * comfort;
      islandRoot.position.z = Math.cos(phaseB * 0.91) * islandMotionState.driftAmplitude * windRT.dirZ * comfort;
      islandRoot.rotation.x = Math.sin(phaseB + 1.1) * islandMotionState.tiltAmplitude * (0.7 + wind01) * comfort;
      islandRoot.rotation.z = Math.cos(phaseB * 0.87 - 0.4) * islandMotionState.tiltAmplitude * (0.7 + wind01) * comfort;
      islandRoot.rotation.y = Math.sin(phaseA * 0.21) * islandMotionState.tiltAmplitude * 0.30 * comfort;
      islandRoot.updateMatrixWorld(true);
    }

    // ───── Herbe (grass) ─────
    // Herbe unique par utilisateur/nouvelle partie : on garde un nombre d'instances raisonnable
    // pour la fluidité, mais on agrandit vraiment la zone et sa forme visuelle.
    const BLADE_COUNT = MICROCOSM_RUNTIME_PERF.grassBladeInstances;
    const GRASS_GRID = MICROCOSM_RUNTIME_PERF.grassGrid;
    const FIELD_SIZE = 58;
    // Dark blue background to allow the ocean and horizon to show through
    const BACKGROUND_HEX = '#0a1628';
    // Dark navy ground to match the ocean’s palette
    const GROUND_HEX = '#041e42';
    const BLADE_BASE_HEX = '#252d0b';
    const BLADE_TIP_HEX = '#97d638';
    // Scene background for grass is already integrated in global scene background

    const GRASS_USER_SEED_KEY = 'microcosm-grass-user-seed-v1';
    const GRASS_GAME_SEED_KEY = 'microcosm-grass-game-seed-v1';
    function createGrassSeedString(prefix = 'grass') {
      const bytes = new Uint32Array(4);
      if (window.crypto?.getRandomValues) {
        window.crypto.getRandomValues(bytes);
      } else {
        const now = Date.now();
        for (let i = 0; i < bytes.length; i++) bytes[i] = ((now + i * 2654435761) ^ Math.floor(Math.random() * 0xffffffff)) >>> 0;
      }
      return `${prefix}-${Date.now().toString(36)}-${Array.from(bytes, n => n.toString(36)).join('-')}`;
    }
    function readGrassStorageSeed(key, prefix) {
      try {
        let value = localStorage.getItem(key);
        if (!value) {
          value = createGrassSeedString(prefix);
          localStorage.setItem(key, value);
        }
        return value;
      } catch (_) {
        return createGrassSeedString(prefix);
      }
    }
    function hashGrassSeedString(str) {
      let h = 2166136261 >>> 0;
      for (let i = 0; i < str.length; i++) {
        h ^= str.charCodeAt(i);
        h = Math.imul(h, 16777619) >>> 0;
      }
      return h >>> 0;
    }
    function grassSeeded01(a, b = 0) {
      let h = (grassSeedBase ^ Math.imul((a * 1000003) | 0, 0x9e3779b1) ^ Math.imul((b * 9176) | 0, 0x85ebca6b)) >>> 0;
      h ^= h >>> 16;
      h = Math.imul(h, 0x7feb352d) >>> 0;
      h ^= h >>> 15;
      h = Math.imul(h, 0x846ca68b) >>> 0;
      h ^= h >>> 16;
      return (h >>> 0) / 4294967296;
    }
    const grassUserSeedString = readGrassStorageSeed(GRASS_USER_SEED_KEY, 'grass-user');
    let grassGameSeedString = readGrassStorageSeed(GRASS_GAME_SEED_KEY, 'grass-game');
    let grassSeedBase = hashGrassSeedString(`${grassUserSeedString}|${grassGameSeedString}`);
    function newGrassGameSeed() {
      grassGameSeedString = createGrassSeedString('grass-game');
      try { localStorage.setItem(GRASS_GAME_SEED_KEY, grassGameSeedString); } catch (_) {}
      grassSeedBase = hashGrassSeedString(`${grassUserSeedString}|${grassGameSeedString}`);
      applyGrassSeedUniforms();
      if (typeof groundRadiusGrass !== 'undefined' && typeof rebuildIslandEarth === 'function') rebuildIslandEarth(groundRadiusGrass.value);
    }

    const grassSeedConfig = {
      x: grassSeeded01(11, 3) * 1000,
      z: grassSeeded01(17, 5) * 1000,
      phaseA: grassSeeded01(23, 7) * Math.PI * 2,
      phaseB: grassSeeded01(29, 9) * Math.PI * 2,
      size: 0.92 + grassSeeded01(31, 11) * 0.20,
      roundness: 0.11 + grassSeeded01(37, 13) * 0.13,
    };
    // GPU storage buffers for grass
    const bladeData = instancedArray(BLADE_COUNT, 'vec4');
    const bendState = instancedArray(BLADE_COUNT, 'vec4');
    const bladeBound = instancedArray(BLADE_COUNT, 'float');
    // Uniforms for grass
    const mouseWorld = uniform(new THREE.Vector3(99999, 0, 99999));
    const mouseRadius = uniform(2.2);
    const mouseStrength = uniform(1.8);
    const icoWorld = uniform(new THREE.Vector3(99999, 0, 99999));
    const icoRadius = uniform(2.5);
    const icoStrengthU = uniform(2.0);
    // V8.18 · herbe plus dense par défaut.
    // V8.25 · le slider développeur “Densité générée ×” multiplie vraiment le pool visible.
    // Desktop : pool GPU ×10. Mobile : pool plafonné pour éviter le gel instantané.
    const grassDensity = uniform(1.86);
    const grassDensityMultiplier = uniform(1.0);
    const grassMaxDensityMultiplier = uniform(MICROCOSM_RUNTIME_PERF.grassMaxDensityMultiplier);
    let grassDensityManualOverride = false;
    const windAmplitude = uniform(0.22);
    const grassSeedX = uniform(grassSeedConfig.x);
    const grassSeedZ = uniform(grassSeedConfig.z);
    const grassShapePhaseA = uniform(grassSeedConfig.phaseA);
    const grassShapePhaseB = uniform(grassSeedConfig.phaseB);
    const grassShapeRoundness = uniform(grassSeedConfig.roundness);
    const grassSizeFactor = uniform(grassSeedConfig.size);
    const grassVisibleRadius = uniform(15.0 * grassSeedConfig.size);
    const grassMaxRadius = uniform(34.0);
    oceanIslandShapePhaseA.value = grassSeedConfig.phaseA;
    oceanIslandShapePhaseB.value = grassSeedConfig.phaseB;
    oceanIslandShapePhaseC.value = grassSeedConfig.x * 0.013;
    oceanIslandShapeRoundness.value = grassSeedConfig.roundness;
    // Même uniform que l'océan : la terre et l'herbe gardent le même angle de courbure.
    const islandCurveStrength = uniform(0.0032);
    function applyGrassSeedUniforms() {
      grassSeedConfig.x = grassSeeded01(11, 3) * 1000;
      grassSeedConfig.z = grassSeeded01(17, 5) * 1000;
      grassSeedConfig.phaseA = grassSeeded01(23, 7) * Math.PI * 2;
      grassSeedConfig.phaseB = grassSeeded01(29, 9) * Math.PI * 2;
      grassSeedConfig.size = 0.92 + grassSeeded01(31, 11) * 0.20;
      grassSeedConfig.roundness = 0.11 + grassSeeded01(37, 13) * 0.13;
      grassSeedX.value = grassSeedConfig.x;
      grassSeedZ.value = grassSeedConfig.z;
      grassShapePhaseA.value = grassSeedConfig.phaseA;
      grassShapePhaseB.value = grassSeedConfig.phaseB;
      grassShapeRoundness.value = grassSeedConfig.roundness;
      oceanIslandShapePhaseA.value = grassSeedConfig.phaseA;
      oceanIslandShapePhaseB.value = grassSeedConfig.phaseB;
      oceanIslandShapePhaseC.value = grassSeedConfig.x * 0.013;
      oceanIslandShapeRoundness.value = grassSeedConfig.roundness;
      grassSizeFactor.value = grassSeedConfig.size;
    }

    // Vent global synchronisé : océan, nuages, herbe, branches fines, feuilles, fleurs et feuilles tombantes.
    const globalWind = {
      speed: params.windSpeed,
      direction: Math.PI * 0.25,
      force: windAmplitude.value,
    };
    function syncGlobalWind({ speed, direction, force, rebuildOcean = false, fromMicroClimate = false } = {}) {
      if (Number.isFinite(speed)) {
        globalWind.speed = speed;
        params.windSpeed = speed;
        windSpeedU.value = speed;
        if (!fromMicroClimate) microClimate.windSpeed = speed;
      }
      if (Number.isFinite(direction)) {
        globalWind.direction = direction;
        windDir.value = direction;
        if (!fromMicroClimate) microClimate.windDirection = direction;
      }
      if (Number.isFinite(force)) {
        globalWind.force = force;
        windAmplitude.value = force;
        if (!fromMicroClimate) microClimate.windForce = force;
      }
      const speed01 = THREE.MathUtils.clamp(globalWind.speed / 50, 0, 1);
      // Les nuages, l'herbe, les feuilles, l'océan et les particules utilisent cette même énergie.
      cloudSpeed.value = THREE.MathUtils.lerp(0.0025, 0.024, speed01);
      if (rebuildOcean) renderer.compute(initWaves);
    }
    function getWindRuntime() {
      const speed01 = THREE.MathUtils.clamp(globalWind.speed / 50, 0, 1);
      const force = THREE.MathUtils.clamp(globalWind.force, 0, 1);
      return {
        speedMul: 0.45 + speed01 * 2.75,
        branchAmp: 0.35 + force * 2.6,
        leafAmp: 0.35 + force * 2.35,
        fallAmp: 0.25 + force * 2.15,
        dirX: Math.cos(globalWind.direction),
        dirZ: Math.sin(globalWind.direction),
      };
    }
    syncGlobalWind();

    function dayOfYearFromDate(date) {
      const start = new Date(date.getFullYear(), 0, 0);
      return Math.floor((date - start) / 86400000);
    }

    function smoothPulse01(x, a, b, c, d) {
      return THREE.MathUtils.smoothstep(x, a, b) * (1.0 - THREE.MathUtils.smoothstep(x, c, d));
    }

    function updateMicroClimateFromBrowserClock(now = new Date()) {
      if (!microClimate.autoBrowserClock || microClimate.liveWeatherEnabled) return;

      const hour = now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600;
      const time01 = hour / 24;
      const day = dayOfYearFromDate(now);
      const season01 = day / 365;
      const yearPhase = season01 * Math.PI * 2;
      const dayPhase = time01 * Math.PI * 2;

      const daylight = smoothPulse01(hour, 5.2, 8.1, 18.2, 22.1);
      const seasonWarmth = Math.sin(yearPhase - Math.PI * 0.48) * 0.5 + 0.5;
      const diurnalWarmth = Math.sin(dayPhase - Math.PI * 0.55) * 0.5 + 0.5;
      const slowWeather = Math.sin(yearPhase * 7.0 + Math.cos(dayPhase * 0.5)) * 0.5 + 0.5;

      microClimate.time01 = time01;
      microClimate.season01 = season01;
      microClimate.daylight = daylight;
      microClimate.airTemperature = THREE.MathUtils.lerp(7, 25, seasonWarmth) + THREE.MathUtils.lerp(-2.5, 3.8, diurnalWarmth);
      microClimate.humidity = THREE.MathUtils.clamp(72 - (microClimate.airTemperature - 12) * 1.3 + slowWeather * 24, 30, 96);
      microClimate.pressure = 1004 + (1.0 - slowWeather) * 18;
      microClimate.cloudCover = THREE.MathUtils.clamp(0.18 + slowWeather * 0.68 + (microClimate.humidity - 55) * 0.006, 0.08, 0.96);
      microClimate.rain = THREE.MathUtils.clamp((microClimate.cloudCover - 0.62) * 1.55 + (microClimate.humidity - 78) * 0.018, 0, 1);
      microClimate.windSpeed = THREE.MathUtils.clamp(5.2 + slowWeather * 13.0 + microClimate.rain * 4.0, 2.0, 28.0);
      microClimate.windDirection = (Math.PI * 0.25 + yearPhase * 0.35 + Math.sin(dayPhase * 0.7) * 0.55) % (Math.PI * 2);
      microClimate.windForce = THREE.MathUtils.clamp(0.13 + microClimate.windSpeed / 45 + microClimate.rain * 0.12, 0.05, 0.86);
      microClimate.tide01 = Math.sin((now.getTime() / 1000) * 0.00018 * PHI + season01 * Math.PI * 4.0);
      microClimate.liveWeatherStatus = 'horloge navigateur';
    }

    function applyMicroClimateToWorld(elapsed) {
      syncMicrocosmRealtimeLight(new Date());
      applyRealtimeSkyPalette();
      const wind01 = THREE.MathUtils.clamp(microClimate.windSpeed / 35, 0, 1);
      const humidity01 = THREE.MathUtils.clamp(microClimate.humidity / 100, 0, 1);
      const cloud01 = THREE.MathUtils.clamp(microClimate.cloudCover, 0, 1);
      const rain01 = THREE.MathUtils.clamp(microClimate.rain, 0, 1);
      const temp01 = THREE.MathUtils.clamp((microClimate.airTemperature + 5) / 35, 0, 1);

      syncGlobalWind({
        speed: microClimate.windSpeed,
        direction: microClimate.windDirection,
        force: microClimate.windForce,
        fromMicroClimate: true
      });

      // Air / lumière
      cloudCoverage.value = cloud01;
      cloudDensity.value = THREE.MathUtils.lerp(0.38, 1.12, cloud01);
      params.sunElevation = THREE.MathUtils.lerp(-0.8, 27.0, microClimate.daylight);
      params.sunAzimuth = (microClimate.time01 * 360 + 105) % 360;
      updateSun(params.sunElevation, params.sunAzimuth);

      // Eau : les réglages océaniques restent dérivés du même vent et de la même humidité.
      const nextChop = THREE.MathUtils.lerp(0.34, 1.05, wind01) + rain01 * 0.10;
      const nextHeight = THREE.MathUtils.lerp(0.32, 0.82, wind01) + rain01 * 0.12;
      const nextBump = THREE.MathUtils.lerp(0.075, 0.17, wind01) + rain01 * 0.035;
      choppiness.value = THREE.MathUtils.lerp(choppiness.value, nextChop, 0.018);
      heightScale.value = THREE.MathUtils.lerp(heightScale.value, nextHeight, 0.012);
      bumpStrengthU.value = THREE.MathUtils.lerp(bumpStrengthU.value, nextBump, 0.018);
      foamIntensity.value = THREE.MathUtils.lerp(foamIntensity.value, THREE.MathUtils.lerp(0.62, 1.45, wind01 + rain01 * 0.35), 0.018);
      if (!oceanCycleState.manualTideControls && !oceanManualOverrides.tideAmplitude) {
        oceanCycleState.tideAmplitude = THREE.MathUtils.lerp(0.018, 0.075, Math.abs(microClimate.tide01) * 0.6 + wind01 * 0.4);
        oceanCycleState.tideSpeed = THREE.MathUtils.lerp(0.16, 0.34, wind01);
      }

      // Végétation : le même air règle le mouvement et la vigueur visuelle.
      windAmplitude.value = THREE.MathUtils.lerp(0.09, 0.34, wind01) + rain01 * 0.04;
      if (!grassDensityManualOverride) {
        grassDensity.value = THREE.MathUtils.clamp(THREE.MathUtils.lerp(1.58, 1.92, humidity01) - rain01 * 0.04, 1.35, 1.98);
      }

      // Rivage : le fondu particulaire suit l'état de l'eau, sauf si l'utilisateur a repris la main.
      if (!oceanManualOverrides.shoreFade) {
        shoreFadeState.opacity = THREE.MathUtils.lerp(0.30, 0.58, cloud01 * 0.45 + wind01 * 0.55);
        shoreFadeState.width = THREE.MathUtils.lerp(1.15, 2.20, wind01 + rain01 * 0.25);
      }
      if (!oceanManualOverrides.shoreFoamStrength) {
        oceanShoreFoamStrength.value = THREE.MathUtils.lerp(0.52, 0.95, wind01 + rain01 * 0.25);
      }
      if (!oceanManualOverrides.shoreCalmDistance) {
        oceanShoreMinimumWave.value = THREE.MathUtils.lerp(0.018, 0.055, wind01);
      }

      // Le spectre de l'océan est recalculé par palier, pas à chaque frame.
      if (elapsed - microClimate.lastSpectrumUpdate > 5.0) {
        microClimate.lastSpectrumUpdate = elapsed;
        renderer.compute(initWaves);
      }

      // Une température basse ralentit très légèrement la respiration végétale, sans toucher à la croissance validée.
      cloudSpeed.value = THREE.MathUtils.lerp(0.003, 0.020, wind01) * THREE.MathUtils.lerp(0.82, 1.12, temp01);
    }

    function setManualMicroClimate(field, value) {
      microClimate.autoBrowserClock = false;
      microClimate.liveWeatherEnabled = false;
      microClimate[field] = value;
      applyMicroClimateToWorld(clock?.elapsedTime || 0);
    }

    function weatherCodeLabel(code) {
      const c = Number(code);
      if (c === 0) return 'Ensoleillé';
      if ([1, 2].includes(c)) return 'Partiellement nuageux';
      if (c === 3) return 'Nuageux';
      if ([45, 48].includes(c)) return 'Brouillard';
      if ([51, 53, 55, 56, 57].includes(c)) return 'Bruine';
      if ([61, 63, 65, 66, 67, 80, 81, 82].includes(c)) return 'Pluie';
      if ([71, 73, 75, 77, 85, 86].includes(c)) return 'Neige';
      if ([95, 96, 99].includes(c)) return 'Orage';
      return microClimate.liveWeatherEnabled ? 'Météo live' : 'Microclimat simulé';
    }

    function kmDistanceApprox(lat1, lon1, lat2, lon2) {
      const R = 6371;
      const dLat = THREE.MathUtils.degToRad(lat2 - lat1);
      const dLon = THREE.MathUtils.degToRad(lon2 - lon1);
      const a = Math.sin(dLat / 2) ** 2
        + Math.cos(THREE.MathUtils.degToRad(lat1)) * Math.cos(THREE.MathUtils.degToRad(lat2))
        * Math.sin(dLon / 2) ** 2;
      return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    }

    function weatherPlaceLabel() {
      if (microClimate.locationLabel) return microClimate.locationLabel;
      if (!Number.isFinite(microClimate.latitude) || !Number.isFinite(microClimate.longitude)) {
        return microClimate.liveWeatherEnabled ? 'Position météo' : 'Microclimat local';
      }
      const distToulouse = kmDistanceApprox(microClimate.latitude, microClimate.longitude, 43.6045, 1.4440);
      if (distToulouse < 35) return 'Toulouse, Occitanie';
      return `${microClimate.latitude.toFixed(3)}, ${microClimate.longitude.toFixed(3)}`;
    }



    // =====================================================
    // MICROCOSM CONFIG — zones modifiables sans toucher au cœur
    // Version stable prod : v8.21_simplified_game_ui_perf_weather_focus
    // La croissance validée de l'arbre n'est pas configurée ici.
    // =====================================================
    const MICROCOSM_CONFIG = {
      versionLabel: 'v8.21_simplified_game_ui_perf_weather_focus',
      urls: {
        yggdrasilLogo: 'https://presentcomposedesign.fr/wp-content/uploads/2026/05/YGGDRASIL-LOGO_ROUND-WHITE_PCdlab_2026.svg',
        arBaseUrl: 'https://presentcomposedesign.fr/microcosm-ar/'
      },

      // Compatibilité avec le code existant : ne pas supprimer ces deux alias.
      arExperienceBaseUrl: 'https://presentcomposedesign.fr/microcosm-ar/',
      authenticityLogoUrl: 'https://presentcomposedesign.fr/wp-content/uploads/2026/05/YGGDRASIL-LOGO_ROUND-WHITE_PCdlab_2026.svg',

      cards: {
        qrSize: 220,
        contributionDays: 371,
        maxLocationChips: 8,
        enableQr: true,
        enableSvgTexture: true,
        enablePngTexture: true,
        singleHtmlExportOnly: true,
        memoryCardFilenamePrefix: 'microcosm-memory-card'
      },

      weather: {
        compactByDefault: true,
        showLunarInfo: true,
        showWateringAdvice: true,
        lunarReference: {
          source: 'configuration locale + météo live si disponible',
          exampleDate: '2026-05-09',
          phase: 'Dernier quartier',
          movement: 'Lune montante',
          gardenType: 'Racines'
        },
        lunarOverrides: {
          '2026-05-09': {
            phaseLabel: 'Dernier quartier',
            gardenType: 'Racines',
            motion: 'Lune montante',
            illumination: 'décroissante',
            moonrise: '',
            moonset: ''
          }
        }
      },

      island: {
        grassInsetRatio: 0.985,
        grassToEarthRatio: 0.985,
        earthEdgeMargin: 0.28,
        shoreMargin: 0.42,
        bevelAngleDeg: 5,
        bevelDraftDegrees: 5,
        soilDepth: 1.4,
        edgeLip: 0.18
      },

      ocean: {
        preserveManualTideOverrides: true
      },

      product: {
        freeClicksPerDay: 1,
        windsAddonClicksPerDay: 5,
        lifetimeEnabled: false
      },

      performance: {
        maxPixelRatio: MICROCOSM_RUNTIME_PERF.maxPixelRatio,
        grassBladeInstances: MICROCOSM_RUNTIME_PERF.grassBladeInstances,
        grassMaxDensityMultiplier: MICROCOSM_RUNTIME_PERF.grassMaxDensityMultiplier,
        grassUpdateEvery: MICROCOSM_RUNTIME_PERF.grassUpdateEvery,
        oceanFineUpdateEvery: MICROCOSM_RUNTIME_PERF.oceanFineUpdateEvery,
        hudUpdateMs: MICROCOSM_RUNTIME_PERF.hudUpdateMs
      }
    };

    const YGGDRASIL_AUTH_STAMP_URL = MICROCOSM_CONFIG.authenticityLogoUrl;

    function getMicrocosmArLaunchUrl(payload = {}) {
      const base = MICROCOSM_CONFIG.arExperienceBaseUrl;
      const qs = new URLSearchParams({
        edition: payload.edition || 'preview',
        seed: payload.seedPublic || 'microcosm',
        day: String(payload.day || growthClicksTree || 0)
      });
      return `${base}?${qs.toString()}`;
    }

    function getQrImageUrl(data, size = MICROCOSM_CONFIG.cards.qrSize) {
      return `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&margin=12&format=svg&data=${encodeURIComponent(data)}`;
    }

    function syncMicrocosmRealtimeLight(now = new Date()) {
      const hour = now.getHours() + now.getMinutes() / 60 + now.getSeconds() / 3600;
      const time01 = hour / 24;
      const browserDaylight = smoothPulse01(hour, 5.2, 8.1, 18.2, 22.1);
      microClimate.time01 = time01;
      if (microClimate.liveWeatherEnabled && Number.isFinite(microClimate.liveWeatherIsDay)) {
        microClimate.daylight = microClimate.liveWeatherIsDay ? Math.max(browserDaylight, 0.76) : Math.min(browserDaylight, 0.08);
      } else if (!Number.isFinite(microClimate.daylight)) {
        microClimate.daylight = browserDaylight;
      } else if (microClimate.autoBrowserClock) {
        microClimate.daylight = browserDaylight;
      }
      return microClimate.daylight;
    }

    function applyRealtimeSkyPalette() {
      const d = THREE.MathUtils.clamp(microClimate.daylight ?? 1, 0, 1);
      const night = 1 - d;
      const rain = THREE.MathUtils.clamp(microClimate.rain ?? 0, 0, 1);
      const cloud = THREE.MathUtils.clamp(microClimate.cloudCover ?? 0, 0, 1);
      const skyDay = new THREE.Color(0x9edcff);
      const skyNight = new THREE.Color(0x070b12);
      const skyStorm = new THREE.Color(0x31404a);
      const bg = skyNight.clone().lerp(skyDay, d).lerp(skyStorm, Math.min(0.45, rain * 0.32 + cloud * 0.10));
      scene.background.copy(bg);
      renderer.setClearColor(bg, 1);
      if (skyDomeMat?.color) skyDomeMat.color.copy(new THREE.Color(0x3c4d5a).lerp(new THREE.Color(0xffffff), d));
      ambientLight.intensity = THREE.MathUtils.lerp(0.12, 0.52, d) * THREE.MathUtils.lerp(1.0, 0.76, rain);
      hemiLight.intensity = THREE.MathUtils.lerp(0.10, 0.34, d);
      sunLight.intensity = THREE.MathUtils.lerp(0.20, 4.5, d) * THREE.MathUtils.lerp(1.0, 0.72, cloud);
      treeDirLight.intensity = THREE.MathUtils.lerp(0.42, 1.8, d) * THREE.MathUtils.lerp(1.0, 0.82, rain);
      rimLight.intensity = THREE.MathUtils.lerp(0.14, 0.50, d);
      pointLight.color.set(night > 0.45 ? 0xd7c38a : 0xffccbb);
    }

    const rainMemoryState = (() => {
      try {
        return JSON.parse(localStorage.getItem('microcosm-rain-memory-v1') || '{}') || {};
      } catch (_) {
        return {};
      }
    })();

    function updateRainMemoryFromMicroClimate(now = new Date()) {
      const key = now.toISOString().slice(0, 10);
      const isRainy = (microClimate.rain ?? 0) > 0.08;
      if (!rainMemoryState.lastDate) {
        rainMemoryState.lastDate = key;
        rainMemoryState.daysWithoutRain = isRainy ? 0 : 1;
      } else if (rainMemoryState.lastDate !== key) {
        rainMemoryState.daysWithoutRain = isRainy ? 0 : Math.max(0, Number(rainMemoryState.daysWithoutRain || 0)) + 1;
        rainMemoryState.lastDate = key;
      } else if (isRainy) {
        rainMemoryState.daysWithoutRain = 0;
      }
      try { localStorage.setItem('microcosm-rain-memory-v1', JSON.stringify(rainMemoryState)); } catch (_) {}
      return Math.max(0, Number(rainMemoryState.daysWithoutRain || 0));
    }

    function computeMicrocosmWeatherInfo(daysWithoutRain) {
      const temp = Number(microClimate.airTemperature || 0);
      const humidity = Number(microClimate.humidity || 0);
      const wind = Number(microClimate.windSpeed || 0);
      const rain = Number(microClimate.rain || 0);
      const isNight = (microClimate.daylight ?? 1) < 0.16;
      if (rain > 0.08) return 'Pluie détectée · l’île se recharge naturellement.';
      if (isNight) return 'Nuit locale · croissance calme, respiration lente.';
      if (wind > 26) return 'Vent fort · feuilles et rivage plus actifs.';
      if (temp > 28 && humidity < 42) return 'Air chaud et sec · surveille la terre.';
      if (daysWithoutRain >= 3) return 'Séquence sèche · humidité à surveiller.';
      return 'Microclimat stable · observation quotidienne.';
    }

    function computeMicrocosmWateringAdvice(daysWithoutRain) {
      const temp = Number(microClimate.airTemperature || 0);
      const humidity = Number(microClimate.humidity || 0);
      const rain = Number(microClimate.rain || 0);
      if (rain > 0.08 || humidity >= 74) return 'Arrosage non nécessaire';
      if (daysWithoutRain >= 3 || (temp >= 25 && humidity <= 45)) return 'Arroser pourrait être utile aujourd’hui';
      return 'Arrosage léger seulement si la mousse est sèche';
    }



    function localDateKey(date = new Date()) {
      const y = date.getFullYear();
      const m = String(date.getMonth() + 1).padStart(2, '0');
      const d = String(date.getDate()).padStart(2, '0');
      return `${y}-${m}-${d}`;
    }

    const LUNAR_CALENDAR_OVERRIDES = {
      // 09/05/2026 : référence demandée pour l'écran d'inspiration.
      // Les données d'influence lunaire viennent du calendrier lunaire consulté.
      '2026-05-08': { phase: 'Décroissante', movement: 'Montante', gardenType: 'Racines', illumination: 63.5, riseSet: '02:48 · 11:05 Paris' },
      '2026-05-09': { phase: 'Dernier quartier', movement: 'Montante', gardenType: 'Racines', illumination: 53.8, riseSet: '03:12 · 12:17 Paris' }
    };

    function positiveModulo(value, modulo) {
      return ((value % modulo) + modulo) % modulo;
    }

    function computeLunarMicroInfo(date = new Date()) {
      const key = localDateKey(date);
      const override = LUNAR_CALENDAR_OVERRIDES[key];
      if (override) {
        return {
          ...override,
          icon: override.icon || '◑',
          source: MICROCOSM_CONFIG.weather.lunarReference.source,
          label: `${override.phase} · ${override.movement}`,
          subline: `${override.gardenType} · ${Math.round(override.illumination)}% · ${override.riseSet}`
        };
      }

      // Approximation astronomique suffisante pour l'UI locale :
      // nouvelle lune de référence 2000-01-06 18:14 UTC, cycle synodique moyen.
      const synodic = 29.53058867;
      const ref = Date.UTC(2000, 0, 6, 18, 14, 0);
      const days = (date.getTime() - ref) / 86400000;
      const age = positiveModulo(days, synodic);
      const phase01 = age / synodic;
      const illumination = Math.round((1 - Math.cos(2 * Math.PI * phase01)) * 50);
      let phase = 'Nouvelle lune';
      let icon = '●';
      if (phase01 >= 0.03 && phase01 < 0.22) { phase = 'Croissante'; icon = '☽'; }
      else if (phase01 >= 0.22 && phase01 < 0.28) { phase = 'Premier quartier'; icon = '◐'; }
      else if (phase01 >= 0.28 && phase01 < 0.47) { phase = 'Gibbeuse croissante'; icon = '◖'; }
      else if (phase01 >= 0.47 && phase01 < 0.53) { phase = 'Pleine lune'; icon = '○'; }
      else if (phase01 >= 0.53 && phase01 < 0.72) { phase = 'Gibbeuse décroissante'; icon = '◗'; }
      else if (phase01 >= 0.72 && phase01 < 0.78) { phase = 'Dernier quartier'; icon = '◑'; }
      else if (phase01 >= 0.78) { phase = 'Décroissante'; icon = '☾'; }
      const sidereal = 27.321661;
      const movement = Math.sin((positiveModulo(days, sidereal) / sidereal) * Math.PI * 2) >= 0 ? 'Montante' : 'Descendante';
      const gardenTypes = ['Racines', 'Fleurs', 'Feuilles', 'Fruits'];
      const gardenType = gardenTypes[Math.floor(positiveModulo(days + 1, 4))];
      return {
        phase,
        movement,
        gardenType,
        illumination,
        icon,
        source: 'estimation locale',
        label: `${phase} · ${movement}`,
        subline: `${gardenType} · ${illumination}% · ${date.toLocaleDateString('fr-FR', { day: '2-digit', month: 'short' })}`
      };
    }

    function getWeatherVisualIcon(lunarInfo = computeLunarMicroInfo()) {
      const isNight = microClimate.liveWeatherIsDay === 0 || (microClimate.daylight ?? 1) < 0.16;
      if (isNight) return { icon: lunarInfo.icon || '☾', label: 'Nuit locale' };
      if ((microClimate.rain ?? 0) > 0.08) return { icon: '🌧️', label: 'Pluie' };
      if ((microClimate.cloudCover ?? 0) > 0.72) return { icon: '☁️', label: 'Nuageux' };
      if ((microClimate.cloudCover ?? 0) > 0.38) return { icon: '⛅', label: 'Variable' };
      return { icon: '☀️', label: 'Soleil' };
    }

    const MICRO_ACTIVITY_KEY = 'microcosm-tree-activity-v2';
    const microActivityState = (() => {
      try {
        const parsed = JSON.parse(localStorage.getItem(MICRO_ACTIVITY_KEY) || '{}') || {};
        return {
          daily: parsed.daily && typeof parsed.daily === 'object' ? parsed.daily : {},
          locations: Array.isArray(parsed.locations) ? parsed.locations.slice(0, 40) : []
        };
      } catch (_) {
        return { daily: {}, locations: [] };
      }
    })();

    function saveMicroActivityState() {
      try { localStorage.setItem(MICRO_ACTIVITY_KEY, JSON.stringify(microActivityState)); } catch (_) {}
    }

    const microActivitySessionSeen = new Set();

    function recordMicrocosmVisit(date = new Date(), options = {}) {
      const key = localDateKey(date);
      const place = weatherPlaceLabel();
      const sessionKey = `${key}|${place || 'local'}`;
      if (!options.force && microActivitySessionSeen.has(sessionKey)) return;
      microActivitySessionSeen.add(sessionKey);
      const row = microActivityState.daily[key] || { visits: 0, clicks: 0, places: [] };
      row.visits = Math.min(99, Number(row.visits || 0) + 1);
      if (place && !row.places.includes(place)) row.places.push(place);
      microActivityState.daily[key] = row;
      if (place && !microActivityState.locations.some(item => item.place === place)) {
        microActivityState.locations.unshift({ place, firstSeen: date.toISOString(), day: growthClicksTree });
        microActivityState.locations = microActivityState.locations.slice(0, 40);
      }
      saveMicroActivityState();
    }

    function recordMicrocosmTreeClick(date = new Date()) {
      const key = localDateKey(date);
      const row = microActivityState.daily[key] || { visits: 0, clicks: 0, places: [] };
      row.clicks = Math.min(99, Number(row.clicks || 0) + 1);
      const place = weatherPlaceLabel();
      if (place && !row.places.includes(place)) row.places.push(place);
      microActivityState.daily[key] = row;
      saveMicroActivityState();
    }

    function getMicrocosmActivitySnapshot(days = MICROCOSM_CONFIG.cards.contributionDays) {
      const out = [];
      const today = new Date();
      for (let i = days - 1; i >= 0; i--) {
        const d = new Date(today);
        d.setDate(today.getDate() - i);
        const key = localDateKey(d);
        const row = microActivityState.daily[key] || { visits: 0, clicks: 0, places: [] };
        const score = Number(row.visits || 0) + Number(row.clicks || 0) * 2;
        const level = score <= 0 ? 0 : score === 1 ? 1 : score <= 3 ? 2 : score <= 6 ? 3 : 4;
        out.push({
          date: key,
          visits: Number(row.visits || 0),
          clicks: Number(row.clicks || 0),
          level,
          places: Array.isArray(row.places) ? row.places.slice(0, 3) : []
        });
      }
      return {
        days: out,
        locations: microActivityState.locations.slice(0, MICROCOSM_CONFIG.cards.maxLocationChips),
        totalVisits: out.reduce((sum, item) => sum + item.visits, 0),
        totalClicks: out.reduce((sum, item) => sum + item.clicks, 0)
      };
    }

    const weatherHudEls = {
      root: document.getElementById('weather-hud'),
      place: document.getElementById('weather-place'),
      time: document.getElementById('weather-time'),
      date: document.getElementById('weather-date'),
      visualIcon: document.getElementById('weather-visual-icon'),
      lunarIcon: document.getElementById('weather-lunar-icon'),
      lunarPhase: document.getElementById('weather-lunar-phase'),
      lunarDetail: document.getElementById('weather-lunar-detail'),
      advice: document.getElementById('weather-advice'),
      rainlessDays: document.getElementById('weather-rainless-days'),
      source: document.getElementById('weather-source'),
      icon: document.getElementById('weather-icon'),
      temp: document.getElementById('weather-temp'),
      desc: document.getElementById('weather-desc'),
      wind: document.getElementById('weather-wind'),
      humidity: document.getElementById('weather-humidity'),
      feels: document.getElementById('weather-feels'),
      pressure: document.getElementById('weather-pressure'),
      clouds: document.getElementById('weather-clouds'),
      rain: document.getElementById('weather-rain'),
      daylight: document.getElementById('weather-daylight'),
      sync: document.getElementById('weather-sync'),
      compareApi: document.getElementById('weather-compare-api'),
      compareWorld: document.getElementById('weather-compare-world'),
      note: document.getElementById('weather-note'),
      refresh: document.getElementById('weather-refresh'),
      locationInput: document.getElementById('weather-location-input'),
      locationSearch: document.getElementById('weather-location-search'),
      locationGps: document.getElementById('weather-location-gps'),
      tune: document.querySelector('.weather-tune-btn'),
      focusMain: document.getElementById('weather-focus-main'),
      focusSub: document.getElementById('weather-focus-sub')
    };
    let lastHudUpdate = 0;


    function mountPlayerHudIntoRitualDock() {
      const dock = document.querySelector('.microcosm-ritual-dock');
      const weatherAnchor = dock?.querySelector('.ritual-weather-anchor');
      const progressAnchor = dock?.querySelector('.ritual-progress-anchor');
      if (weatherAnchor && weatherHudEls.root && weatherHudEls.root.parentElement !== weatherAnchor) {
        weatherAnchor.appendChild(weatherHudEls.root);
      }
      const growthUi = document.querySelector('.tree-growth-ui');
      if (progressAnchor && growthUi && growthUi.parentElement !== progressAnchor) {
        progressAnchor.appendChild(growthUi);
      }
    }
    requestAnimationFrame(mountPlayerHudIntoRitualDock);


    const hudOverlayEls = {
      toggle: document.getElementById('ui-toggle'),
      panel: document.getElementById('ui-panel')
    };

    function updateHudStackLayout() {
      const weatherRoot = weatherHudEls.root;
      const panelRoot = hudOverlayEls.panel;
      if (!weatherRoot || !panelRoot) return;
      if (!window.matchMedia('(max-width: 900px)').matches) {
        document.documentElement.style.setProperty('--microcosm-weather-stack-bottom', '64px');
        return;
      }
      const gap = 10;
      const stackedTop = Math.round(weatherRoot.offsetTop + weatherRoot.offsetHeight + gap);
      document.documentElement.style.setProperty('--microcosm-weather-stack-bottom', `${stackedTop}px`);
    }

    if ('ResizeObserver' in window && weatherHudEls.root) {
      const hudStackObserver = new ResizeObserver(() => {
        updateHudStackLayout();
      });
      hudStackObserver.observe(weatherHudEls.root);
    }

    function compactWateringAdviceLabel(advice = '') {
      const value = String(advice || '').toLowerCase();
      if (value.includes('non nécessaire') || value.includes('pas nécessaire')) return 'Arrosage non nécessaire';
      if (value.includes('léger')) return 'Arrosage léger';
      if (value.includes('utile') || value.includes('arroser')) return 'Arrosage utile aujourd’hui';
      if (value.includes('sèche') || value.includes('sec')) return 'Vérifier la mousse';
      return 'Observer le sol';
    }

    function usefulMicroclimateLine(daysWithoutRain = 0) {
      const humidity = Math.round(microClimate.humidity);
      const rain = Math.round((microClimate.rain || 0) * 100);
      const wind = Math.round(microClimate.windSpeed);
      const isNight = microClimate.liveWeatherIsDay === 0 || (microClimate.daylight ?? 1) < 0.16;
      if (rain > 8) return `pluie ${rain}% · l’île se recharge`;
      if (humidity < 45) return `humidité ${humidity}% · mousse à surveiller`;
      if (humidity >= 74) return `humidité ${humidity}% · sol déjà frais`;
      if (daysWithoutRain >= 3) return `${daysWithoutRain} jours sans pluie · surveiller la terre`;
      if (isNight) return `nuit locale · croissance calme`;
      if (wind > 26) return `vent ${wind} km/h · feuillage actif`;
      return `humidité ${humidity}% · microclimat stable`;
    }

    function updateWeatherHUD(force = false) {
      const nowMs = performance.now();
      if (!force && nowMs - lastHudUpdate < MICROCOSM_RUNTIME_PERF.hudUpdateMs) return;
      lastHudUpdate = nowMs;

      const live = microClimate.liveWeatherEnabled && !!microClimate.lastLiveWeatherFetch;
      const status = live ? 'LIVE' : (microClimate.autoBrowserClock ? 'AUTO' : 'MANUEL');
      const desc = live ? weatherCodeLabel(microClimate.liveWeatherCode) : 'Horloge navigateur';
      const feels = Number.isFinite(microClimate.liveWeatherApparent) ? microClimate.liveWeatherApparent : microClimate.airTemperature;
      const windDeg = (THREE.MathUtils.radToDeg(microClimate.windDirection) + 360) % 360;
      const apiDiffT = live ? Math.abs(microClimate.airTemperature - (Number.isFinite(microClimate.liveWeatherApparent) ? microClimate.liveWeatherApparent : microClimate.airTemperature)) : 0;
      const syncOk = live ? 'OK live' : (microClimate.autoBrowserClock ? 'simulé' : 'manuel');
      const localNow = new Date();
      const timeText = localNow.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
      const dateText = localNow.toLocaleDateString('fr-FR', { weekday: 'short', day: '2-digit', month: 'short' });
      const daysWithoutRain = updateRainMemoryFromMicroClimate(localNow);
      const weatherInfo = computeMicrocosmWeatherInfo(daysWithoutRain);
      const wateringAdvice = computeMicrocosmWateringAdvice(daysWithoutRain);
      const lunarInfo = computeLunarMicroInfo(localNow);
      const visual = getWeatherVisualIcon(lunarInfo);

      weatherHudEls.place.textContent = weatherPlaceLabel();
      if (weatherHudEls.locationInput && !document.activeElement?.isSameNode(weatherHudEls.locationInput)) {
        weatherHudEls.locationInput.value = microClimate.locationLabel || '';
      }
      weatherHudEls.time.textContent = timeText;
      if (weatherHudEls.date) weatherHudEls.date.textContent = dateText;
      weatherHudEls.source.textContent = status;
      if (weatherHudEls.visualIcon) {
        weatherHudEls.visualIcon.textContent = visual.icon;
        weatherHudEls.visualIcon.setAttribute('aria-label', visual.label);
      }
      if (weatherHudEls.lunarIcon) weatherHudEls.lunarIcon.textContent = lunarInfo.icon || '☾';
      if (weatherHudEls.lunarPhase) weatherHudEls.lunarPhase.textContent = lunarInfo.label;
      if (weatherHudEls.lunarDetail) weatherHudEls.lunarDetail.textContent = lunarInfo.subline + ' · ' + lunarInfo.source;
      weatherHudEls.temp.textContent = Math.round(microClimate.airTemperature);
      weatherHudEls.desc.textContent = weatherInfo;
      if (weatherHudEls.advice) weatherHudEls.advice.textContent = wateringAdvice;
      if (weatherHudEls.rainlessDays) weatherHudEls.rainlessDays.textContent = String(daysWithoutRain);
      if (weatherHudEls.focusMain) weatherHudEls.focusMain.textContent = `${compactWateringAdviceLabel(wateringAdvice)} · ${Math.round(microClimate.airTemperature)}°`;
      if (weatherHudEls.focusSub) weatherHudEls.focusSub.textContent = usefulMicroclimateLine(daysWithoutRain);
      weatherHudEls.wind.textContent = `${Math.round(microClimate.windSpeed)} km/h`;
      weatherHudEls.humidity.textContent = `${Math.round(microClimate.humidity)}%`;
      weatherHudEls.feels.textContent = `${Math.round(feels)}°`;
      weatherHudEls.pressure.textContent = `${Math.round(microClimate.pressure)} hPa`;
      weatherHudEls.clouds.textContent = `${Math.round(microClimate.cloudCover * 100)}%`;
      weatherHudEls.rain.textContent = `${Math.round(microClimate.rain * 100)}%`;
      weatherHudEls.daylight.textContent = `${Math.round(microClimate.daylight * 100)}%`;
      weatherHudEls.sync.textContent = syncOk;

      const iconHue = microClimate.liveWeatherIsDay === 0 || microClimate.daylight < 0.15 ? 'radial-gradient(circle at 35% 30%, rgba(224, 219, 184, 0.92), rgba(65, 71, 48, 0.70) 62%, rgba(22, 18, 12, 0.62))' :
        microClimate.rain > 0.25 ? 'radial-gradient(circle at 35% 30%, rgba(207, 220, 196, 0.92), rgba(82, 106, 87, 0.70) 62%, rgba(31, 38, 27, 0.58))' :
        microClimate.cloudCover > 0.65 ? 'radial-gradient(circle at 35% 30%, rgba(231, 226, 199, 0.92), rgba(142, 133, 102, 0.72) 62%, rgba(53, 43, 28, 0.58))' :
        'radial-gradient(circle at 35% 30%, rgba(255, 238, 181, 0.95), rgba(176, 126, 54, 0.74) 58%, rgba(87, 62, 31, 0.52))';
      weatherHudEls.icon.style.background = iconHue;

      weatherHudEls.compareApi.textContent = live
        ? `T ${Math.round(microClimate.airTemperature)}° · vent ${Math.round(microClimate.windSpeed)} · dir ${Math.round(windDeg)}°`
        : 'pas de relevé live';
      weatherHudEls.compareWorld.textContent =
        `houle ${heightScale.value.toFixed(2)} · herbe ${windAmplitude.value.toFixed(2)}`;
      weatherHudEls.note.textContent = live
        ? `Données appliquées au microcosme. Si ton app météo affiche ~${Math.round(microClimate.airTemperature)}°C, ${Math.round(microClimate.windSpeed)} km/h et ${Math.round(microClimate.humidity)}% d’humidité, la synchro fonctionne.`
        : (microClimate.liveWeatherError || 'Active “Météo live” puis autorise la position pour vérifier avec les données réelles.');
      recordMicrocosmVisit(localNow);
      requestAnimationFrame(updateHudStackLayout);
    }

    weatherHudEls.refresh?.addEventListener('click', (event) => {
      event.stopPropagation();
      microClimate.liveWeatherEnabled = true;
      microClimate.autoBrowserClock = false;
      requestLiveMicroWeather();
      updateWeatherHUD(true);
    });
    weatherHudEls.locationSearch?.addEventListener('click', (event) => {
      event.stopPropagation();
      searchWeatherLocation(weatherHudEls.locationInput?.value || '');
    });
    weatherHudEls.locationGps?.addEventListener('click', (event) => {
      event.stopPropagation();
      microClimate.locationLabel = '';
      microClimate.liveWeatherEnabled = true;
      microClimate.autoBrowserClock = false;
      requestLiveMicroWeather();
      updateWeatherHUD(true);
    });
    weatherHudEls.locationInput?.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') {
        event.preventDefault();
        event.stopPropagation();
        searchWeatherLocation(weatherHudEls.locationInput.value);
      }
    });
    function setWeatherAdvancedOpen(open) {
      if (!weatherHudEls.root) return;
      weatherHudEls.root.classList.toggle('compact', !open);
      weatherHudEls.tune?.setAttribute('aria-expanded', open ? 'true' : 'false');
      weatherHudEls.tune && (weatherHudEls.tune.textContent = open ? 'Réduire' : 'Affiner');
      requestAnimationFrame(updateHudStackLayout);
    }

    weatherHudEls.tune?.addEventListener('click', (event) => {
      event.stopPropagation();
      setWeatherAdvancedOpen(weatherHudEls.root?.classList.contains('compact'));
    });

    async function fetchLiveWeatherAt(lat, lon, label = '') {
      microClimate.liveWeatherStatus = 'météo live...';
      microClimate.liveWeatherError = '';
      microClimate.liveWeatherEnabled = true;
      microClimate.autoBrowserClock = false;
      microClimate.latitude = lat;
      microClimate.longitude = lon;
      if (label) microClimate.locationLabel = label;
      try {
        const url = `https://api.open-meteo.com/v1/forecast?latitude=${microClimate.latitude}&longitude=${microClimate.longitude}&current=temperature_2m,relative_humidity_2m,apparent_temperature,pressure_msl,wind_speed_10m,wind_direction_10m,cloud_cover,precipitation,weather_code,is_day&timezone=auto`;
        const res = await fetch(url);
        const data = await res.json();
        const cur = data.current || {};
        if (typeof cur.time === 'string') microClimate.liveWeatherTime = cur.time;
        if (Number.isFinite(cur.temperature_2m)) microClimate.airTemperature = cur.temperature_2m;
        if (Number.isFinite(cur.apparent_temperature)) microClimate.liveWeatherApparent = cur.apparent_temperature;
        if (Number.isFinite(cur.relative_humidity_2m)) microClimate.humidity = cur.relative_humidity_2m;
        if (Number.isFinite(cur.pressure_msl)) microClimate.pressure = cur.pressure_msl;
        if (Number.isFinite(cur.wind_speed_10m)) microClimate.windSpeed = THREE.MathUtils.clamp(cur.wind_speed_10m, 0, 45);
        if (Number.isFinite(cur.wind_direction_10m)) microClimate.windDirection = THREE.MathUtils.degToRad(cur.wind_direction_10m);
        if (Number.isFinite(cur.cloud_cover)) microClimate.cloudCover = THREE.MathUtils.clamp(cur.cloud_cover / 100, 0, 1);
        if (Number.isFinite(cur.precipitation)) microClimate.rain = THREE.MathUtils.clamp(cur.precipitation / 4, 0, 1);
        if (Number.isFinite(cur.weather_code)) microClimate.liveWeatherCode = cur.weather_code;
        if (Number.isFinite(cur.is_day)) microClimate.liveWeatherIsDay = cur.is_day;
        syncMicrocosmRealtimeLight(new Date());
        microClimate.windForce = THREE.MathUtils.clamp(0.12 + microClimate.windSpeed / 48 + microClimate.rain * 0.12, 0.05, 0.9);
        microClimate.liveWeatherStatus = 'météo live';
        microClimate.liveWeatherError = '';
        microClimate.lastLiveWeatherFetch = Date.now();
        applyMicroClimateToWorld(clock?.elapsedTime || 0);
        updateWeatherHUD(true);
      } catch (err) {
        console.warn('Météo live indisponible :', err);
        microClimate.liveWeatherError = 'Météo live indisponible : vérifie la connexion ou réessaie.';
        updateWeatherHUD(true);
      }
    }

    async function searchWeatherLocation(query) {
      const q = String(query || '').trim();
      if (!q) return;
      microClimate.liveWeatherStatus = 'recherche lieu...';
      microClimate.liveWeatherError = '';
      updateWeatherHUD(true);

      const gpsMatch = q.match(/^\s*(-?\d+(?:[\.,]\d+)?)\s*[,; ]\s*(-?\d+(?:[\.,]\d+)?)\s*$/);
      if (gpsMatch) {
        const lat = parseFloat(gpsMatch[1].replace(',', '.'));
        const lon = parseFloat(gpsMatch[2].replace(',', '.'));
        if (Number.isFinite(lat) && Number.isFinite(lon)) {
          await fetchLiveWeatherAt(lat, lon, `${lat.toFixed(3)}, ${lon.toFixed(3)}`);
        }
        return;
      }

      try {
        const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(q)}&count=1&language=fr&format=json`;
        const res = await fetch(url);
        const data = await res.json();
        const place = data.results?.[0];
        if (!place) {
          microClimate.liveWeatherError = `Adresse introuvable : “${q}”. Essaie une ville ou lat,lon.`;
          updateWeatherHUD(true);
          return;
        }
        const label = [place.name, place.admin1, place.country].filter(Boolean).join(', ');
        await fetchLiveWeatherAt(place.latitude, place.longitude, label);
      } catch (err) {
        console.warn('Recherche météo impossible :', err);
        microClimate.liveWeatherError = 'Recherche météo impossible : connexion ou API indisponible.';
        updateWeatherHUD(true);
      }
    }

    async function requestLiveMicroWeather() {
      if (!navigator.geolocation) {
        microClimate.liveWeatherStatus = 'géolocalisation indisponible';
        return;
      }
      microClimate.liveWeatherStatus = 'géolocalisation...';
      navigator.geolocation.getCurrentPosition(async (pos) => {
        const lat = pos.coords.latitude;
        const lon = pos.coords.longitude;
        const distToulouse = kmDistanceApprox(lat, lon, 43.6045, 1.4440);
        const label = distToulouse < 35 ? 'Toulouse, Occitanie' : `${lat.toFixed(3)}, ${lon.toFixed(3)}`;
        await fetchLiveWeatherAt(lat, lon, label);
      }, () => {
        microClimate.liveWeatherEnabled = false;
        microClimate.autoBrowserClock = true;
        microClimate.liveWeatherStatus = 'localisation refusée';
        microClimate.liveWeatherError = 'Localisation refusée : impossible de vérifier Toulouse automatiquement.';
        updateWeatherHUD(true);
      }, { enableHighAccuracy: false, timeout: 8000, maximumAge: 20 * 60 * 1000 });
    }

    const bladeWidth = uniform(1.4);
    const bladeHeight = uniform(0.7);
    let grassBladeWidthUser = 1.0;
    let grassBladeLengthUser = 1.0;
    const bladeLean = uniform(1.3);
    const noiseAmplitude = uniform(1.85);
    const noiseFrequency = uniform(0.3);
    const noise2Amplitude = uniform(0.25);
    const noise2Frequency = uniform(15);
    const bladeColorVariation = uniform(0.97);
    const bladeGradientFalloff = uniform(1.7);
    const groundRadiusGrass = uniform(15.0 * grassSeedConfig.size);
    const groundFalloff = uniform(2.8);
    const bladeBaseColorU = uniform(new THREE.Color(BLADE_BASE_HEX));
    const bladeTipColorU = uniform(new THREE.Color(BLADE_TIP_HEX));
    const backgroundColorU = uniform(new THREE.Color(BACKGROUND_HEX));
    const groundColorU = uniform(new THREE.Color(GROUND_HEX));
    // 2D noise
    const noise2D = Fn(([x, z]) => {
      return mx_noise_float(vec3(x, float(0), z)).mul(0.5).add(0.7);
    });
    const grassBoundaryMask = Fn(([wx, wz, radius]) => {
      const dist = sqrt(wx.mul(wx).add(wz.mul(wz)));
      const ang = atan(wz, wx);
      const lobeA = sin(ang.mul(3.0).add(grassShapePhaseA));
      const lobeB = sin(ang.mul(5.0).add(grassShapePhaseB)).mul(0.55);
      const edgeNoise = mx_noise_float(vec3(wx.mul(0.075).add(grassSeedX), float(0), wz.mul(0.075).add(grassSeedZ))).sub(0.5);
      const shape = float(1.0).add(lobeA.add(lobeB).mul(grassShapeRoundness)).add(edgeNoise.mul(0.16));
      const shapedRadius = radius.mul(shape).max(3.0);
      return float(1).sub(smoothstep(shapedRadius.sub(groundFalloff), shapedRadius, dist));
    });
    // Compute init blades
    const computeInitGrass = Fn(() => {
      const blade = bladeData.element(instanceIndex);
      const col = instanceIndex.mod(GRASS_GRID);
      const row = instanceIndex.div(GRASS_GRID);
      const jx = hash(instanceIndex).sub(0.5);
      const jz = hash(instanceIndex.add(7919)).sub(0.5);
      const wx = col.toFloat().add(jx).div(float(GRASS_GRID)).sub(0.5).mul(FIELD_SIZE);
      const wz = row.toFloat().add(jz).div(float(GRASS_GRID)).sub(0.5).mul(FIELD_SIZE);
      blade.x.assign(wx);
      blade.y.assign(wz);
      blade.z.assign(hash(instanceIndex.add(1337)).mul(PI.mul(2)));
      const n1 = noise2D(wx.mul(noiseFrequency), wz.mul(noiseFrequency));
      const n2 = noise2D(
        wx.mul(noiseFrequency.mul(noise2Frequency)).add(50),
        wz.mul(noiseFrequency.mul(noise2Frequency)).add(50)
      );
      const clump = n1.mul(noiseAmplitude).sub(noise2Amplitude).add(n2.mul(noise2Amplitude).mul(2)).max(0);
      blade.w.assign(clump);
      const boundary = grassBoundaryMask(wx, wz, grassMaxRadius);
      bladeBound.element(instanceIndex).assign(select(boundary.lessThan(0.035), float(0), boundary));
    })().compute(BLADE_COUNT);
    // Compute update grass
    const computeUpdateGrass = Fn(() => {
      const blade = bladeData.element(instanceIndex);
      const bend = bendState.element(instanceIndex);
      const bx = blade.x;
      const bz = blade.y;
      // Use the global wind speed uniform (windSpeedU) to drive grass movement
      const w1 = sin(bx.mul(0.35).add(bz.mul(0.12)).add(time.mul(windSpeedU)));
      const w2 = sin(bx.mul(0.28).add(bz.mul(0.28)).add(time.mul(windSpeedU.mul(0.67))).add(1.7));
      const windX = w1.add(w2).mul(windAmplitude);
      const windZ = w1.sub(w2).mul(windAmplitude.mul(0.55));
      const lw = deltaTime.mul(4.0).saturate();
      bend.x.assign(mix(bend.x, windX, lw));
      bend.y.assign(mix(bend.y, windZ, lw));
      const dx = bx.sub(mouseWorld.x);
      const dz = bz.sub(mouseWorld.z);
      const dist = sqrt(dx.mul(dx).add(dz.mul(dz))).add(0.0001);
      const falloff = float(1).sub(dist.div(mouseRadius).saturate());
      const influence = falloff.mul(falloff).mul(mouseStrength);
      const pushX = dx.div(dist).mul(influence);
      const pushZ = dz.div(dist).mul(influence);
      const idx2 = bx.sub(icoWorld.x);
      const idz2 = bz.sub(icoWorld.z);
      const idist2 = sqrt(idx2.mul(idx2).add(idz2.mul(idz2))).add(0.0001);
      const ifalloff2 = float(1).sub(idist2.div(icoRadius).saturate());
      const iinfluence2 = ifalloff2.mul(ifalloff2).mul(icoStrengthU);
      const ipushX = idx2.div(idist2).mul(iinfluence2);
      const ipushZ = idz2.div(idist2).mul(iinfluence2);
      const totalPushX = pushX.add(ipushX);
      const totalPushZ = pushZ.add(ipushZ);
      const targetMag = sqrt(totalPushX.mul(totalPushX).add(totalPushZ.mul(totalPushZ)));
      const currentMag = sqrt(bend.z.mul(bend.z).add(bend.w.mul(bend.w)));
      const lm = select(targetMag.greaterThan(currentMag), deltaTime.mul(12.0), deltaTime.mul(1)).saturate();
      bend.z.assign(mix(bend.z, totalPushX, lm));
      bend.w.assign(mix(bend.w, totalPushZ, lm));
    })().compute(BLADE_COUNT);
    // Grass geometry
    function createBladeGeometry() {
      const segs = 5;
      const W = 0.055;
      const H = 1.0;
      const verts = [], norms = [], uvArr = [], idx = [];
      for (let i = 0; i <= segs; i++) {
        const t = i / segs;
        const y = t * H;
        const hw = W * 0.5 * (1.0 - t * 0.82);
        verts.push(-hw, y, 0, hw, y, 0);
        norms.push(0, 0, 1, 0, 0, 1);
        uvArr.push(0, t, 1, t);
      }
      for (let i = 0; i < segs; i++) {
        const b = i * 2;
        idx.push(b, b + 1, b + 2, b + 1, b + 3, b + 2);
      }
      const geo = new THREE.BufferGeometry();
      geo.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3));
      geo.setAttribute('normal', new THREE.Float32BufferAttribute(norms, 3));
      geo.setAttribute('uv', new THREE.Float32BufferAttribute(uvArr, 2));
      geo.setIndex(idx);
      return geo;
    }
    const grassMat = new THREE.MeshBasicNodeMaterial({ side: THREE.DoubleSide });
    grassMat.positionNode = Fn(() => {
      const blade = bladeData.element(instanceIndex);
      const bend = bendState.element(instanceIndex);
      const worldX = blade.x;
      const worldZ = blade.y;
      const rotY = blade.z;
      const boundary = bladeBound.element(instanceIndex).mul(grassBoundaryMask(worldX, worldZ, grassVisibleRadius));
      // Pool d'herbe surdimensionné : à ×1 on garde le rendu v8.24, à ×10 on libère vraiment ~10× plus de brins.
      const densityThreshold = clamp(grassDensity.mul(0.5).mul(grassDensityMultiplier).div(grassMaxDensityMultiplier), float(0.0), float(1.0));
      const rawVisible = select(hash(instanceIndex.add(9999)).lessThan(densityThreshold), float(1), float(0));
      // Fenêtres de terre : autour du gland jouable et du centre, la terre reste visible même sur mobile.
      const dxPlayable = worldX.sub(icoWorld.x);
      const dzPlayable = worldZ.sub(icoWorld.z);
      const distPlayable = sqrt(dxPlayable.mul(dxPlayable).add(dzPlayable.mul(dzPlayable)));
      const playableEarthWindow = smoothstep(float(0.34), float(0.88), distPlayable);
      const distCenter = sqrt(worldX.mul(worldX).add(worldZ.mul(worldZ)));
      const centerEarthWindow = smoothstep(float(0.24), float(0.76), distCenter);
      const visible = rawVisible.mul(playableEarthWindow).mul(centerEarthWindow);
      const heightScale = float(0.25).add(blade.w).mul(boundary).mul(visible);
      const lx = positionGeometry.x.mul(bladeWidth).mul(heightScale.sign());
      const ly = positionGeometry.y.mul(heightScale).mul(bladeHeight);
      const cY = cos(rotY);
      const sY = sin(rotY);
      const rx = lx.mul(cY);
      const rz = lx.mul(sY);
      const t = uv().y;
      const bendFactor = pow(t, 1.8);
      const staticBendX = hash(instanceIndex.add(7777)).sub(0.5).mul(bladeLean);
      const staticBendZ = hash(instanceIndex.add(8888)).sub(0.5).mul(bladeLean);
      const bendX = staticBendX.add(bend.x).add(bend.z);
      const bendZ = staticBendZ.add(bend.y).add(bend.w);
      const relX = rx.add(bendX.mul(bendFactor).mul(bladeHeight));
      const relY = ly;
      const relZ = rz.add(bendZ.mul(bendFactor).mul(bladeHeight));
      const origLen = sqrt(rx.mul(rx).add(ly.mul(ly)).add(rz.mul(rz)));
      const newLen = sqrt(relX.mul(relX).add(relY.mul(relY)).add(relZ.mul(relZ)));
      const scale = origLen.div(newLen.max(0.0001));
      const islandDist = sqrt(worldX.mul(worldX).add(worldZ.mul(worldZ)));
      const islandCurveY = islandDist.mul(islandDist).mul(islandCurveStrength).negate();
      return vec3(worldX.add(relX.mul(scale)), relY.mul(scale).add(islandCurveY), worldZ.add(relZ.mul(scale)));
    })();
    grassMat.colorNode = Fn(() => {
      const t = uv().y;
      const clump = bladeData.element(instanceIndex).w.saturate();
      const gradient = pow(t, bladeGradientFalloff);
      const tipMix = float(1).sub(bladeColorVariation).add(clump.mul(bladeColorVariation));
      const variedTip = mix(bladeBaseColorU, bladeTipColorU, tipMix);
      return mix(bladeBaseColorU, variedTip, gradient);
    })();
    grassMat.opacityNode = smoothstep(float(0.0), float(0.1), uv().y);
    grassMat.transparent = true;
    const bladeGeo = createBladeGeometry();
    const grass = new THREE.InstancedMesh(bladeGeo, grassMat, BLADE_COUNT);
    grass.frustumCulled = false;
    islandRoot.add(grass);
    const dummyMatrix = new THREE.Object3D();
    for (let i = 0; i < BLADE_COUNT; i++) grass.setMatrixAt(i, dummyMatrix.matrix);
    grass.instanceMatrix.needsUpdate = true;
    // Icosahedron
    const icoGeo = new THREE.IcosahedronGeometry(0.33, 1);
    icoGeo.computeVertexNormals();
    const icoMat = new THREE.MeshStandardNodeMaterial({ color: new THREE.Color('#ffffff'), roughness: 0.95, metalness: 0.05, flatShading: false });
    const icosahedron = new THREE.Mesh(icoGeo, icoMat);
    icosahedron.position.set(0, 0.45, 0);
    // Proxy physique invisible par défaut : aucune ombre ni masque hérité de l'ancienne géosphère.
    icosahedron.castShadow = false;
    icosahedron.receiveShadow = false;
    islandRoot.add(icosahedron);
    const icoVisualModelHost = new THREE.Group();
    icoVisualModelHost.name = 'PlayableAcornModelHost';
    icosahedron.add(icoVisualModelHost);
    const icoVel = new THREE.Vector3();
    const DEFAULT_INTERACTIVE_AVATAR_MODEL_URL = 'https://presentcomposedesign.fr/wp-content/uploads/2026/04/Oak-seed-hyper3dai_basic_pbr_PCdlab.glb';
    const DEFAULT_INTERACTIVE_AVATAR_LABEL = 'Gland de chêne';
    const interactiveSphereState = {
      speed: 65.0,
      damping: 0.92,
      gravity: 22.0,
      jumpForce: 10.0,
      modelUrl: DEFAULT_INTERACTIVE_AVATAR_MODEL_URL,
      modelLabel: DEFAULT_INTERACTIVE_AVATAR_LABEL,
      modelObject: null,
      modelIsFallback: false
    };
    const keysPressed = {};
    // Le gland est un élément jouable posé sur la terre, pas un avatar flottant.
    const playableAcornRadius = 0.35;
    const playableAcornContactInset = 0.018;
    let groundY = playableAcornRadius;
    function getPlayableGroundYAt(x, z) {
      try {
        if (typeof islandCurveYAtXZ === 'function') return islandCurveYAtXZ(x, z) - 0.035;
      } catch (_) {}
      return 0;
    }
    function getPlayableAcornCenterY(x, z) {
      return getPlayableGroundYAt(x, z) + playableAcornRadius - playableAcornContactInset;
    }
    function alignPlayableAcornBottomToGround(object) {
      if (!object) return;
      object.updateMatrixWorld(true);
      const box = new THREE.Box3().setFromObject(object);
      if (!Number.isFinite(box.min.y)) return;
      object.position.y += (-playableAcornRadius) - box.min.y;
      object.updateMatrixWorld(true);
    }
    const interactiveAvatarLoader = new GLTFLoader();
    interactiveAvatarLoader.setCrossOrigin?.('anonymous');
    let interactiveAvatarLoadToken = 0;

    function setPhysicsProxyVisible(visible) {
      icoMat.transparent = !visible;
      icoMat.opacity = visible ? 1 : 0;
      icosahedron.visible = true;
      icosahedron.castShadow = !!visible;
      icosahedron.receiveShadow = !!visible;
      icoMat.depthWrite = !!visible;
      icoMat.needsUpdate = true;
    }

    function disposeInteractiveAvatarHost() {
      while (icoVisualModelHost.children.length) {
        const child = icoVisualModelHost.children[0];
        icoVisualModelHost.remove(child);
        child.traverse?.((node) => {
          if (node.geometry?.dispose) node.geometry.dispose();
          const mats = Array.isArray(node.material) ? node.material : (node.material ? [node.material] : []);
          mats.forEach(mat => mat?.dispose?.());
        });
      }
    }

    function createProceduralAcornAvatar() {
      const acorn = new THREE.Group();
      acorn.name = 'FallbackProceduralPlayableOakSeed';
      const bodyMat = new THREE.MeshStandardMaterial({
        color: new THREE.Color('#8a5a2b'),
        roughness: 0.88,
        metalness: 0.02
      });
      const capMat = new THREE.MeshStandardMaterial({
        color: new THREE.Color('#3f2b18'),
        roughness: 0.94,
        metalness: 0.02
      });
      const stemMat = new THREE.MeshStandardMaterial({
        color: new THREE.Color('#2c1d10'),
        roughness: 0.96,
        metalness: 0.0
      });
      const body = new THREE.Mesh(new THREE.SphereGeometry(0.28, 24, 18), bodyMat);
      body.name = 'OakSeedBody';
      body.scale.set(0.82, 1.18, 0.82);
      body.position.y = 0.03;
      const cap = new THREE.Mesh(new THREE.SphereGeometry(0.30, 24, 12, 0, Math.PI * 2, 0, Math.PI * 0.56), capMat);
      cap.name = 'OakSeedCap';
      cap.scale.set(0.98, 0.48, 0.98);
      cap.position.y = 0.31;
      const stem = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.055, 0.18, 10), stemMat);
      stem.name = 'OakSeedStem';
      stem.position.y = 0.54;
      stem.rotation.z = -0.28;
      [body, cap, stem].forEach((part) => {
        part.castShadow = true;
        part.receiveShadow = true;
        acorn.add(part);
      });
      acorn.rotation.z = -0.10;
      alignPlayableAcornBottomToGround(acorn);
      return acorn;
    }

    function mountProceduralAcornAvatar(reason = 'fallback') {
      disposeInteractiveAvatarHost();
      const acorn = createProceduralAcornAvatar();
      icoVisualModelHost.add(acorn);
      interactiveSphereState.modelObject = acorn;
      interactiveSphereState.modelLabel = reason === 'default' ? DEFAULT_INTERACTIVE_AVATAR_LABEL : 'Gland de chêne · fallback local';
      interactiveSphereState.modelIsFallback = reason !== 'loaded';
      setPhysicsProxyVisible(false);
      return acorn;
    }

    function clearInteractiveAvatarModel({ showPhysicsProxy = false } = {}) {
      disposeInteractiveAvatarHost();
      interactiveSphereState.modelObject = null;
      interactiveSphereState.modelUrl = '';
      interactiveSphereState.modelLabel = showPhysicsProxy ? 'Proxy physique blanc' : DEFAULT_INTERACTIVE_AVATAR_LABEL;
      interactiveSphereState.modelIsFallback = false;
      setPhysicsProxyVisible(showPhysicsProxy);
    }

    async function loadInteractiveAvatarModel(url, options = {}) {
      const cleanUrl = String(url || '').trim();
      const fallbackToAcorn = options.fallbackToAcorn !== false;
      if (!cleanUrl) {
        resetInteractiveAvatarToDefault();
        return;
      }
      const token = ++interactiveAvatarLoadToken;
      interactiveSphereState.modelUrl = cleanUrl;
      interactiveSphereState.modelLabel = cleanUrl === DEFAULT_INTERACTIVE_AVATAR_MODEL_URL ? DEFAULT_INTERACTIVE_AVATAR_LABEL : 'Modèle 3D personnalisé';
      try {
        const gltf = await interactiveAvatarLoader.loadAsync(cleanUrl);
        if (token !== interactiveAvatarLoadToken) return;
        const model = gltf.scene || gltf.scenes?.[0];
        if (!model) throw new Error('Modèle 3D vide');
        const box = new THREE.Box3().setFromObject(model);
        const size = new THREE.Vector3();
        const center = new THREE.Vector3();
        box.getSize(size);
        box.getCenter(center);
        const maxAxis = Math.max(size.x, size.y, size.z, 0.001);
        const scale = 0.78 / maxAxis;
        disposeInteractiveAvatarHost();
        model.position.sub(center.multiplyScalar(scale));
        model.scale.setScalar(scale);
        alignPlayableAcornBottomToGround(model);
        model.traverse((node) => {
          if (node.isMesh) {
            node.castShadow = true;
            node.receiveShadow = true;
          }
        });
        icoVisualModelHost.add(model);
        interactiveSphereState.modelObject = model;
        interactiveSphereState.modelIsFallback = false;
        setPhysicsProxyVisible(false);
      } catch (err) {
        if (token !== interactiveAvatarLoadToken) return;
        console.warn('[Microcosm] Modèle 3D du gland jouable non chargé, fallback gland local utilisé :', err);
        if (fallbackToAcorn) {
          mountProceduralAcornAvatar(cleanUrl === DEFAULT_INTERACTIVE_AVATAR_MODEL_URL ? 'default' : 'fallback');
          interactiveSphereState.modelUrl = cleanUrl;
        } else {
          clearInteractiveAvatarModel({ showPhysicsProxy: true });
        }
      }
    }

    function resetInteractiveAvatarToDefault() {
      interactiveAvatarLoadToken++;
      interactiveSphereState.modelUrl = DEFAULT_INTERACTIVE_AVATAR_MODEL_URL;
      mountProceduralAcornAvatar('default');
      loadInteractiveAvatarModel(DEFAULT_INTERACTIVE_AVATAR_MODEL_URL, { fallbackToAcorn: true });
    }

    resetInteractiveAvatarToDefault();

    let icoYVel = 0;
    let isGrounded = true;
    window.addEventListener('keydown', (e) => {
      keysPressed[e.key.toLowerCase()] = true;
      if (e.code === 'Space') {
        e.preventDefault();
        if (isGrounded) {
          icoYVel = interactiveSphereState.jumpForce;
          isGrounded = false;
        }
      }
    });
    window.addEventListener('keyup', (e) => {
      keysPressed[e.key.toLowerCase()] = false;
    });
    const cameraOffset = new THREE.Vector3(0, 22, 54);
    const cameraLerpSpeed = 1.6;
    const cameraTargetPos = new THREE.Vector3();
    const cameraTargetLook = new THREE.Vector3();
    function updateIcosahedron(dt) {
      const accel = new THREE.Vector3();
      if (keysPressed['z']) accel.z -= 1;
      if (keysPressed['s']) accel.z += 1;
      if (keysPressed['q']) accel.x -= 1;
      if (keysPressed['d']) accel.x += 1;
      const isMoving = accel.length() > 0.5;
      if (isMoving) {
        accel.normalize().multiplyScalar(interactiveSphereState.speed * dt);
        icoVel.add(accel);
      }
      icoVel.multiplyScalar(interactiveSphereState.damping);
      icosahedron.position.x += icoVel.x * dt;
      icosahedron.position.z += icoVel.z * dt;
      icoYVel += -Math.abs(interactiveSphereState.gravity) * dt;
      icosahedron.position.y += icoYVel * dt;
      groundY = getPlayableAcornCenterY(icosahedron.position.x, icosahedron.position.z);
      if (icosahedron.position.y <= groundY) {
        icosahedron.position.y = groundY;
        icoYVel = 0;
        isGrounded = true;
      }
      const halfField = FIELD_SIZE * 50;
      icosahedron.position.x = Math.max(-halfField, Math.min(halfField, icosahedron.position.x));
      icosahedron.position.z = Math.max(-halfField, Math.min(halfField, icosahedron.position.z));
      if (vrPlayableState.enabled) {
        const planarLen = Math.hypot(icosahedron.position.x, icosahedron.position.z);
        if (planarLen > vrPlayableState.radius) {
          const k = vrPlayableState.radius / Math.max(planarLen, 0.0001);
          icosahedron.position.x *= k;
          icosahedron.position.z *= k;
          icoVel.x *= 0.25;
          icoVel.z *= 0.25;
        }
      }
      if (icoVel.length() > 0.01) {
        const rotAxis = new THREE.Vector3(icoVel.z, 0, -icoVel.x).normalize();
        const rotAmount = icoVel.length() * dt * 5.0;
        icosahedron.rotateOnWorldAxis(rotAxis, rotAmount);
      }
      const heightAboveGround = icosahedron.position.y - groundY;
      const airFactor = Math.max(0, 1 - heightAboveGround * 1.5);
      icoWorld.value.set(
        icosahedron.position.x * airFactor + 99999 * (1 - airFactor),
        0,
        icosahedron.position.z * airFactor + 99999 * (1 - airFactor)
      );
      cameraTargetPos.set(
        icosahedron.position.x + cameraOffset.x,
        cameraOffset.y,
        icosahedron.position.z + cameraOffset.z
      );
      cameraTargetLook.set(
        icosahedron.position.x,
        icosahedron.position.y - 0.75,
        icosahedron.position.z
      );
      // La caméra automatique est maintenant cadrée sur la croissance de l'arbre
      // dans animate(), pas sur l'icosaèdre, afin d'éviter les cadrages vides.
    }
    // Ancien masque de sol/île. Il reste calculable mais invisible :
    // il créait une plaque carrée sombre autour de la graine et recouvrait la terre.
    const groundMatGrass = new THREE.MeshBasicNodeMaterial();
    groundMatGrass.colorNode = Fn(() => {
      const wx = positionWorld.x;
      const wz = positionWorld.z;
      const mask = grassBoundaryMask(wx, wz, groundRadiusGrass);
      return mix(backgroundColorU, groundColorU, mask);
    })();
    // Sol de masque courbé : il suit la même courbure que l'herbe, au lieu de rester plat.
    function buildCurvedGrassGroundGeometry() {
      const geo = new THREE.PlaneGeometry(FIELD_SIZE * 50, FIELD_SIZE * 50, 112, 112);
      geo.rotateX(-Math.PI / 2);
      const pos = geo.getAttribute('position');
      for (let i = 0; i < pos.count; i++) {
        const x = pos.getX(i);
        const z = pos.getZ(i);
        const sag = -(x * x + z * z) * islandCurveStrength.value;
        pos.setY(i, Math.max(sag, oceanPlanetCurveLimit.value + 1.5));
      }
      pos.needsUpdate = true;
      geo.computeVertexNormals();
      return geo;
    }
    const grassGround = new THREE.Mesh(buildCurvedGrassGroundGeometry(), groundMatGrass);
    grassGround.receiveShadow = false;
    grassGround.visible = false;
    islandRoot.add(grassGround);

    // Couche de terre stable sous l'herbe : île fixe, organique et légèrement courbée.
    // Elle ne dépend pas du déplacement de l'océan : utile pour le confort VR.
    const islandEarthMat = new THREE.MeshStandardMaterial({
      color: 0x5a3821,
      roughness: 0.98,
      metalness: 0.0,
      emissive: 0x160c05,
      emissiveIntensity: 0.16,
      side: THREE.DoubleSide
    });
    const islandEdgeMat = new THREE.MeshStandardMaterial({
      color: 0x342014,
      roughness: 1.0,
      metalness: 0.0,
      emissive: 0x100804,
      emissiveIntensity: 0.10,
      side: THREE.DoubleSide
    });
    const islandEarthGroup = new THREE.Group();
    islandRoot.add(islandEarthGroup);
    let islandEarthMesh = null;
    let islandEdgeMesh = null;

    function islandShapeRadiusAtAngle(angle, radius) {
      const lobeA = Math.sin(angle * 3.0 + grassSeedConfig.phaseA);
      const lobeB = Math.sin(angle * 5.0 + grassSeedConfig.phaseB) * 0.55;
      const lobeC = Math.sin(angle * 7.0 + grassSeedConfig.x * 0.013) * 0.20;
      return radius * Math.max(0.72, 1.0 + (lobeA + lobeB) * grassSeedConfig.roundness + lobeC * 0.07);
    }
    function islandCurveYAtXZ(x, z) {
      return Math.max(-(x * x + z * z) * islandCurveStrength.value, oceanPlanetCurveLimit.value + 1.5);
    }

    function buildIslandEarthGeometry(radius) {
      const radial = 26;
      const segments = 144;
      const positions = [];
      const normals = [];
      const uvs = [];
      const indices = [];
      for (let r = 0; r <= radial; r++) {
        const rn = r / radial;
        for (let s = 0; s < segments; s++) {
          const a = (s / segments) * Math.PI * 2;
          const edgeR = islandShapeRadiusAtAngle(a, radius + MICROCOSM_CONFIG.island.earthEdgeMargin);
          const rr = edgeR * rn;
          const x = Math.cos(a) * rr;
          const z = Math.sin(a) * rr;
          // Même courbure que l'herbe : la terre devient un vrai kokedama/îlot,
          // et non plus un disque plat posé derrière.
          const dome = islandCurveYAtXZ(x, z) - 0.018 - rn * rn * 0.040;
          positions.push(x, dome, z);
          normals.push(0, 1, 0);
          uvs.push(rn, s / segments);
        }
      }
      for (let r = 0; r < radial; r++) {
        for (let s = 0; s < segments; s++) {
          const a = r * segments + s;
          const b = r * segments + ((s + 1) % segments);
          const c = (r + 1) * segments + s;
          const d = (r + 1) * segments + ((s + 1) % segments);
          indices.push(a, c, b, b, c, d);
        }
      }
      const geo = new THREE.BufferGeometry();
      geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
      geo.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
      geo.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
      geo.setIndex(indices);
      geo.computeVertexNormals();
      return geo;
    }

    function buildIslandEdgeGeometry(radius) {
      const segments = 144;
      const positions = [];
      const normals = [];
      const uvs = [];
      const indices = [];
      for (let s = 0; s < segments; s++) {
        const a = (s / segments) * Math.PI * 2;
        const edgeR = islandShapeRadiusAtAngle(a, radius + MICROCOSM_CONFIG.island.earthEdgeMargin);
        const x = Math.cos(a) * edgeR;
        const z = Math.sin(a) * edgeR;
        const curvedTop = islandCurveYAtXZ(x, z) - 0.026;
        const thickness = 0.46 + radius * 0.007;
        const draftInset = thickness * Math.tan(THREE.MathUtils.degToRad(MICROCOSM_CONFIG.island.bevelDraftDegrees));
        const bottomR = Math.max(0.1, edgeR - draftInset);
        const xb = Math.cos(a) * bottomR;
        const zb = Math.sin(a) * bottomR;
        const yTop = curvedTop;
        const yBottom = curvedTop - thickness;
        positions.push(x, yTop, z, xb, yBottom, zb);
        normals.push(Math.cos(a), 0.15, Math.sin(a), Math.cos(a), -0.2, Math.sin(a));
        uvs.push(s / segments, 0, s / segments, 1);
      }
      for (let s = 0; s < segments; s++) {
        const a = s * 2;
        const b = ((s + 1) % segments) * 2;
        indices.push(a, a + 1, b, b, a + 1, b + 1);
      }
      const geo = new THREE.BufferGeometry();
      geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
      geo.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
      geo.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
      geo.setIndex(indices);
      geo.computeVertexNormals();
      return geo;
    }

    function rebuildIslandEarth(radius) {
      if (islandEarthMesh) {
        islandEarthGroup.remove(islandEarthMesh);
        islandEarthMesh.geometry.dispose();
      }
      if (islandEdgeMesh) {
        islandEarthGroup.remove(islandEdgeMesh);
        islandEdgeMesh.geometry.dispose();
      }
      islandEarthMesh = new THREE.Mesh(buildIslandEarthGeometry(radius), islandEarthMat);
      islandEdgeMesh = new THREE.Mesh(buildIslandEdgeGeometry(radius), islandEdgeMat);
      islandEarthMesh.receiveShadow = true;
      islandEdgeMesh.receiveShadow = true;
      islandEarthMesh.renderOrder = -2;
      islandEdgeMesh.renderOrder = -3;
      islandEarthGroup.add(islandEarthMesh, islandEdgeMesh);
      if (window.__microcosmShoreFadeReady && typeof rebuildShoreBlueFadeParticles === 'function') rebuildShoreBlueFadeParticles(radius);
    }
    rebuildIslandEarth(groundRadiusGrass.value);

    const vrPlayableRingGeo = new THREE.RingGeometry(vrPlayableState.radius - 0.025, vrPlayableState.radius + 0.025, 128);
    vrPlayableRingGeo.rotateX(-Math.PI / 2);
    const vrPlayableRingMat = new THREE.MeshBasicMaterial({
      color: 0x7ee7ff,
      transparent: true,
      opacity: 0.34,
      side: THREE.DoubleSide,
      depthWrite: false
    });
    const vrPlayableRing = new THREE.Mesh(vrPlayableRingGeo, vrPlayableRingMat);
    vrPlayableRing.position.y = 0.022;
    vrPlayableRing.visible = vrPlayableState.visible;
    vrPlayableRing.renderOrder = 20;
    islandRoot.add(vrPlayableRing);

    window.__microcosmShoreFadeReady = true;
    let shoreFadePoints = null;
    function rebuildShoreOrganicFoamParticles(radius) {
      if (shoreFadePoints) {
        islandRoot.remove(shoreFadePoints);
        shoreFadePoints.geometry.dispose();
        shoreFadePoints.material.dispose();
        shoreFadePoints = null;
      }
      const count = Math.max(80, Math.floor(shoreFadeState.density));
      const positions = new Float32Array(count * 3);
      const colors = new Float32Array(count * 3);
      const baseColor = new THREE.Color(shoreFadeState.color);
      for (let i = 0; i < count; i++) {
        const a = (i / count) * Math.PI * 2 + (Math.random() - 0.5) * 0.035;
        const edgeR = islandShapeRadiusAtAngle(a, radius + MICROCOSM_CONFIG.island.earthEdgeMargin + MICROCOSM_CONFIG.island.shoreMargin);
        // Décalage principalement extérieur : la mousse épouse le tracé de l'îlot sans rentrer sous la terre.
        const radialJitter = (0.16 + Math.random() * 0.84) * shoreFadeState.width;
        const rr = edgeR + radialJitter;
        const x = Math.cos(a) * rr;
        const z = Math.sin(a) * rr;
        const y = islandCurveYAtXZ(x, z) - 0.012 + Math.random() * 0.055;
        positions[i * 3 + 0] = x;
        positions[i * 3 + 1] = y;
        positions[i * 3 + 2] = z;
        const c = baseColor.clone().lerp(new THREE.Color(0xffffff), 0.30 + Math.random() * 0.34);
        colors[i * 3 + 0] = c.r;
        colors[i * 3 + 1] = c.g;
        colors[i * 3 + 2] = c.b;
      }
      const geo = new THREE.BufferGeometry();
      geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
      geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
      const mat = new THREE.PointsMaterial({
        size: 0.092,
        vertexColors: true,
        transparent: true,
        opacity: shoreFadeState.enabled ? shoreFadeState.opacity : 0,
        depthWrite: false,
        sizeAttenuation: true
      });
      shoreFadePoints = new THREE.Points(geo, mat);
      shoreFadePoints.renderOrder = 18;
      shoreFadePoints.frustumCulled = false;
      islandRoot.add(shoreFadePoints);
    }
    rebuildShoreOrganicFoamParticles(groundRadiusGrass.value);

    // ───── Arbre (tree) ─────
    const treeGroup = new THREE.Group();
    islandRoot.add(treeGroup);
    const saplingGroupTree = new THREE.Group();
    islandRoot.add(saplingGroupTree);
    // Start the tree with a small sprout (10% grown)
    let growthProgress = 0;
    let isGrowing = false;
    let targetGrowth = 0;
    let growthSpeedTree = 0.065;
    const FIB_GROWTH_TOTAL_CLICKS_TREE = 365;
    const FIB_GROWTH_MILESTONES_TREE = [0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 365];
    let growthClicksTree = 0;
    let manualGrowthModeTree = false;
    let seasonModeTree = true;
    const treeSeasonState = {
      name: 'hiver',
      leafVisibility: 0.28,
      leafScale: 0.48,
      leafHue: new THREE.Color(0x6b8f2a),
      fallingLeaves: 0.15
    };
    const fibStageNamesTree = [
      'graine dormante',
      'premier germe',
      'racines profondes',
      'jeune pousse',
      'tige robuste',
      'premier rameau',
      'jeune arbre',
      'charpente naissante',
      'couronne en formation',
      'chêne adulte',
      'grand chêne',
      'vieux chêne',
      'chêne centenaire'
    ];
    function smoothStepTree(t) {
      t = Math.max(0, Math.min(1, t));
      return t * t * (3 - 2 * t);
    }
    function getFibonacciStageIndexTree(clicks) {
      for (let i = 0; i < FIB_GROWTH_MILESTONES_TREE.length - 1; i++) {
        if (clicks <= FIB_GROWTH_MILESTONES_TREE[i + 1]) return i;
      }
      return FIB_GROWTH_MILESTONES_TREE.length - 2;
    }
    function fibonacciGrowthCurveTree(clicks) {
      const c = Math.max(0, Math.min(FIB_GROWTH_TOTAL_CLICKS_TREE, clicks));
      if (c <= 0) return 0;
      if (c >= FIB_GROWTH_TOTAL_CLICKS_TREE) return 1;
      const stageIndex = getFibonacciStageIndexTree(c);
      const a = FIB_GROWTH_MILESTONES_TREE[stageIndex];
      const b = FIB_GROWTH_MILESTONES_TREE[stageIndex + 1];
      const localT = smoothStepTree((c - a) / Math.max(1, b - a));
      const fibStageProgress = (stageIndex + localT) / (FIB_GROWTH_MILESTONES_TREE.length - 2);
      const dayProgress = c / FIB_GROWTH_TOTAL_CLICKS_TREE;
      // Progression continue : les jalons Fibonacci rythment les stades,
      // mais la taille reste dominée par le nombre réel de jours/clics pour éviter les sauts visibles.
      const blended = dayProgress * 0.84 + fibStageProgress * 0.16;
      return Math.min(1, Math.pow(blended, 1.38));
    }
    function getFibonacciStageNameTree(clicks) {
      if (clicks >= FIB_GROWTH_TOTAL_CLICKS_TREE) return 'chêne centenaire accompli';
      const idx = getFibonacciStageIndexTree(Math.max(1, clicks));
      return fibStageNamesTree[Math.min(idx, fibStageNamesTree.length - 1)];
    }
    function getSeasonFromDayTree(day) {
      // Rythme saisonnier français sur 365 jours.
      // La densité des feuilles varie de façon continue, pas par ruptures :
      // printemps = bourgeonnement / repousse, été = pleine densité,
      // automne = jaunissement + perte, hiver = repos végétatif.
      const d = ((Math.round(day) - 1) % 365 + 365) % 365 + 1;

      if (d >= 60 && d <= 151) {
        const t = (d - 60) / 91;
        const e = smoothStepTree(t);
        return {
          name: 'printemps',
          leafVisibility: THREE.MathUtils.lerp(0.42, 0.92, e),
          leafScale: THREE.MathUtils.lerp(0.56, 0.92, e),
          leafHue: new THREE.Color(0x72b83f),
          fallingLeaves: 0.01
        };
      }

      if (d >= 152 && d <= 243) {
        const t = (d - 152) / 91;
        return {
          name: 'été',
          leafVisibility: 0.96,
          leafScale: 1.0,
          leafHue: new THREE.Color(0x2f7d32),
          fallingLeaves: 0.006
        };
      }

      if (d >= 244 && d <= 334) {
        const t = (d - 244) / 90;
        const e = smoothStepTree(t);
        return {
          name: 'automne',
          leafVisibility: THREE.MathUtils.lerp(0.86, 0.32, e),
          leafScale: THREE.MathUtils.lerp(0.86, 0.58, e),
          leafHue: new THREE.Color(0xa58a32),
          fallingLeaves: THREE.MathUtils.lerp(0.16, 0.55, e)
        };
      }

      // Hiver : l'arbre peut garder quelques bourgeons/feuilles terminales,
      // mais pas des rubans verts complets sur toutes les branches.
      return {
        name: 'hiver',
        leafVisibility: 0.16,
        leafScale: 0.40,
        leafHue: new THREE.Color(0x5f6f28),
        fallingLeaves: 0.05
      };
    }
    function setTreeGrowthByPercent(percent, updateClicks = true) {
      manualGrowthModeTree = true;
      const pct = THREE.MathUtils.clamp(percent, 0, 100);
      const p = pct / 100;
      growthProgress = p;
      targetGrowth = p;
      isGrowing = false;
      if (updateClicks) growthClicksTree = Math.round(p * FIB_GROWTH_TOTAL_CLICKS_TREE);
      restoreCameraAutoFollow({ clearUserLock: true });
      updateTreeCameraFrame(0.016, true);
      updateGrowthUITree?.();
      if (growthSliderTree) growthSliderTree.value = pct;
    }
    function updateTreeCameraFrame(dt, force = false) {
      if ((!cameraAutoFollow && !force) || renderer.xr.isPresenting) return;
      if (vrRuntimeState.viewMode === '1re personne' && typeof icosahedron !== 'undefined') {
        const lerpFactor = force ? 1 : 1.0 - Math.exp(-3.2 * dt);
        const forwardZ = keysPressed?.['z'] ? -1 : (keysPressed?.['s'] ? 1 : -1);
        cameraTargetPos.set(icosahedron.position.x, icosahedron.position.y + 1.35, icosahedron.position.z + 1.65 * forwardZ);
        cameraTargetLook.set(icosahedron.position.x, icosahedron.position.y + 1.05, icosahedron.position.z - 6.0 * forwardZ);
        cameraTargetPos.y += islandRoot.position.y;
        cameraTargetLook.y += islandRoot.position.y;
        camera.position.lerp(cameraTargetPos, lerpFactor);
        controls.target.lerp(cameraTargetLook, lerpFactor);
        if (force) {
          camera.lookAt(cameraTargetLook);
          camera.updateProjectionMatrix();
          controls.update();
        }
        return;
      }
      const p = THREE.MathUtils.clamp(growthProgress, 0, 1);
      // À partir de 69 % de croissance, on bloque le calcul de cadrage.
      // L'arbre continue à pousser, mais la caméra ne recule/dézoome plus automatiquement.
      const cameraP = Math.min(p, 0.69);
      const early = THREE.MathUtils.smoothstep(cameraP, 0.0, 0.18);
      const mid = THREE.MathUtils.smoothstep(cameraP, 0.16, 0.62);
      const late = THREE.MathUtils.smoothstep(cameraP, 0.55, 1.0);

      // Cadrage 1 an : mêmes étapes qu'avant, mais environ 2× plus zoomées.
      // Le cadrage est plafonné à 69 % pour éviter tout dézoom après ce seuil.
      const closePos = new THREE.Vector3(0, 2.6, 7.0);
      const midPos = new THREE.Vector3(0, 8.2, 18.0);
      const farPos = new THREE.Vector3(-4.0, 20.5, 42.0);
      const closeLook = new THREE.Vector3(0, 0.75, 0);
      const midLook = new THREE.Vector3(0, 5.2, 0);
      const farLook = new THREE.Vector3(0, 13.5, 0);

      cameraTargetPos.copy(closePos).lerp(midPos, early).lerp(farPos, late);
      cameraTargetLook.copy(closeLook).lerp(midLook, mid).lerp(farLook, late);
      // L'auto-cadrage suit le micro-monde flottant sans provoquer de dézoom.
      cameraTargetPos.y += islandRoot.position.y;
      cameraTargetLook.y += islandRoot.position.y;
      const lerpFactor = force ? 1 : 1.0 - Math.exp(-1.55 * dt);
      camera.position.lerp(cameraTargetPos, lerpFactor);
      controls.target.lerp(cameraTargetLook, lerpFactor);
      if (force) {
        camera.lookAt(cameraTargetLook);
        camera.updateProjectionMatrix();
        controls.update();
      }
    }

    function matureOakScaleTree(progress) {
      // Croissance organique continue : le chêne part très petit,
      // puis augmente sans palier visible. Le tronc adulte n’est jamais affiché
      // directement à sa taille finale.
      const p = Math.max(0, Math.min(1, progress));
      // Le modèle adulte commence vraiment après les premiers jours : avant cela,
      // on affiche une pousse dédiée, fine et feuillue, pour éviter un tronc adulte miniature.
      // Le modèle adulte ne prend le relais qu'après une vraie phase de jeune pousse lisible.
      // Avant ~J55, on laisse un brin unique/tige élégante porter l'expérience visuelle.
      const start = fibonacciGrowthCurveTree(55);
      const q = THREE.MathUtils.clamp((p - start) / (1 - start), 0, 1);
      const eased = q * q * (3 - 2 * q);
      return q <= 0 ? 0.0001 : 0.06 + eased * 0.94;
    }
    const branches = [];
    const leavesArr = [];
    const canopyPatchesArr = [];
    const flowersArr = [];
    const fallingLeavesArr = [];
    const barkMat = new THREE.MeshStandardMaterial({ color: 0x5f4938, roughness: 0.96, metalness: 0.01 });
    const darkBarkMat = new THREE.MeshStandardMaterial({ color: 0x3f3026, roughness: 0.98, metalness: 0.01 });
    // Graine déterministe : chaque navigateur/utilisateur garde son arbre,
    // et le bouton « Nouvelle graine » crée une nouvelle partie réellement unique.
    const TREE_USER_SEED_KEY = 'microcosm-tree-user-seed-v1';
    const TREE_GAME_SEED_KEY = 'microcosm-tree-game-seed-v1';
    function createTreeSeedStringTree(prefix = 'tree') {
      const bytes = new Uint32Array(4);
      if (window.crypto?.getRandomValues) {
        window.crypto.getRandomValues(bytes);
      } else {
        const now = Date.now();
        for (let i = 0; i < bytes.length; i++) bytes[i] = ((now + i * 2654435761) ^ Math.floor(Math.random() * 0xffffffff)) >>> 0;
      }
      return `${prefix}-${Date.now().toString(36)}-${Array.from(bytes, n => n.toString(36)).join('-')}`;
    }
    function readTreeStorageSeedTree(key, prefix) {
      try {
        let value = localStorage.getItem(key);
        if (!value) {
          value = createTreeSeedStringTree(prefix);
          localStorage.setItem(key, value);
        }
        return value;
      } catch (_) {
        return createTreeSeedStringTree(prefix);
      }
    }
    function hashTreeSeedStringTree(str) {
      let h = 2166136261 >>> 0;
      for (let i = 0; i < str.length; i++) {
        h ^= str.charCodeAt(i);
        h = Math.imul(h, 16777619) >>> 0;
      }
      return h >>> 0;
    }
    const treeUserSeedString = readTreeStorageSeedTree(TREE_USER_SEED_KEY, 'user');
    let treeGameSeedString = readTreeStorageSeedTree(TREE_GAME_SEED_KEY, 'game');
    let treeSeedBase = hashTreeSeedStringTree(`${treeUserSeedString}|${treeGameSeedString}`);
    let treeSeedState = treeSeedBase || 0x9e3779b9;
    function resetTreeRandomStreamTree() {
      treeSeedBase = hashTreeSeedStringTree(`${treeUserSeedString}|${treeGameSeedString}`);
      treeSeedState = treeSeedBase || 0x9e3779b9;
    }
    function treeRandom() {
      treeSeedState = (treeSeedState + 0x6D2B79F5) >>> 0;
      let t = treeSeedState;
      t = Math.imul(t ^ (t >>> 15), t | 1);
      t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    }
    function newTreeGameSeedTree() {
      treeGameSeedString = createTreeSeedStringTree('game');
      try { localStorage.setItem(TREE_GAME_SEED_KEY, treeGameSeedString); } catch (_) {}
      resetTreeRandomStreamTree();
    }
    resetTreeRandomStreamTree();
    // Use green hues for tree leaves instead of pink cherry blossoms
    const leafColorsArr = [0x1f5f2a, 0x2f7d32, 0x3d8f3f, 0x5f9f3a, 0x6b8f2a, 0x8aa13a];
    const flowerColorsArr = [0xffb7c5, 0xffc1cc, 0xffd4dc, 0xff9eb5, 0xffffff, 0xffe0e8];
    function createTaperedTubeTree(curve, segments, radiusStart, radiusEnd, radialSegments) {
      const frames = curve.computeFrenetFrames(segments, false);
      const vertices = [];
      const normals = [];
      const uvs = [];
      const indices = [];
      for (let i = 0; i <= segments; i++) {
        const t = i / segments;
        const r = radiusStart * (1 - t) + radiusEnd * t;
        const P = curve.getPointAt(t);
        const N = frames.normals[i];
        const B = frames.binormals[i];
        for (let j = 0; j <= radialSegments; j++) {
          const v = (j / radialSegments) * Math.PI * 2;
          const sinV = Math.sin(v);
          const cosV = -Math.cos(v);
          const nx = cosV * N.x + sinV * B.x;
          const ny = cosV * N.y + sinV * B.y;
          const nz = cosV * N.z + sinV * B.z;
          vertices.push(P.x + r * nx, P.y + r * ny, P.z + r * nz);
          normals.push(nx, ny, nz);
          uvs.push(j / radialSegments, t);
        }
      }
      for (let i = 0; i < segments; i++) {
        for (let j = 0; j < radialSegments; j++) {
          const a = i * (radialSegments + 1) + j;
          const b = a + radialSegments + 1;
          indices.push(a, b, a + 1);
          indices.push(b, b + 1, a + 1);
        }
      }
      const geo = new THREE.BufferGeometry();
      geo.setIndex(indices);
      geo.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
      geo.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
      geo.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
      return geo;
    }
    function createBranchTree(startPos, direction, length, radius, depth, parentGrowth) {
      // Ramification plus proche d'un chêne : charpentières solides,
      // puis nombreuses petites branches fines. Les diamètres diminuent
      // fortement pour éviter les “tuyaux” disproportionnés.
      const wobble = depth === 0 ? 0.025 : 0.045 + depth * 0.014;
      const gravityDroop = depth >= 2 ? -0.045 * depth : 0;
      const pts = [];
      const numPts = depth < 2 ? 8 : 6;
      for (let i = 0; i <= numPts; i++) {
        const t = i / numPts;
        const p = startPos.clone().add(direction.clone().multiplyScalar(length * t));
        if (i > 0 && i < numPts) {
          p.x += (treeRandom() - 0.5) * length * wobble;
          p.y += (treeRandom() - 0.35) * length * wobble * 0.35 + gravityDroop * t;
          p.z += (treeRandom() - 0.5) * length * wobble;
        }
        pts.push(p);
      }
      const curve = new THREE.CatmullRomCurve3(pts);
      const segments = Math.max(18, Math.floor(length * 8.0));
      const radialSegments = depth === 0 ? 14 : depth < 3 ? 9 : 6;
      const radiusBase = depth === 0 ? radius : radius * 1.03;
      // Évite l'effet de tronc/cime sectionné : même le tronc principal se termine
      // en vraie pointe organique, puis la cime est reprise par des branches terminales.
      const radiusEnd = radius * (depth === 0 ? 0.22 : depth < 2 ? 0.34 : depth < 4 ? 0.22 : 0.14);
      const geo = createTaperedTubeTree(curve, segments, radiusBase, radiusEnd, radialSegments);
      const mesh = new THREE.Mesh(geo, depth < 2 ? barkMat : darkBarkMat);
      mesh.castShadow = true;
      mesh.receiveShadow = true;
      const endPos = curve.getPointAt(1);
      const tangentEnd = curve.getTangentAt(1).normalize();
      const baseWindow = 0.16;
      const depthBonus = depth * 0.045;
      const lengthFactor = length / 10;
      const growthWindow = Math.min(0.48, (baseWindow + depthBonus) * (0.82 + lengthFactor * 0.18));
      const branchData = {
        mesh,
        curve,
        depth,
        growthStart: parentGrowth,
        growthEnd: parentGrowth + growthWindow,
        parentStartT: 0,
        endPos,
        endDirection: tangentEnd,
        length,
        radius,
        radiusEnd,
        segments,
        radialSegments,
        children: [],
        leavesAdded: false,
        fullGeo: geo,
        _lastVisibleSeg: -1,
        startPos: startPos.clone(),
        parentBranch: null,
      };
      geo.setDrawRange(0, 0);
      mesh.visible = false;
      mesh.frustumCulled = false;
      treeGroup.add(mesh);
      branches.push(branchData);

      if (depth < 5) {
        const numChildren = depth === 0 ? 5 + Math.floor(treeRandom() * 2) :
                            depth === 1 ? 4 + Math.floor(treeRandom() * 2) :
                            depth === 2 ? 3 + Math.floor(treeRandom() * 2) :
                            depth === 3 ? 2 + Math.floor(treeRandom() * 2) :
                            treeRandom() > 0.42 ? 1 : 0;
        for (let i = 0; i < numChildren; i++) {
          const childStartT = depth === 0 ? 0.36 + treeRandom() * 0.60 : 0.26 + treeRandom() * 0.68;
          const childStart = curve.getPointAt(childStartT);
          const parentTangent = curve.getTangentAt(childStartT).normalize();
          const connectionOverlap = radius * 0.18;
          childStart.addScaledVector(parentTangent, -connectionOverlap);
          const spreadAngle = depth === 0 ? 1.05 + treeRandom() * 0.55 : depth < 3 ? 0.85 + treeRandom() * 0.55 : 0.65 + treeRandom() * 0.45;
          const upwardBias = depth === 0 ? 0.42 : depth < 3 ? 0.26 : 0.14;
          const randomPerp = new THREE.Vector3((treeRandom() - 0.5), 0, (treeRandom() - 0.5));
          if (randomPerp.lengthSq() < 0.001) randomPerp.set(1, 0, 0);
          randomPerp.normalize();
          const childDir = new THREE.Vector3()
            .addScaledVector(parentTangent, depth === 0 ? 0.44 : 0.50)
            .addScaledVector(randomPerp, spreadAngle)
            .add(new THREE.Vector3(0, upwardBias, 0))
            .normalize();
          const lengthMult = depth === 0 ? 0.58 + treeRandom() * 0.18 :
                             depth === 1 ? 0.52 + treeRandom() * 0.16 :
                             depth === 2 ? 0.42 + treeRandom() * 0.15 :
                             depth === 3 ? 0.34 + treeRandom() * 0.13 :
                             0.28 + treeRandom() * 0.10;
          const childLength = length * lengthMult;
          const radiusMult = depth === 0 ? 0.42 + treeRandom() * 0.08 :
                             depth === 1 ? 0.38 + treeRandom() * 0.08 :
                             depth === 2 ? 0.32 + treeRandom() * 0.07 :
                             0.26 + treeRandom() * 0.06;
          const childRadius = Math.max(0.025, radius * radiusMult);
          const childGrowthStart = branchData.growthStart + (branchData.growthEnd - branchData.growthStart) * childStartT;
          const childBranch = createBranchTree(childStart, childDir, childLength, childRadius, depth + 1, childGrowthStart);
          childBranch.parentBranch = branchData;
          childBranch.parentStartT = childStartT;
          branchData.children.push(childBranch);
        }
      }
      return branchData;
    }
    // Leaves and flowers instanced meshes for tree
    const MAX_LEAVES_TREE = 16000; // budget optimisé : assez dense, mais beaucoup moins coûteux GPU que 30000
    const leafInstanceGeoTree = new THREE.SphereGeometry(0.42, 7, 5);
    leafInstanceGeoTree.scale(1.45, 0.22, 0.82);
    const leafInstanceMatsTree = leafColorsArr.map(c => new THREE.MeshStandardMaterial({ color: c, roughness: 0.5, metalness: 0.02, side: THREE.DoubleSide, emissive: new THREE.Color(c).multiplyScalar(0.08) }));
    const leafInstancedMeshesTree = [];
    const leafInstancesPerMeshTree = Math.ceil(MAX_LEAVES_TREE / leafColorsArr.length);
    for (let i = 0; i < leafColorsArr.length; i++) {
      const im = new THREE.InstancedMesh(leafInstanceGeoTree, leafInstanceMatsTree[i], leafInstancesPerMeshTree);
      im.count = 0;
      im.castShadow = true;
      im.frustumCulled = false;
      treeGroup.add(im);
      leafInstancedMeshesTree.push(im);
    }
    const flowerPetalGeoTree = new THREE.SphereGeometry(0.1, 5, 4);
    flowerPetalGeoTree.scale(1.8, 0.25, 1.2);
    const flowerPetalMatsTree = flowerColorsArr.map(c => new THREE.MeshStandardMaterial({ color: c, roughness: 0.4, metalness: 0.02, emissive: new THREE.Color(c).multiplyScalar(0.06), transparent: true, opacity: 0.92 }));
    const flowerInstancedMeshesTree = [];
    const petalsPerColorMeshTree = Math.ceil((MAX_LEAVES_TREE / 5) / flowerColorsArr.length);
    for (let i = 0; i < flowerColorsArr.length; i++) {
      const im = new THREE.InstancedMesh(flowerPetalGeoTree, flowerPetalMatsTree[i], petalsPerColorMeshTree);
      im.count = 0;
      im.castShadow = true;
      im.frustumCulled = false;
      treeGroup.add(im);
      flowerInstancedMeshesTree.push(im);
    }
    const flowerCenterGeoTree = new THREE.SphereGeometry(0.04, 6, 6);
    const flowerCenterMatTree = new THREE.MeshStandardMaterial({ color: 0xf5d76e, emissive: 0x4a3520, roughness: 0.6 });
    const flowerCenterIMTree = new THREE.InstancedMesh(flowerCenterGeoTree, flowerCenterMatTree, 300);
    flowerCenterIMTree.count = 0;
    flowerCenterIMTree.castShadow = true;
    flowerCenterIMTree.frustumCulled = false;
    treeGroup.add(flowerCenterIMTree);
    // helpers for leaves and flowers
    const _leafDummyTree = new THREE.Object3D();
    const _leafMatrixTree = new THREE.Matrix4();
    const _tempVecTree = new THREE.Vector3();
    const _tempQuatTree = new THREE.Quaternion();
    const _tempScaleTree = new THREE.Vector3();
    const _tempEulerTree = new THREE.Euler();
    const _leafTmpATree = new THREE.Vector3();
    const _leafTmpBTree = new THREE.Vector3();
    const _leafTmpCTree = new THREE.Vector3();
    function seeded01Tree(a, b = 0) {
      const seedOffset = (treeSeedBase % 1000003) * 0.000137;
      const s = Math.sin((a + seedOffset) * 127.1 + (b + seedOffset * 0.73) * 311.7) * 43758.5453123;
      return s - Math.floor(s);
    }
    function makeBranchSurfaceOffsetTree(branch, branchT, ringAngle, looseness = 1) {
      const tangent = branch.curve.getTangentAt(branchT).clone().normalize();
      const upRef = Math.abs(tangent.y) > 0.82 ? _leafTmpATree.set(1, 0, 0) : _leafTmpATree.set(0, 1, 0);
      const normal = _leafTmpBTree.copy(tangent).cross(upRef).normalize();
      if (normal.lengthSq() < 1e-6) normal.set(1, 0, 0);
      const binormal = _leafTmpCTree.copy(tangent).cross(normal).normalize();
      const branchRadiusAtT = THREE.MathUtils.lerp(branch.radius, branch.radiusEnd, branchT);
      const attachRadius = Math.max(0.025, branchRadiusAtT * (0.78 + 0.18 * looseness));
      const n = normal.clone().multiplyScalar(Math.cos(ringAngle) * attachRadius);
      const b = binormal.clone().multiplyScalar(Math.sin(ringAngle) * attachRadius);
      const along = tangent.multiplyScalar((seeded01Tree(branch.depth + branchT * 17.0, ringAngle) - 0.5) * attachRadius * 0.25);
      return n.add(b).add(along);
    }
    function createCanopyPatchTree(branch, branchT, ringAngle, options = {}) {
      // Désactivé : les patchs foliaires en Mesh séparés créaient trop de draw calls
      // et donnaient un feuillage incohérent en rubans. On utilise désormais
      // uniquement des feuilles instanciées optimisées.
      return null;
    }
    function createLeafTree(position, options = {}) {
      // Répartition équilibrée entre les meshes de couleurs : évite qu'une couleur
      // soit saturée trop tôt et que les feuilles générées plus tard, notamment
      // celles de la cime, soient simplement ignorées.
      let colorIdx = options.colorIdx ?? ((leavesArr.length + Math.floor(treeRandom() * leafColorsArr.length)) % leafColorsArr.length);
      let im = leafInstancedMeshesTree[colorIdx];
      let idx = im.count;
      if (idx >= leafInstancesPerMeshTree) {
        let found = false;
        for (let ci = 0; ci < leafInstancedMeshesTree.length; ci++) {
          const testIdx = (colorIdx + ci + 1) % leafInstancedMeshesTree.length;
          if (leafInstancedMeshesTree[testIdx].count < leafInstancesPerMeshTree) {
            colorIdx = testIdx;
            im = leafInstancedMeshesTree[colorIdx];
            idx = im.count;
            found = true;
            break;
          }
        }
        if (!found) return;
      }
      im.count = idx + 1;

      const branch = options.branch || null;
      const branchT = options.branchT ?? 1;
      const ringAngle = options.ringAngle ?? treeRandom() * Math.PI * 2;
      const jitterScale = options.jitterScale ?? (branch ? Math.max(0.22, 0.9 - branch.depth * 0.07) : 1.0);
      const localOffset = options.offset ? options.offset.clone() : branch
        ? makeBranchSurfaceOffsetTree(branch, branchT, ringAngle, jitterScale)
        : new THREE.Vector3(
            (treeRandom() - 0.5) * 0.35 * jitterScale,
            (treeRandom() - 0.25) * 0.20 * jitterScale,
            (treeRandom() - 0.5) * 0.35 * jitterScale
          );
      const basePos = branch ? branch.curve.getPointAt(branchT).add(localOffset) : position.clone().add(localOffset);
      const px = basePos.x;
      const py = basePos.y;
      const pz = basePos.z;
      const rx = treeRandom() * Math.PI;
      const ry = treeRandom() * Math.PI;
      const rz = treeRandom() * Math.PI;
      _leafDummyTree.position.set(px, py, pz);
      _leafDummyTree.rotation.set(rx, ry, rz);
      _leafDummyTree.scale.set(0.001, 0.001, 0.001);
      _leafDummyTree.updateMatrix();
      im.setMatrixAt(idx, _leafDummyTree.matrix);
      im.instanceMatrix.needsUpdate = true;

      const branchGrowthAtLeaf = branch
        ? branch.growthStart + (branch.growthEnd - branch.growthStart) * branchT
        : 0.02;
      // Les feuilles démarrent presque en même temps que le segment qui les porte.
      // Elles ne doivent plus attendre la moitié de la croissance globale.
      const earlyBias = branch ? (branch.depth <= 1 ? -0.095 : -0.065) : -0.015;
      const growthStart = THREE.MathUtils.clamp(
        options.growthStart ?? (branchGrowthAtLeaf + earlyBias + treeRandom() * 0.028),
        0.004,
        0.88
      );

      const leafData = {
        instancedMesh: im,
        instanceIdx: idx,
        colorIdx,
        branch,
        branchT,
        localOffset,
        growthStart,
        targetScale: options.targetScale ?? (0.54 + treeRandom() * 0.46),
        seasonSeed: options.seasonSeed ?? treeRandom(),
        terminalFollower: options.terminalFollower === true,
        tipPriority: options.tipPriority ?? 0,
        swayOffset: treeRandom() * Math.PI * 2,
        swaySpeed: 0.5 + treeRandom() * 1.5,
        originalPos: new THREE.Vector3(px, py, pz),
        baseRotX: rx,
        baseRotY: ry,
        baseRotZ: rz,
        currentScale: 0,
      };
      leavesArr.push(leafData);
    }
    const _flowerDummyTree = new THREE.Object3D();
    function createFlowerTree(position) {
      const colorIdx = Math.floor(treeRandom() * flowerColorsArr.length);
      const im = flowerInstancedMeshesTree[colorIdx];
      const baseRotX = treeRandom() * 0.5;
      const baseRotY = treeRandom() * Math.PI * 2;
      const baseRotZ = treeRandom() * 0.5;
      const targetScale = 0.8 + treeRandom() * 0.6;
      const petalIndices = [];
      const numPetals = 5;
      for (let i = 0; i < numPetals; i++) {
        const idx = im.count;
        if (idx >= petalsPerColorMeshTree) return;
        im.count = idx + 1;
        _flowerDummyTree.position.set(0, 0, 0);
        _flowerDummyTree.scale.set(0.001, 0.001, 0.001);
        _flowerDummyTree.updateMatrix();
        im.setMatrixAt(idx, _flowerDummyTree.matrix);
        petalIndices.push(idx);
      }
      im.instanceMatrix.needsUpdate = true;
      const centerIdx = flowerCenterIMTree.count;
      if (centerIdx >= 300) return;
      flowerCenterIMTree.count = centerIdx + 1;
      _flowerDummyTree.position.set(0, 0, 0);
      _flowerDummyTree.scale.set(0.001, 0.001, 0.001);
      _flowerDummyTree.updateMatrix();
      flowerCenterIMTree.setMatrixAt(centerIdx, _flowerDummyTree.matrix);
      flowerCenterIMTree.instanceMatrix.needsUpdate = true;
      const flowerData = {
        instancedMesh: im,
        petalIndices,
        centerIdx,
        colorIdx,
        position: position.clone(),
        growthStart: 0.7 + treeRandom() * 0.25,
        targetScale,
        swayOffset: treeRandom() * Math.PI * 2,
        baseRotX,
        baseRotY,
        baseRotZ,
        currentScale: 0,
      };
      flowersArr.push(flowerData);
    }
    // Falling leaves pool for tree
    const FALLING_LEAF_POOL_SIZE_TREE = 80;
    const fallingLeafPoolTree = [];
    let fallingLeafNextIdxTree = 0;
    const fallingLeafGeoTree = new THREE.PlaneGeometry(0.36, 0.46);
    const fallingLeafMatTree = new THREE.MeshStandardMaterial({ roughness: 0.45, side: THREE.DoubleSide, transparent: true, opacity: 1.0 });
    const fallingLeafIMTree = new THREE.InstancedMesh(fallingLeafGeoTree, fallingLeafMatTree, FALLING_LEAF_POOL_SIZE_TREE);
    fallingLeafIMTree.count = FALLING_LEAF_POOL_SIZE_TREE;
    fallingLeafIMTree.castShadow = true;
    fallingLeafIMTree.frustumCulled = false;
    treeGroup.add(fallingLeafIMTree);
    const _flDummyTree = new THREE.Object3D();
    for (let i = 0; i < FALLING_LEAF_POOL_SIZE_TREE; i++) {
      _flDummyTree.position.set(0, -100, 0);
      _flDummyTree.scale.set(0.001, 0.001, 0.001);
      _flDummyTree.updateMatrix();
      fallingLeafIMTree.setMatrixAt(i, _flDummyTree.matrix);
      const colorIdx = Math.floor(treeRandom() * leafColorsArr.length);
      const c = new THREE.Color(leafColorsArr[colorIdx]);
      fallingLeafIMTree.setColorAt(i, c);
      fallingLeafPoolTree.push({
        idx: i,
        position: new THREE.Vector3(0, -100, 0),
        rotation: new THREE.Euler(),
        scale: 1,
        velocity: new THREE.Vector3(),
        rotSpeed: new THREE.Vector3(),
        swayOffset: 0,
        life: 0,
        opacity: 0,
        active: false,
      });
    }
    fallingLeafIMTree.instanceMatrix.needsUpdate = true;
    fallingLeafIMTree.instanceColor.needsUpdate = true;
    function spawnFallingLeafTree() {
      const entry = fallingLeafPoolTree[fallingLeafNextIdxTree];
      fallingLeafNextIdxTree = (fallingLeafNextIdxTree + 1) % FALLING_LEAF_POOL_SIZE_TREE;
      const startAngle = treeRandom() * Math.PI * 2;
      const startRadius = 2 + treeRandom() * 13;
      const s = 0.7 + treeRandom() * 0.8;
      entry.position.set(Math.cos(startAngle) * startRadius, 16 + treeRandom() * 18, Math.sin(startAngle) * startRadius);
      entry.rotation.set(treeRandom() * Math.PI, treeRandom() * Math.PI, treeRandom() * Math.PI);
      entry.scale = s;
      entry.opacity = 0.85;
      const windRT = getWindRuntime();
      const fallPush = 0.006 * windRT.fallAmp * windRT.speedMul;
      entry.velocity.set(
        (treeRandom() - 0.5) * 0.02 + windRT.dirX * fallPush,
        -0.01 - treeRandom() * 0.02,
        (treeRandom() - 0.5) * 0.02 + windRT.dirZ * fallPush
      );
      entry.rotSpeed.set(
        (treeRandom() - 0.5) * 0.05 * windRT.speedMul,
        (treeRandom() - 0.5) * 0.05 * windRT.speedMul,
        (treeRandom() - 0.5) * 0.03 * windRT.speedMul
      );
      entry.swayOffset = treeRandom() * Math.PI * 2;
      entry.life = 1;
      entry.active = true;
      const colorIdx = Math.floor(treeRandom() * leafColorsArr.length);
      fallingLeafIMTree.setColorAt(entry.idx, new THREE.Color(leafColorsArr[colorIdx]));
    }
    // Visuels des premiers stades : graine → germe → jeune pousse → jeune arbre.
    // Ils évitent l'apparition irréaliste d'un gros tronc dès les premiers clics.
    const saplingPartsTree = [];
    function makeSaplingMaterialTree(color, opacity = 1) {
      return new THREE.MeshStandardMaterial({
        color,
        roughness: 0.74,
        metalness: 0.02,
        transparent: true,
        opacity,
        side: THREE.DoubleSide
      });
    }
    function addSaplingPartTree(mesh, start, peak, end, maxScale = 1) {
      mesh.visible = false;
      mesh.frustumCulled = false;
      const baseScale = mesh.scale.clone();
      saplingGroupTree.add(mesh);
      saplingPartsTree.push({ mesh, start, peak, end, maxScale, baseScale });
      return mesh;
    }
    function buildSaplingStagesTree() {
      saplingPartsTree.length = 0;
      const seedMat = makeSaplingMaterialTree(0x6a4a2f, 0.95);
      const stemMat = makeSaplingMaterialTree(0x4a3828, 0.98);
      const youngStemMat = makeSaplingMaterialTree(0x3f3124, 0.98);
      const leafMat = makeSaplingMaterialTree(0x4f8f2f, 0.96);
      const youngLeafMat = makeSaplingMaterialTree(0x6aa83d, 0.96);

      const d3 = fibonacciGrowthCurveTree(3);
      const d5 = fibonacciGrowthCurveTree(5);
      const d8 = fibonacciGrowthCurveTree(8);
      const d13 = fibonacciGrowthCurveTree(13);
      const d21 = fibonacciGrowthCurveTree(21);
      const d34 = fibonacciGrowthCurveTree(34);
      const d55 = fibonacciGrowthCurveTree(55);
      const d89 = fibonacciGrowthCurveTree(89);

      function makeBaseStemTree(height, radiusTop, radiusBottom, mat, radial = 10) {
        const geo = new THREE.CylinderGeometry(radiusTop, radiusBottom, height, radial);
        // Origine à la base : le scale donne une vraie pousse depuis le sol.
        geo.translate(0, height * 0.5, 0);
        const mesh = new THREE.Mesh(geo, mat.clone ? mat.clone() : mat);
        mesh.position.set(0, 0, 0);
        return mesh;
      }

      function makeTwigTree(baseY, length, angle, elevation, radius, mat) {
        const geo = new THREE.CylinderGeometry(radius * 0.52, radius, length, 8);
        geo.translate(0, length * 0.5, 0);
        const mesh = new THREE.Mesh(geo, mat.clone ? mat.clone() : mat);
        const dir = new THREE.Vector3(
          Math.cos(angle) * Math.cos(elevation),
          Math.sin(elevation),
          Math.sin(angle) * Math.cos(elevation)
        ).normalize();
        mesh.position.set(0, baseY, 0);
        mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
        return { mesh, dir, baseY, length, angle };
      }

      function addLeafAtTree(pos, angle, start, peak, end, scale = 1, mat = leafMat) {
        const leaf = new THREE.Mesh(new THREE.SphereGeometry(0.115, 8, 5), mat.clone());
        leaf.scale.set(1.45 * scale, 0.18 * scale, 0.70 * scale);
        leaf.position.copy(pos);
        leaf.rotation.set(0.55, -angle, 0.18);
        addSaplingPartTree(leaf, start, peak, end, 1.0);
        return leaf;
      }

      // Graine : courte, claire, avant l'apparition du brin.
      const seed = new THREE.Mesh(new THREE.SphereGeometry(0.16, 12, 8), seedMat);
      seed.scale.set(1.25, 0.50, 0.95);
      seed.position.set(0, 0.055, 0);
      addSaplingPartTree(seed, 0.0, fibonacciGrowthCurveTree(2), d8, 1.0);

      // Une seule tige initiale, progressive et lisible : pas de bouquet fouillis.
      const stemA = makeBaseStemTree(0.58, 0.020, 0.034, stemMat, 9);
      addSaplingPartTree(stemA, d3, d5, d13, 1.0);

      const stemB = makeBaseStemTree(0.96, 0.024, 0.044, stemMat, 9);
      stemB.rotation.z = 0.015;
      addSaplingPartTree(stemB, d5, d13, d21, 1.0);

      const stemC = makeBaseStemTree(1.42, 0.030, 0.056, youngStemMat, 10);
      stemC.rotation.z = 0.025;
      stemC.rotation.x = -0.012;
      addSaplingPartTree(stemC, d13, d21, d34, 1.0);

      const stemD = makeBaseStemTree(2.08, 0.040, 0.078, youngStemMat, 10);
      stemD.rotation.z = 0.035;
      stemD.rotation.x = 0.018;
      addSaplingPartTree(stemD, d21, d34, d89, 1.0);

      // Feuilles simples dès J5 : petites, attachées, pas en amas.
      addLeafAtTree(new THREE.Vector3(-0.105, 0.48, 0.025), Math.PI * 0.52, d5, d8, d34, 0.78, youngLeafMat);
      addLeafAtTree(new THREE.Vector3(0.115, 0.51, -0.020), -Math.PI * 0.48, d5, d8, d34, 0.78, youngLeafMat);

      // Croissance stable : quelques feuilles montent le long du brin à chaque palier Fibonacci.
      const leafSpecs = [
        { day: 8,  y: 0.78, a: 0.35, s: 0.80 },
        { day: 13, y: 1.02, a: 2.65, s: 0.86 },
        { day: 21, y: 1.28, a: 4.35, s: 0.92 },
        { day: 34, y: 1.62, a: 1.35, s: 0.95 }
      ];
      for (const spec of leafSpecs) {
        const ds = fibonacciGrowthCurveTree(spec.day);
        const dp = fibonacciGrowthCurveTree(spec.day === 8 ? 13 : spec.day === 13 ? 21 : spec.day === 21 ? 34 : 55);
        const pos = new THREE.Vector3(Math.cos(spec.a) * 0.12, spec.y, Math.sin(spec.a) * 0.12);
        addLeafAtTree(pos, spec.a, ds, dp, d89, spec.s, youngLeafMat);
      }

      // Premier rameau : un seul rameau au départ, puis un second plus tard.
      // Le joueur voit une ramification continue, pas une explosion de branches.
      const twigA = makeTwigTree(1.05, 0.62, 0.25, 0.30, 0.017, stemMat);
      addSaplingPartTree(twigA.mesh, d21, d34, d89, 1.0);
      for (let i = 0; i < 4; i++) {
        const f = 0.45 + i * 0.16;
        const side = i % 2 === 0 ? 1 : -1;
        const sideDir = new THREE.Vector3(-twigA.dir.z, 0, twigA.dir.x).normalize().multiplyScalar(0.055 * side);
        const pos = new THREE.Vector3().copy(twigA.dir).multiplyScalar(twigA.length * f).add(sideDir).add(new THREE.Vector3(0, twigA.baseY + i * 0.012, 0));
        addLeafAtTree(pos, twigA.angle + side * 0.45, d21, d34, d89, 0.78, youngLeafMat);
      }

      const twigB = makeTwigTree(1.34, 0.78, 2.72, 0.24, 0.018, stemMat);
      addSaplingPartTree(twigB.mesh, d34, d55, 0.30, 1.0);
      for (let i = 0; i < 5; i++) {
        const f = 0.35 + i * 0.14;
        const side = i % 2 === 0 ? 1 : -1;
        const sideDir = new THREE.Vector3(-twigB.dir.z, 0, twigB.dir.x).normalize().multiplyScalar(0.06 * side);
        const pos = new THREE.Vector3().copy(twigB.dir).multiplyScalar(twigB.length * f).add(sideDir).add(new THREE.Vector3(0, twigB.baseY + i * 0.012, 0));
        addLeafAtTree(pos, twigB.angle + side * 0.38, d34, d55, 0.30, 0.84, youngLeafMat);
      }
    }
    function updateSaplingStagesTree(progress, elapsed) {
      for (const part of saplingPartsTree) {
        const { mesh, start, peak, end, maxScale, baseScale } = part;
        if (progress <= start || progress >= end) {
          mesh.visible = false;
          continue;
        }
        mesh.visible = true;
        const fadeIn = smoothStepTree((progress - start) / Math.max(0.0001, peak - start));
        const fadeOut = 1 - smoothStepTree((progress - peak) / Math.max(0.0001, end - peak));
        const alpha = Math.max(0, Math.min(1, Math.min(fadeIn, fadeOut)));
        const grow = Math.max(0.001, maxScale * fadeIn);
        mesh.scale.copy(baseScale).multiplyScalar(grow);
        mesh.rotation.y += Math.sin(elapsed * 0.8 + start * 20) * 0.0008;
        if (mesh.material) {
          mesh.material.opacity = 0.12 + alpha * 0.88;
          if (mesh.material.color && mesh.geometry?.type === 'SphereGeometry') {
            mesh.material.color.lerp(treeSeasonState.leafHue, 0.08);
          }
        }
      }
    }

    // Build tree function
    function buildTree() {
      resetTreeRandomStreamTree();
      while (treeGroup.children.length > 0) {
        const child = treeGroup.children[0];
        treeGroup.remove(child);
        if (child.geometry && !child.isInstancedMesh) child.geometry.dispose();
        if (child.material && !Array.isArray(child.material) && !child.isInstancedMesh) child.material.dispose();
      }
      while (saplingGroupTree.children.length > 0) {
        const child = saplingGroupTree.children[0];
        saplingGroupTree.remove(child);
        if (child.geometry) child.geometry.dispose();
        if (child.material && !Array.isArray(child.material)) child.material.dispose();
      }
      branches.length = 0;
      leavesArr.length = 0;
      canopyPatchesArr.length = 0;
      flowersArr.length = 0;
      buildSaplingStagesTree();
      for (let i = 0; i < leafInstancedMeshesTree.length; i++) {
        leafInstancedMeshesTree[i].count = 0;
        treeGroup.add(leafInstancedMeshesTree[i]);
      }
      for (let i = 0; i < flowerInstancedMeshesTree.length; i++) {
        flowerInstancedMeshesTree[i].count = 0;
        treeGroup.add(flowerInstancedMeshesTree[i]);
      }
      flowerCenterIMTree.count = 0;
      treeGroup.add(flowerCenterIMTree);
      const trunkDir = new THREE.Vector3(0.012, 1, 0.004).normalize();
      // Chêne centenaire : charpente plus stable, plus fine, et feuillage abondant
      // qui suit les branches dès leur apparition.
      const rootBranchTree = createBranchTree(new THREE.Vector3(0, 0, 0), trunkDir, 20.5, 1.28, 0, 0);

      // Cime terminale : ajoute de vraies branches au sommet du tronc pour éviter
      // l'effet de cylindre coupé visible au centre de l'arbre adulte.
      const apexBaseTree = rootBranchTree.curve.getPointAt(0.965);
      const apexTangentTree = rootBranchTree.curve.getTangentAt(0.965).normalize();
      const apexDirsTree = [
        new THREE.Vector3(0.10, 1.00, 0.02),
        new THREE.Vector3(0.55, 0.72, 0.30),
        new THREE.Vector3(-0.48, 0.76, -0.22),
        new THREE.Vector3(0.20, 0.82, -0.56),
        new THREE.Vector3(-0.18, 0.92, 0.42),
        new THREE.Vector3(0.38, 0.88, -0.18)
      ];
      for (let ai = 0; ai < apexDirsTree.length; ai++) {
        const d = apexDirsTree[ai].addScaledVector(apexTangentTree, 0.45).normalize();
        const len = 6.4 + ai * 0.9 + treeRandom() * 1.4;
        const r = 0.34 + treeRandom() * 0.10;
        const apexBranch = createBranchTree(apexBaseTree.clone(), d, len, r, 1, Math.max(0.18, rootBranchTree.growthEnd * 0.72));
        apexBranch.parentBranch = rootBranchTree;
        apexBranch.parentStartT = 0.965;
        rootBranchTree.children.push(apexBranch);
      }

      function addBranchTipLeafClusterTree(b, branchIndex, extra = 1.0) {
        // Feuilles de pointe : elles suivent l'extrémité réellement visible de la branche
        // pendant la croissance. Ainsi, chaque branche garde un petit bourgeon/feuillage
        // à son extrémité, au lieu d'avoir des bouts nus jusqu'à la fin.
        const golden = Math.PI * (3 - Math.sqrt(5));
        const count = Math.max(8, Math.round((b.depth <= 1 ? 18 : b.depth === 2 ? 15 : b.depth === 3 ? 11 : 8) * extra));
        for (let ti = 0; ti < count; ti++) {
          const tEnd = 0.965 + seeded01Tree(branchIndex + 43, ti + 9) * 0.028;
          const ringAngle = ti * golden + branchIndex * 1.47;
          const pointGrowth = b.growthStart + (b.growthEnd - b.growthStart) * Math.min(0.72, tEnd);
          createLeafTree(b.curve.getPointAt(tEnd), {
            branch: b,
            branchT: tEnd,
            ringAngle,
            jitterScale: b.depth <= 1 ? 0.62 : 0.72,
            targetScale: (b.depth <= 1 ? 0.78 : b.depth <= 3 ? 0.70 : 0.58) + seeded01Tree(ti + 19, branchIndex) * 0.22,
            seasonSeed: Math.min(0.16, seeded01Tree(branchIndex + 173, ti + 37) * 0.25),
            terminalFollower: true,
            tipPriority: 1,
            growthStart: Math.max(0.006, Math.min(pointGrowth - 0.20 + seeded01Tree(ti + 7, branchIndex) * 0.018, fibonacciGrowthCurveTree(5) + b.depth * 0.009))
          });
        }
      }

      branches.forEach((b, branchIndex) => {
        // Distribution optimisée :
        // - pas de meshes supplémentaires par patch foliaire (meilleures perfs GPU)
        // - feuilles instanciées seulement
        // - chaque branche reçoit une présence terminale garantie
        // - densité répartie le long du dernier tiers / des rameaux, sans rubans continus
        const golden = Math.PI * (3 - Math.sqrt(5));
        const isUpperCrownTree = b.endPos.y > 9.5 || (b.depth <= 1 && b.startPos.y > 10.0);
        const isApexTree = b.parentBranch === rootBranchTree || b.startPos.y > 12.0 || b.endPos.y > 13.0;

        const tMin = b.depth === 0 ? 0.68 :
                     b.depth === 1 ? 0.34 :
                     b.depth === 2 ? 0.22 :
                     b.depth === 3 ? 0.16 : 0.10;
        const tMax = 0.985;

        // Quantité modérée : on couvre mieux, mais on ne surcharge plus le GPU.
        const densityByDepth = b.depth === 0 ? 0.55 :
                               b.depth === 1 ? 0.95 :
                               b.depth === 2 ? 1.15 :
                               b.depth === 3 ? 1.05 : 0.78;
        const baseCount = Math.round((b.length * 1.15 + 8) * densityByDepth);
        const crownBonus = isUpperCrownTree ? 5 : 0;
        const numLeaves = Math.min(
          b.depth <= 1 ? 28 : b.depth === 2 ? 30 : 22,
          Math.max(8, baseCount + crownBonus)
        );

        for (let i = 0; i < numLeaves; i++) {
          const u = (i + 0.5) / numLeaves;
          const jitter = (seeded01Tree(branchIndex + 19, i + 5) - 0.5) * 0.035;
          const t = THREE.MathUtils.clamp(tMin + (tMax - tMin) * u + jitter, tMin, tMax);
          const ringAngle = i * golden + branchIndex * 0.77;

          const scaleByDepth = b.depth === 0 ? 0.30 :
                               b.depth === 1 ? 0.46 :
                               b.depth === 2 ? 0.60 :
                               b.depth === 3 ? 0.66 : 0.54;

          const leafScale = (b.depth <= 1 ? 0.54 :
                             b.depth === 2 ? 0.64 :
                             b.depth === 3 ? 0.58 : 0.48)
                            + seeded01Tree(i + 13, branchIndex) * 0.16;

          const pointGrowth = b.growthStart + (b.growthEnd - b.growthStart) * t;

          createLeafTree(b.curve.getPointAt(t), {
            branch: b,
            branchT: t,
            ringAngle,
            jitterScale: scaleByDepth,
            targetScale: leafScale,
            // répartition saisonnière stable mais équilibrée
            seasonSeed: THREE.MathUtils.clamp(0.10 + seeded01Tree(branchIndex + 17, i + 29) * 0.88, 0, 0.98),
            growthStart: Math.max(
              0.006,
              Math.min(
                pointGrowth - 0.12 + seeded01Tree(i, branchIndex) * 0.025,
                fibonacciGrowthCurveTree(5) + b.depth * 0.012
              )
            )
          });
        }

        // Feuillage terminal garanti : quelques bourgeons/feuilles au bout de chaque
        // branche dès qu'elle existe, pour ne plus avoir de branches totalement nues.
        // Ces feuilles sont prioritaires mais petites en hiver.
        const tipCount = isApexTree ? 9 :
                         b.depth <= 1 ? 7 :
                         b.depth === 2 ? 6 :
                         b.depth === 3 ? 5 : 4;
        for (let ti = 0; ti < tipCount; ti++) {
          const uTip = tipCount === 1 ? 0.5 : ti / (tipCount - 1);
          const tEnd = THREE.MathUtils.clamp(0.90 + uTip * 0.09, 0.88, 0.995);
          const ringAngle = ti * golden + branchIndex * 1.41;
          const pointGrowth = b.growthStart + (b.growthEnd - b.growthStart) * tEnd;

          createLeafTree(b.curve.getPointAt(tEnd), {
            branch: b,
            branchT: tEnd,
            ringAngle,
            jitterScale: isApexTree ? 0.50 : 0.46,
            targetScale: (isApexTree ? 0.62 : 0.52) + seeded01Tree(ti + 31, branchIndex) * 0.12,
            seasonSeed: isApexTree ? 0.025 + seeded01Tree(branchIndex + 71, ti + 5) * 0.12
                                    : 0.06 + seeded01Tree(branchIndex + 71, ti + 5) * 0.18,
            terminalFollower: true,
            tipPriority: isApexTree ? 2 : 1,
            growthStart: Math.max(
              0.006,
              Math.min(
                pointGrowth - 0.18 + seeded01Tree(ti + 7, branchIndex) * 0.02,
                fibonacciGrowthCurveTree(5) + b.depth * 0.009
              )
            )
          });
        }

        if (b.depth >= 3 && treeRandom() > 0.965) {
          createFlowerTree(b.endPos);
        }
      });
    }
    buildTree();
    // UI pour l’arbre : croissance Fibonacci en 365 clics exactement.
    const uiContainerTree = document.createElement('div');
    uiContainerTree.className = 'tree-growth-ui';
    (document.querySelector('.microcosm-ritual-dock .ritual-progress-anchor') || document.body).appendChild(uiContainerTree);

    const growthCardTree = document.createElement('div');
    growthCardTree.className = 'tree-growth-card';
    uiContainerTree.appendChild(growthCardTree);

    const growthTitleTree = document.createElement('div');
    growthTitleTree.className = 'tree-growth-title';
    growthTitleTree.textContent = 'Croissance naturelle · Fibonacci';
    growthCardTree.appendChild(growthTitleTree);

    const growthStatusTree = document.createElement('div');
    growthStatusTree.className = 'tree-growth-status';
    growthCardTree.appendChild(growthStatusTree);

    const progressContainerTree = document.createElement('div');
    progressContainerTree.className = 'tree-progress-track';
    growthCardTree.appendChild(progressContainerTree);

    const progressBarTree = document.createElement('div');
    progressBarTree.className = 'tree-progress-fill';
    progressContainerTree.appendChild(progressBarTree);

    const buttonRowTree = document.createElement('div');
    buttonRowTree.className = 'tree-button-row';
    growthCardTree.appendChild(buttonRowTree);

    let growthSliderTree = null;

    const TREE_CYCLE_MODAL_DISMISSED_KEY = 'microcosm-tree-cycle-modal-dismissed-v1';
    let cycleModalShownThisSession = false;

    function shouldShowCycleModalTree() {
      if (growthClicksTree < FIB_GROWTH_TOTAL_CLICKS_TREE) return false;
      try {
        return localStorage.getItem(TREE_CYCLE_MODAL_DISMISSED_KEY) !== treeGameSeedString;
      } catch (_) {
        return !cycleModalShownThisSession;
      }
    }

    function openCycleModalTree() {
      const modal = document.getElementById('cycle-modal');
      if (!modal || !shouldShowCycleModalTree()) return;
      modal.classList.add('open');
      modal.setAttribute('aria-hidden', 'false');
      cycleModalShownThisSession = true;
    }

    function closeCycleModalTree({ remember = true } = {}) {
      const modal = document.getElementById('cycle-modal');
      if (!modal) return;
      modal.classList.remove('open');
      modal.setAttribute('aria-hidden', 'true');
      if (remember) {
        try { localStorage.setItem(TREE_CYCLE_MODAL_DISMISSED_KEY, treeGameSeedString); } catch (_) {}
      }
    }

    function updateGrowthUITree() {
      const percent = Math.round(targetGrowth * 1000) / 10;
      const stageName = getFibonacciStageNameTree(growthClicksTree);
      const seasonName = seasonModeTree ? ` · ${treeSeasonState.name}` : '';
      growthStatusTree.textContent = `${growthClicksTree}/${FIB_GROWTH_TOTAL_CLICKS_TREE} jours · ${stageName}${seasonName} · ${percent}%`;
      const waterBtn = document.getElementById('water-tree-btn');
      const waterHint = document.getElementById('water-tree-btn-hint');
      const ritualLabel = document.getElementById('save-tree-state-label');
      if (waterBtn) {
        const done = growthClicksTree >= FIB_GROWTH_TOTAL_CLICKS_TREE;
        waterBtn.disabled = done;
        waterBtn.setAttribute('aria-disabled', done ? 'true' : 'false');
        waterBtn.classList.toggle('is-disabled', done);
        waterBtn.firstChild.textContent = done ? 'Arbre accompli' : 'Arroser mon arbre';
      }
      if (waterHint) waterHint.textContent = growthClicksTree >= FIB_GROWTH_TOTAL_CLICKS_TREE
        ? 'Cycle annuel complet'
        : `Jour ${String(growthClicksTree).padStart(3, '0')}/365 · action volontaire`;
      if (ritualLabel) ritualLabel.textContent = `Jour ${String(growthClicksTree).padStart(3, '0')}/365 · ${stageName} · ${percent}%`;
      if (pushButtonTree) {
        pushButtonTree.disabled = growthClicksTree >= FIB_GROWTH_TOTAL_CLICKS_TREE;
        pushButtonTree.textContent = growthClicksTree >= FIB_GROWTH_TOTAL_CLICKS_TREE ? 'Chêne accompli' : 'Arroser mon arbre';
      }
      if (growthClicksTree >= FIB_GROWTH_TOTAL_CLICKS_TREE) {
        requestAnimationFrame(openCycleModalTree);
      }
    }

    function advanceFibonacciGrowthTree(options = {}) {
      if (growthClicksTree >= FIB_GROWTH_TOTAL_CLICKS_TREE) return;
      const syncCamera = options.syncCamera !== false;
      manualGrowthModeTree = false;
      growthClicksTree += 1;
      recordMicrocosmTreeClick(new Date());
      targetGrowth = fibonacciGrowthCurveTree(growthClicksTree);
      isGrowing = growthProgress < targetGrowth;
      if (syncCamera && !userCameraLocked) {
        restoreCameraAutoFollow();
        updateTreeCameraFrame(0.016, true);
      }
      updateGrowthUITree();
    }

    function resetFibonacciGrowthTree() {
      newTreeGameSeedTree();
      try { localStorage.removeItem(TREE_CYCLE_MODAL_DISMISSED_KEY); } catch (_) {}
      closeCycleModalTree({ remember: false });
      newGrassGameSeed();
      renderer.computeAsync(computeInitGrass).catch((err) => console.warn('Impossible de recalculer l’herbe :', err));
      growthClicksTree = 0;
      growthProgress = 0;
      targetGrowth = 0;
      isGrowing = false;
      updateGrassIslandScale();
      buildTree();
      islandRoot.position.set(0, 0, 0);
      islandRoot.rotation.set(0, 0, 0);
      restoreCameraAutoFollow({ clearUserLock: true });
      updateTreeCameraFrame(0.016, true);
      updateGrowthUITree();
    }

    function createUIButtonTree(text, onClick, variant = 'primary') {
      const btn = document.createElement('button');
      btn.textContent = text;
      btn.className = variant === 'secondary' ? 'tree-btn secondary' : 'tree-btn';
      btn.addEventListener('click', (event) => {
        event.stopPropagation();
        onClick();
      });
      buttonRowTree.appendChild(btn);
      return btn;
    }

    let pushButtonTree = null;
    pushButtonTree = createUIButtonTree('Pousser +1 jour', advanceFibonacciGrowthTree);
    createUIButtonTree('Nouvelle graine', resetFibonacciGrowthTree, 'secondary');
    updateGrowthUITree();

    renderer.domElement.addEventListener('click', () => {
      // STABLE PROD : la scène ne compte plus comme “clic/jour”.
      // Seuls les CTA explicites déclenchent advanceFibonacciGrowthTree().
      // Un clic sur le canvas peut seulement sortir du splashscreen et verrouiller la caméra.
      if (openingGrassSplashActive) {
        exitOpeningGrassSplash();
        return;
      }
      disableCameraAutoFollow();
    });
    // Pollen/fireflies particles for tree
    const particleCountTree = 90;
    const particleGeoTree = new THREE.BufferGeometry();
    const particlePositionsTree = new Float32Array(particleCountTree * 3);
    const particleDataTree = [];
    for (let i = 0; i < particleCountTree; i++) {
      const angle = treeRandom() * Math.PI * 2;
      const radius = 4 + treeRandom() * 20;
      particlePositionsTree[i * 3] = Math.cos(angle) * radius;
      particlePositionsTree[i * 3 + 1] = 2 + treeRandom() * 34;
      particlePositionsTree[i * 3 + 2] = Math.sin(angle) * radius;
      particleDataTree.push({ speed: 0.2 + treeRandom() * 0.5, offset: treeRandom() * Math.PI * 2, radius: radius, baseY: particlePositionsTree[i * 3 + 1] });
    }
    particleGeoTree.setAttribute('position', new THREE.BufferAttribute(particlePositionsTree, 3));
    const particleMatTree = new THREE.PointsMaterial({ color: 0xffccd5, size: 0.1, transparent: true, opacity: 0.5, blending: THREE.NormalBlending, depthWrite: false });
    const particlesTree = new THREE.Points(particleGeoTree, particleMatTree);
    islandRoot.add(particlesTree);
    // Timer for falling leaves
    let leafTimerTree = 0;
    let frameCountTree = 0;
    // ───── UI Panel (ocean) construction ─────
    const uiPanel = document.getElementById('ui-panel');
    const uiToggle = document.getElementById('ui-toggle');
    let panelOpen = false;
    uiToggle.addEventListener('click', () => {
      panelOpen = !panelOpen;
      uiPanel.classList.toggle('open', panelOpen);
      uiToggle.classList.toggle('active', panelOpen);
      uiToggle.textContent = panelOpen ? '✕' : '☰';
    });
    function fmtVal(v, step) {
      if (step >= 1) return Math.round(v).toString();
      const d = Math.max(0, -Math.floor(Math.log10(step)));
      return v.toFixed(Math.min(d, 3));
    }
    function buildSlider(label, val, min, max, step, onChange) {
      const row = document.createElement('div');
      row.className = 'ui-row';
      const lbl = document.createElement('span');
      lbl.className = 'ui-label';
      lbl.textContent = label;
      const wrap = document.createElement('div');
      wrap.className = 'ui-slider-wrap';
      const inp = document.createElement('input');
      inp.type = 'range';
      inp.className = 'ui-slider';
      inp.min = min; inp.max = max; inp.step = step; inp.value = val;
      const vDisp = document.createElement('span');
      vDisp.className = 'ui-val';
      vDisp.textContent = fmtVal(val, step);
      inp.addEventListener('input', () => {
        const v = parseFloat(inp.value);
        vDisp.textContent = fmtVal(v, step);
        onChange(v);
      });
      wrap.appendChild(inp);
      wrap.appendChild(vDisp);
      row.appendChild(lbl);
      row.appendChild(wrap);
      return row;
    }
    function buildColor(label, val, onChange) {
      const row = document.createElement('div');
      row.className = 'ui-row';
      const lbl = document.createElement('span');
      lbl.className = 'ui-label';
      lbl.textContent = label;
      const wrap = document.createElement('div');
      wrap.className = 'ui-color-wrap';
      const swatch = document.createElement('div');
      swatch.className = 'ui-color-swatch';
      swatch.style.background = val;
      const inp = document.createElement('input');
      inp.type = 'color';
      inp.className = 'ui-color-input';
      inp.value = val;
      inp.addEventListener('input', () => {
        swatch.style.background = inp.value;
        onChange(inp.value);
      });
      wrap.appendChild(swatch);
      wrap.appendChild(inp);
      row.appendChild(lbl);
      row.appendChild(wrap);
      return row;
    }
    function buildSelect(label, options, selected, onChange) {
      const row = document.createElement('div');
      row.className = 'ui-row';
      const lbl = document.createElement('span');
      lbl.className = 'ui-label';
      lbl.textContent = label;
      const sel = document.createElement('select');
      sel.className = 'ui-select';
      for (const opt of options) {
        const o = document.createElement('option');
        o.value = opt; o.textContent = opt;
        if (opt === selected) o.selected = true;
        sel.appendChild(o);
      }
      sel.addEventListener('change', () => onChange(sel.value));
      row.appendChild(lbl);
      row.appendChild(sel);
      return row;
    }
    function buildToggle(label, checked, onChange) {
      const row = document.createElement('div');
      row.className = 'ui-row';
      const lbl = document.createElement('span');
      lbl.className = 'ui-label';
      lbl.textContent = label;
      const inp = document.createElement('input');
      inp.type = 'checkbox';
      inp.checked = checked;
      inp.style.cursor = 'pointer';
      inp.style.width = '18px';
      inp.style.height = '18px';
      inp.addEventListener('change', () => onChange(inp.checked));
      row.appendChild(lbl);
      row.appendChild(inp);
      return row;
    }
    function buildTextInput(label, placeholder, onCommit, initialValue = '') {
      const row = document.createElement('div');
      row.className = 'ui-row';
      const lbl = document.createElement('span');
      lbl.className = 'ui-label';
      lbl.textContent = label;
      const inp = document.createElement('input');
      inp.type = 'text';
      inp.className = 'ui-text-input';
      inp.placeholder = placeholder;
      inp.value = initialValue || '';
      inp.title = initialValue || placeholder || label;
      inp.addEventListener('keydown', (event) => {
        if (event.key === 'Enter') onCommit(inp.value);
      });
      inp.addEventListener('blur', () => {
        if (inp.value.trim()) onCommit(inp.value);
      });
      row.appendChild(lbl);
      row.appendChild(inp);
      return row;
    }
    function buildButton(label, onClick) {
      const row = document.createElement('div');
      row.className = 'ui-row';
      const btn = document.createElement('button');
      btn.className = 'ui-button';
      btn.textContent = label;
      btn.style.width = '100%';
      btn.addEventListener('click', onClick);
      row.appendChild(btn);
      return row;
    }
    function buildSection(title, items, collapsed = true, extraClass = '') {
      const sec = document.createElement('div');
      sec.className = 'ui-section' + (collapsed ? ' collapsed' : '') + (extraClass ? ' ' + extraClass : '');
      sec.dataset.sectionTitle = title;
      const header = document.createElement('div');
      header.className = 'ui-section-header';
      const tEl = document.createElement('span');
      tEl.className = 'ui-section-title';
      tEl.textContent = title;
      const arrow = document.createElement('span');
      arrow.className = 'ui-section-arrow';
      arrow.textContent = '▼';
      header.appendChild(tEl);
      header.appendChild(arrow);
      const body = document.createElement('div');
      body.className = 'ui-section-body';
      for (const item of items) body.appendChild(item);
      header.addEventListener('click', () => sec.classList.toggle('collapsed'));
      sec.appendChild(header);
      sec.appendChild(body);
      return sec;
    }
    function buildInfoCard(html, pills = []) {
      const card = document.createElement('div');
      card.className = 'ui-info-card';
      card.innerHTML = html;
      if (pills.length) {
        const row = document.createElement('div');
        row.className = 'ui-pill-row';
        for (const pillText of pills) {
          const pill = document.createElement('span');
          pill.className = 'ui-pill';
          pill.textContent = pillText;
          row.appendChild(pill);
        }
        card.appendChild(row);
      }
      return card;
    }
    // Brand header
    const brandOcean = document.createElement('div');
    brandOcean.className = 'ui-brand';
    brandOcean.textContent = 'MICROCOSME';
    uiPanel.appendChild(brandOcean);

    uiPanel.appendChild(buildSection('Microclimat', [
      buildInfoCard(
        '<strong>Source unique.</strong> Vent, air, humidité, nuages, pluie, lumière, océan, herbe et rivage sont synchronisés depuis ce bloc.',
        ['gratuit 365j', 'horloge locale', 'météo live']
      ),
      buildToggle('Auto horloge', microClimate.autoBrowserClock, v => {
        microClimate.autoBrowserClock = v;
        if (v) microClimate.liveWeatherEnabled = false;
      }),
      buildToggle('Météo live', microClimate.liveWeatherEnabled, v => {
        microClimate.liveWeatherEnabled = v;
        microClimate.autoBrowserClock = !v;
        if (v) requestLiveMicroWeather();
        updateWeatherHUD(true);
      }),
      buildSlider('Vent', microClimate.windSpeed, 0, 35, 0.5, v => { setManualMicroClimate('windSpeed', v); }),
      buildSlider('Direction', THREE.MathUtils.radToDeg(microClimate.windDirection), 0, 360, 1, v => { setManualMicroClimate('windDirection', THREE.MathUtils.degToRad(v)); }),
      buildSlider('Température', microClimate.airTemperature, -5, 38, 0.5, v => { setManualMicroClimate('airTemperature', v); }),
      buildSlider('Humidité', microClimate.humidity, 0, 100, 1, v => { setManualMicroClimate('humidity', v); }),
      buildSlider('Nuages', microClimate.cloudCover, 0, 1, 0.01, v => { setManualMicroClimate('cloudCover', v); }),
      buildSlider('Pluie', microClimate.rain, 0, 1, 0.01, v => { setManualMicroClimate('rain', v); })
    ], false, 'product-section'));

    uiPanel.appendChild(buildSection('Eau · rivage', [
      buildInfoCard(
        '<strong>Réglages vraiment visibles.</strong> Ils servent à corriger la fusion eau/terre sans casser l’océan.',
        ['niveau', 'marée', 'fondu', 'mousse']
      ),
      buildSlider('Niveau eau', oceanCycleState.baseLevel, -2.0, 0.4, 0.01, v => { oceanManualOverrides.waterLevel = true; oceanCycleState.baseLevel = v; oceanMesh.position.y = v; }),
      buildToggle('Marée', oceanCycleState.tideEnabled, v => { oceanCycleState.tideEnabled = v; }),
      buildSlider('Force marée', oceanCycleState.tideAmplitude, 0.0, 0.22, 0.005, v => { oceanManualOverrides.tideAmplitude = true; oceanCycleState.manualTideControls = true; oceanCycleState.tideAmplitude = v; }),
      buildSlider('Calme rivage', 0.72, 0.0, 1.0, 0.01, v => { oceanManualOverrides.shoreCalmDistance = true; oceanShoreMinimumWave.value = THREE.MathUtils.lerp(0.12, 0.005, v); }),
      buildSlider('Zone rivage', oceanShoreCalmDistance.value, 3.0, 28.0, 0.5, v => { oceanManualOverrides.shoreCalmDistance = true; oceanShoreCalmDistance.value = v; }),
      buildSlider('Mousse rivage', oceanShoreFoamStrength.value, 0.0, 1.4, 0.01, v => { oceanManualOverrides.shoreFoamStrength = true; oceanShoreFoamStrength.value = v; }),
      buildToggle('Mousse organique', shoreFadeState.enabled, v => {
        oceanManualOverrides.shoreFade = true;
        shoreFadeState.enabled = v;
        if (shoreFadePoints) shoreFadePoints.material.opacity = v ? shoreFadeState.opacity : 0;
      }),
      buildSlider('Densité mousse', shoreFadeState.density, 120, 2200, 10, v => {
        oceanManualOverrides.shoreFade = true;
        shoreFadeState.density = v;
        rebuildShoreOrganicFoamParticles(groundRadiusGrass.value);
      }),
      buildSlider('Largeur mousse', shoreFadeState.width, 0.25, 2.4, 0.05, v => {
        oceanManualOverrides.shoreFade = true;
        shoreFadeState.width = v;
        rebuildShoreOrganicFoamParticles(groundRadiusGrass.value);
      })
    ], false, 'product-section'));

    uiPanel.appendChild(buildSection('Terre · herbe', [
      buildInfoCard(
        `<strong>Herbe dense par défaut.</strong> Le multiplicateur libère réellement plus de brins depuis le pool GPU. Desktop : ×${MICROCOSM_RUNTIME_PERF.grassMaxDensityMultiplier}. Mobile : plafond automatique pour préserver la fluidité.`,
        ['manuel', 'densité visible', `pool ${Math.round(MICROCOSM_RUNTIME_PERF.grassBladeInstances / 1000)}k`]
      ),
      buildSlider('Densité générée ×', grassDensityMultiplier.value, 0.25, MICROCOSM_RUNTIME_PERF.grassMaxDensityMultiplier, 0.25, v => {
        grassDensityManualOverride = true;
        grassDensityMultiplier.value = v;
      }),
      buildSlider('Densité microclimat', grassDensity.value, 0.45, 2.0, 0.01, v => {
        grassDensityManualOverride = true;
        grassDensity.value = v;
      }),
      buildButton('Reprendre densité météo', () => { grassDensityManualOverride = false; grassDensityMultiplier.value = 1.0; }),
      buildSlider('Longueur lame', grassBladeLengthUser, 0.4, 2.0, 0.05, v => { grassBladeLengthUser = v; updateGrassIslandScale(); }),
      buildSlider('Largeur lame', grassBladeWidthUser, 0.5, 2.0, 0.05, v => { grassBladeWidthUser = v; updateGrassIslandScale(); })
    ], false));

    uiPanel.appendChild(buildSection('Développeur · gland jouable', [
      buildInfoCard(
        '<strong>Gland jouable par défaut.</strong> Le corps physique reste invisible, mais le modèle GLB du gland est maintenant calé au contact de la terre. Si le fichier distant est bloqué, un gland procédural local prend le relais.',
        ['gland jouable', 'contact terre', 'GLB/GLTF']
      ),
      buildSlider('Vitesse déplacement', interactiveSphereState.speed, 10, 140, 1, v => { interactiveSphereState.speed = v; }),
      buildSlider('Gravité', interactiveSphereState.gravity, 2, 60, 0.5, v => { interactiveSphereState.gravity = v; }),
      buildSlider('Force saut', interactiveSphereState.jumpForce, 1, 22, 0.5, v => { interactiveSphereState.jumpForce = v; }),
      buildSlider('Amortissement', interactiveSphereState.damping, 0.82, 0.99, 0.005, v => { interactiveSphereState.damping = v; }),
      buildTextInput('URL modèle 3D', 'https://.../modele.glb', value => { loadInteractiveAvatarModel(value); }, DEFAULT_INTERACTIVE_AVATAR_MODEL_URL),
      buildButton('Recharger le gland par défaut', resetInteractiveAvatarToDefault),
      buildButton('Afficher proxy physique blanc', () => clearInteractiveAvatarModel({ showPhysicsProxy: true }))
    ], true, 'dev-wide'));


    uiPanel.appendChild(buildSection('VR · espace réel', [
      buildInfoCard(
        '<strong>18 m² non imposés par défaut.</strong> Cette limite s’active seulement pour une vraie zone parcourable VR.',
        ['desktop libre', 'mobile libre', 'VR optionnel']
      ),
      buildToggle('Limite VR 18m²', vrPlayableState.enabled, v => { vrPlayableState.enabled = v; }),
      buildToggle('Voir zone VR', vrPlayableState.visible, v => {
        vrPlayableState.visible = v;
        vrPlayableRing.visible = v;
      }),
      buildToggle('Masquer HUD en VR', vrRuntimeState.hideHudInVR, v => {
        vrRuntimeState.hideHudInVR = v;
        setMicrocosmHudVisibilityForVR(vrRuntimeState.active);
      }),
      buildSelect('Vue joueur XR', ['3e personne', '1re personne'], vrRuntimeState.viewMode, v => {
        vrRuntimeState.viewMode = v;
        restoreCameraAutoFollow({ clearUserLock: true });
        updateTreeCameraFrame(0.016, true);
      }),
      buildToggle('Confort VR', islandMotionState.vrComfort, v => { islandMotionState.vrComfort = v; }),
      buildSlider('Qualité VR', vrRuntimeState.comfortPixelRatio, 0.75, 1.6, 0.05, v => {
        vrRuntimeState.comfortPixelRatio = v;
        if (vrRuntimeState.active) renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, v));
      }),
      buildToggle('Alléger particules', vrRuntimeState.reduceParticles, v => { vrRuntimeState.reduceParticles = v; }),
      buildToggle('Calmer océan VR', vrRuntimeState.reduceOceanWhileVR, v => { vrRuntimeState.reduceOceanWhileVR = v; })
    ], true, 'product-section'));

    uiPanel.appendChild(buildSection('Offre · cycle annuel', [
      buildInfoCard(
        '<strong>Logique produit.</strong> Gratuit pendant la première année : l’arbre grandit jour après jour. À 365 jours, l’utilisateur choisit la suite.',
        ['free 365j', '4,99€ licence', '19,99€ à vie']
      ),
      buildInfoCard(
        '<strong>Gratuit :</strong> carte HTML autonome partageable.<br><strong>Licence 4,99€ :</strong> 10 entretiens/jour + carte mémoire + mode accueil sans HUD.<br><strong>À vie 19,99€ :</strong> sauvegardes illimitées + partage avancé.'
      )
    ], true, 'product-section'));

    const growthControlRowTree = buildSlider('Courbe de croissance', 0, 0, 100, 0.1, v => { setTreeGrowthByPercent(v); });
    growthSliderTree = growthControlRowTree.querySelector('input[type="range"]');
    uiPanel.appendChild(buildSection('Développeur · croissance', [
      buildInfoCard(
        '<strong>Bloc dev.</strong> À conserver pour tester, mais à cacher aux joueurs non premium dans une version stable.',
        ['debug', 'QA', 'base']
      ),
      buildSelect('Formule test', ['Gratuit', 'Add-on vents & marées', 'À vie', 'QA payant temporaire'], 'Gratuit', setMicrocosmProductAccessPreview),
      growthControlRowTree,
      buildToggle('Saisons FR', seasonModeTree, v => { seasonModeTree = v; updateGrowthUITree(); }),
      buildSlider('Vitesse pousse', growthSpeedTree, 0.01, 0.25, 0.005, v => { growthSpeedTree = v; })
    ], true, 'dev-wide'));

    const noiseNames = { 'Domain Warp': 0, 'Ridged': 1, 'Cellular': 2, 'Billow': 3, 'Swiss': 4, 'Turbulent': 5 };
    const bumpNoiseNames = { 'Perlin': 0, 'Ridged': 1, 'Domain Warp': 2, 'Billow': 3, 'Crosshatch': 4, 'FBM': 5 };

    uiPanel.appendChild(buildSection('Développeur · océan shader', [
      buildInfoCard(
        '<strong>Expert.</strong> Ces paramètres sont utiles pour le rendu, mais ne doivent pas être les réglages principaux joueur.',
        ['shader', 'foam', 'fog', 'color']
      ),
      buildSlider('Houle', params.heightScale, 0.05, 1.7, 0.02, v => { heightScale.value = v; params.heightScale = v; renderer.compute(initWaves); }),
      buildSlider('Chop', params.choppiness, 0, 1.8, 0.02, v => { choppiness.value = v; }),
      buildSlider('Courbure océan', oceanCurveAmount.value, 0.0, 1.0, 0.01, v => { oceanCurveAmount.value = v; }),
      buildSlider('Rayon courbure', Math.abs(oceanPlanetCurveLimit.value), 8.0, 80.0, 0.5, v => { oceanPlanetCurveLimit.value = -Math.abs(v); }),
      buildColor('Water', params.waterColor, v => { waterColor.value.set(v); }),
      buildColor('Sky', params.skyColor, v => skyColor.value.set(v)),
      buildSlider('SSS', params.sssIntensity, 0, 3, 0.01, v => { sssIntensity.value = v; }),
      buildSlider('Rough', params.roughness, 0.01, 0.95, 0.01, v => { roughness_u.value = v; }),
      buildSlider('Fresnel', params.fresnelPower, 0.5, 10, 0.1, v => { fresnelPower.value = v; }),
      buildSlider('Reflect', params.reflectionStrength, 0, 1, 0.01, v => { reflectionStrength.value = v; }),
      buildSlider('Bump', params.bumpStrength, 0, 0.8, 0.005, v => { bumpStrengthU.value = v; }),
      buildSelect('Bump Type', Object.keys(bumpNoiseNames), 'FBM', v => { bumpNoiseType.value = bumpNoiseNames[v]; }),
      buildColor('Foam', params.foamColor, v => foamColorU.value.set(v)),
      buildSlider('Foam seuil', params.foamThreshold, 0, 2, 0.05, v => { foamThreshold.value = v; }),
      buildSlider('Foam intensité', params.foamIntensity, 0, 30, 0.05, v => { foamIntensity.value = v; }),
      buildSelect('Foam noise', Object.keys(noiseNames), 'Turbulent', v => { foamNoiseType.value = noiseNames[v]; }),
      buildSlider('Fog densité', params.fogDensity, 0, 1, 0.01, v => { fogDensity.value = v; }),
      buildSlider('Fog near', params.fogNear, 0, 1000, 10, v => { fogNear.value = v; }),
      buildSlider('Fog far', params.fogFar, 200, 5000, 50, v => { fogFar.value = v; })
    ], true, 'dev-wide'));

    uiPanel.appendChild(buildSection('Développeur · scène', [
      buildToggle('PLY skybox externe', plySkyboxWanted, v => {
        plySkyboxWanted = v;
        applyPlySkyboxVisibility();
      }),
      buildToggle('Fallback jour', skyDome.visible, v => {
        if (plySkybox && plySkybox.visible) return;
        skyDome.visible = v;
      }),
      buildSlider('Soleil hauteur', params.sunElevation, -1.5, 33, 0.1, v => { params.sunElevation = v; updateSun(params.sunElevation, params.sunAzimuth); }),
      buildSlider('Soleil azimut', params.sunAzimuth, 0, 360, 1, v => { params.sunAzimuth = v; updateSun(params.sunElevation, params.sunAzimuth); }),
      buildToggle('Flat floor', flatFloor.visible, v => {
        flatFloor.visible = v;
        flatFloorState.enabled = v;
      }),
      buildColor('Couleur floor', flatFloorState.color, v => {
        flatFloorState.color = v;
        flatFloorMat.color.set(v);
      }),
      buildSlider('Hauteur floor', flatFloorState.y, -30.0, -2.0, 0.1, v => {
        flatFloorState.y = v;
        flatFloor.position.y = v;
      }),
      buildToggle('Îlot vivant sync', islandMotionState.enabled, v => { islandMotionState.enabled = v; }),
      buildSlider('Flottaison', islandMotionState.bobAmplitude, 0.0, 0.18, 0.005, v => { islandMotionState.bobAmplitude = v; }),
      buildSlider('Tangage', islandMotionState.tiltAmplitude, 0.0, 0.05, 0.001, v => { islandMotionState.tiltAmplitude = v; })
    ], true, 'dev-wide'));

    // ───── Mode accueil premium : plein écran sans HUD ─────
    function setMicrocosmKioskMode(active) {
      document.body.classList.toggle('microcosm-kiosk-active', !!active);
      if (!active) document.body.classList.remove('microcosm-kiosk-pending');
    }

    async function requestMicrocosmFullscreenWithoutHud() {
      const target = microcosmMount || document.documentElement;
      document.getElementById('premium-panel')?.classList.remove('open');
      setMicrocosmKioskMode(true);
      try {
        if (target.requestFullscreen) {
          await target.requestFullscreen({ navigationUI: 'hide' });
        } else if (target.webkitRequestFullscreen) {
          target.webkitRequestFullscreen();
        }
      } catch (err) {
        console.warn('Fullscreen sans HUD non disponible, HUD masqué uniquement :', err);
      }
    }

    async function exitMicrocosmFullscreenWithoutHud() {
      setMicrocosmKioskMode(false);
      try {
        if (document.fullscreenElement && document.exitFullscreen) await document.exitFullscreen();
        else if (document.webkitFullscreenElement && document.webkitExitFullscreen) document.webkitExitFullscreen();
      } catch (err) {
        console.warn('Sortie fullscreen impossible :', err);
      }
    }

    document.addEventListener('fullscreenchange', () => {
      if (!document.fullscreenElement) setMicrocosmKioskMode(false);
    });
    document.addEventListener('webkitfullscreenchange', () => {
      if (!document.webkitFullscreenElement) setMicrocosmKioskMode(false);
    });
    window.addEventListener('keydown', (event) => {
      if ((event.key || '').toLowerCase() === 'h' && document.body.classList.contains('microcosm-kiosk-active')) {
        exitMicrocosmFullscreenWithoutHud();
      }
    });

    // ───── Export / Import / Card partageable ─────
    // =====================================================
    // MICROCOSM V8.19 CARD VIEWER FLOW CLEANUP
    // =====================================================
    // Objectif : ne pas ajouter de fonctionnalités lourdes, mais rendre le flow
    // carte mémoire plus premium : viewer flottant, clic/tap pour retourner,
    // HUD fantômes, titres adaptatifs et monétisation douce.

    // =====================================================
    // MICROCOSM V8.15 STABLE PROD — notes développeur
    // =====================================================
    // Stable/prod : l'action principale payante est saveTreeMemoryBundle().
    // Elle génère un seul fichier HTML autonome en mémoire puis ouvre une
    // viewer 3D glass natif. Le téléchargement HTML ne part que si l'utilisateur
    // clique ensuite sur le CTA “Enregistrer”.
    //
    // À conserver : collectMicrocosmState(), buildCollectorCardPayload(),
    // buildTreeMemoryBundle(), buildStandaloneCollectorBundleHtml(),
    // buildTransparentCollectorTextureSvg().
    //
    // Fonctions legacy / compatibilité :
    // - buildStandaloneCollectorCardHtml() : ancien template carte dynamique,
    //   conservé uniquement pour éviter les ruptures si un ancien bouton l'appelle.
    // - createShareCardPng() : fallback visuel si la carte HTML ne peut pas être créée.
    // - saveBundleFiles() : fallback téléchargement unique ; ne doit plus servir à
    //   télécharger plusieurs fichiers à la suite.
    //
    // À ne pas toucher sans demande explicite : croissance Fibonacci, seeds,
    // localStorage, growthClicksTree, growthProgress, targetGrowth.
    function collectMicrocosmState() {
      // EXPORT_HTML_FIX_V8_14 : tierInfo doit être recalculé localement.
      // Avant correction, l'export HTML/JSON appelait tierInfo hors scope,
      // ce qui lançait une ReferenceError et bloquait la génération de carte.
      const tierInfo = resolveCardTier();
      return {
        version: 'microcosm-settings-v3-vr-optimized',
        exportedAt: new Date().toISOString(),
        tree: {
          userSeed: treeUserSeedString,
          gameSeed: treeGameSeedString,
          growthClicks: growthClicksTree,
          growthProgress,
          targetGrowth,
          seasonMode: seasonModeTree
        },
        grass: {
          userSeed: grassUserSeedString,
          gameSeed: grassGameSeedString,
          density: grassDensity.value,
          densityMultiplier: grassDensityMultiplier.value,
          bladeLength: grassBladeLengthUser,
          bladeWidth: grassBladeWidthUser
        },
        microClimate: {
          autoBrowserClock: microClimate.autoBrowserClock,
          liveWeatherEnabled: microClimate.liveWeatherEnabled,
          locationLabel: microClimate.locationLabel,
          latitude: microClimate.latitude,
          longitude: microClimate.longitude,
          windSpeed: microClimate.windSpeed,
          windDirection: microClimate.windDirection,
          windForce: microClimate.windForce,
          airTemperature: microClimate.airTemperature,
          humidity: microClimate.humidity,
          pressure: microClimate.pressure,
          cloudCover: microClimate.cloudCover,
          rain: microClimate.rain
        },
        ocean: {
          waterLevel: oceanCycleState.baseLevel,
          tideEnabled: oceanCycleState.tideEnabled,
          tideAmplitude: oceanCycleState.tideAmplitude,
          manualTideControls: oceanCycleState.manualTideControls,
          manualOverrides: { ...oceanManualOverrides },
          curveAmount: oceanCurveAmount.value,
          sphereRadius: oceanSphereRadiusU.value,
          shoreMinimumWave: oceanShoreMinimumWave.value,
          shoreCalmDistance: oceanShoreCalmDistance.value,
          shoreFoamStrength: oceanShoreFoamStrength.value,
          shoreFade: { ...shoreFadeState }
        },
        activity: getMicrocosmActivitySnapshot(tierInfo.tier === 'eternal-glass' ? MICROCOSM_CONFIG.cards.contributionDays * 8 : MICROCOSM_CONFIG.cards.contributionDays),
        vr: {
          playable18m2: vrPlayableState.enabled,
          showPlayableRing: vrPlayableState.visible
        }
      };
    }

    function downloadBlob(filename, text, type = 'application/json') {
      const blob = new Blob([text], { type });
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      URL.revokeObjectURL(a.href);
      a.remove();
    }


    /* ─────────────────────────────────────────────────────────────
       EXPORT_STABILITY_V8_13 · capture robuste
       Certains navigateurs/WebGPU refusent renderer.domElement.toDataURL()
       selon le contexte graphique ou les ressources chargées. L'export ne doit
       jamais échouer pour autant : on tente d'abord la vraie capture, puis on
       crée une image de secours propre afin que le HTML/SVG/JSON restent exportables.
    ───────────────────────────────────────────────────────────── */
    function createFallbackMicrocosmSnapshotDataUrl(reason = '') {
      const c = document.createElement('canvas');
      c.width = 1280;
      c.height = 720;
      const ctx = c.getContext('2d');
      const W = c.width;
      const H = c.height;
      const g = ctx.createLinearGradient(0, 0, W, H);
      g.addColorStop(0, '#1f170c');
      g.addColorStop(0.48, '#17220e');
      g.addColorStop(1, '#050704');
      ctx.fillStyle = g;
      ctx.fillRect(0, 0, W, H);

      const sky = ctx.createRadialGradient(W * 0.44, H * 0.20, 20, W * 0.44, H * 0.28, W * 0.55);
      sky.addColorStop(0, 'rgba(244,213,138,0.22)');
      sky.addColorStop(0.45, 'rgba(85,107,45,0.18)');
      sky.addColorStop(1, 'rgba(0,0,0,0)');
      ctx.fillStyle = sky;
      ctx.fillRect(0, 0, W, H);

      ctx.save();
      ctx.translate(W * 0.5, H * 0.56);
      ctx.beginPath();
      ctx.ellipse(0, 0, W * 0.30, H * 0.13, 0, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(87, 112, 43, 0.82)';
      ctx.fill();
      ctx.strokeStyle = 'rgba(244,213,138,0.20)';
      ctx.lineWidth = 3;
      ctx.stroke();

      ctx.strokeStyle = 'rgba(255,248,225,0.58)';
      ctx.lineWidth = 6;
      ctx.beginPath();
      ctx.moveTo(0, 0);
      ctx.lineTo(0, -H * 0.22 * Math.max(0.08, growthProgress || 0.08));
      ctx.stroke();
      ctx.restore();

      ctx.fillStyle = 'rgba(255,248,225,0.94)';
      ctx.font = '900 44px Inter, system-ui, sans-serif';
      ctx.fillText('MICROCOSM INTERACTIF 9', 64, 88);
      ctx.fillStyle = 'rgba(244,213,138,0.88)';
      ctx.font = '800 26px Inter, system-ui, sans-serif';
      ctx.fillText('Capture de secours exportable', 64, 128);

      ctx.fillStyle = 'rgba(226,218,188,0.78)';
      ctx.font = '700 22px Inter, system-ui, sans-serif';
      ctx.fillText(`${String(growthClicksTree).padStart(3, '0')}/365 · ${getFibonacciStageNameTree(growthClicksTree)}`, 64, H - 86);
      if (reason) {
        ctx.font = '600 16px Inter, system-ui, sans-serif';
        ctx.fillText(String(reason).slice(0, 110), 64, H - 52);
      }
      return c.toDataURL('image/png');
    }

    function safeCaptureMicrocosmSnapshot() {
      try {
        const data = renderer?.domElement?.toDataURL?.('image/png');
        if (typeof data === 'string' && data.startsWith('data:image/png') && data.length > 256) return data;
        return createFallbackMicrocosmSnapshotDataUrl('capture WebGPU vide ou indisponible');
      } catch (err) {
        console.warn('Capture WebGPU remplacée par une capture de secours :', err);
        return createFallbackMicrocosmSnapshotDataUrl(err?.message || 'capture WebGPU indisponible');
      }
    }

    function exportMicrocosmSettings() {
      const state = collectMicrocosmState();
      const day = String(state.tree.growthClicks).padStart(3, '0');
      downloadBlob(`microcosm-arbre-jour-${day}.json`, JSON.stringify(state, null, 2));
    }

    function applyImportedMicrocosmSettings(state) {
      if (!state || typeof state !== 'object') return;
      if (state.tree) {
        if (Number.isFinite(state.tree.growthClicks)) {
          growthClicksTree = THREE.MathUtils.clamp(Math.round(state.tree.growthClicks), 0, FIB_GROWTH_TOTAL_CLICKS_TREE);
          growthProgress = fibonacciGrowthCurveTree(growthClicksTree);
          targetGrowth = growthProgress;
          isGrowing = false;
        }
        if (typeof state.tree.seasonMode === 'boolean') seasonModeTree = state.tree.seasonMode;
      }
      if (state.grass) {
        if (Number.isFinite(state.grass.density)) {
          grassDensity.value = state.grass.density;
          grassDensityManualOverride = true;
        }
        if (Number.isFinite(state.grass.densityMultiplier)) {
          grassDensityMultiplier.value = THREE.MathUtils.clamp(state.grass.densityMultiplier, 0.25, MICROCOSM_RUNTIME_PERF.grassMaxDensityMultiplier);
          grassDensityManualOverride = true;
        }
        if (Number.isFinite(state.grass.bladeLength)) grassBladeLengthUser = state.grass.bladeLength;
        if (Number.isFinite(state.grass.bladeWidth)) grassBladeWidthUser = state.grass.bladeWidth;
      }
      if (state.microClimate) {
        Object.assign(microClimate, state.microClimate);
        microClimate.liveWeatherEnabled = false;
        microClimate.autoBrowserClock = false;
      }
      if (state.ocean) {
        if (Number.isFinite(state.ocean.waterLevel)) {
          oceanCycleState.baseLevel = state.ocean.waterLevel;
          oceanMesh.position.y = state.ocean.waterLevel;
        }
        if (typeof state.ocean.tideEnabled === 'boolean') oceanCycleState.tideEnabled = state.ocean.tideEnabled;
        if (Number.isFinite(state.ocean.tideAmplitude)) oceanCycleState.tideAmplitude = state.ocean.tideAmplitude;
        if (typeof state.ocean.manualTideControls === 'boolean') oceanCycleState.manualTideControls = state.ocean.manualTideControls;
        if (state.ocean.manualOverrides && typeof state.ocean.manualOverrides === 'object') Object.assign(oceanManualOverrides, state.ocean.manualOverrides);
        if (Number.isFinite(state.ocean.curveAmount)) oceanCurveAmount.value = state.ocean.curveAmount;
        if (Number.isFinite(state.ocean.sphereRadius)) oceanSphereRadiusU.value = state.ocean.sphereRadius;
        if (Number.isFinite(state.ocean.shoreMinimumWave)) oceanShoreMinimumWave.value = state.ocean.shoreMinimumWave;
        if (Number.isFinite(state.ocean.shoreCalmDistance)) oceanShoreCalmDistance.value = state.ocean.shoreCalmDistance;
        if (Number.isFinite(state.ocean.shoreFoamStrength)) oceanShoreFoamStrength.value = state.ocean.shoreFoamStrength;
        if (state.ocean.shoreFade) Object.assign(shoreFadeState, state.ocean.shoreFade);
      }
      if (state.activity && Array.isArray(state.activity.days)) {
        state.activity.days.forEach((item) => {
          if (!item?.date) return;
          microActivityState.daily[item.date] = {
            visits: Number(item.visits || 0),
            clicks: Number(item.clicks || 0),
            places: Array.isArray(item.places) ? item.places : []
          };
        });
        if (Array.isArray(state.activity.locations)) microActivityState.locations = state.activity.locations;
        saveMicroActivityState();
      }
      if (state.vr) {
        if (typeof state.vr.playable18m2 === 'boolean') vrPlayableState.enabled = state.vr.playable18m2;
        if (typeof state.vr.showPlayableRing === 'boolean') {
          vrPlayableState.visible = state.vr.showPlayableRing;
          vrPlayableRing.visible = state.vr.showPlayableRing;
        }
      }
      updateGrassIslandScale();
      rebuildShoreOrganicFoamParticles(groundRadiusGrass.value);
      updateGrowthUITree();
      updateWeatherHUD(true);
      renderer.compute(initWaves);
    }

    function importMicrocosmSettingsFile(file) {
      if (!file) return;
      const reader = new FileReader();
      reader.onload = () => {
        try {
          applyImportedMicrocosmSettings(JSON.parse(reader.result));
        } catch (err) {
          console.warn('Import JSON impossible :', err);
          alert('Import impossible : fichier JSON invalide.');
        }
      };
      reader.readAsText(file);
    }

    function captureCurrentViewPng() {
      try {
        const day = String(growthClicksTree).padStart(3, '0');
        const a = document.createElement('a');
        a.href = renderer.domElement.toDataURL('image/png');
        a.download = `microcosm-arbre-jour-${day}.png`;
        a.click();
      } catch (err) {
        console.warn('Capture impossible :', err);
        alert('Capture impossible dans ce navigateur.');
      }
    }


    function escapeMicrocosmCardText(value) {
      return String(value ?? '')
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
    }

    function safeMicrocosmCardJson(data) {
      return JSON.stringify(data).replace(/</g, '\\u003c');
    }


    function getMicrocosmStorageWarningText() {
      return 'Si tu nettoies ce navigateur ou le stockage local, la mémoire de ton arbre disparaît. Sauvegarde-le pour conserver sa carte, sa texture et son JSON de restauration.';
    }

    function updateStorageWarningText() {
      const node = document.getElementById('storage-warning');
      if (!node) return;
      node.innerHTML = '<strong>Mémoire locale :</strong> ' + escapeMicrocosmCardText(getMicrocosmStorageWarningText()) + ' Utilise <strong>“Sauvegarder mon arbre”</strong>.';
    }

    const MICROCOSM_PRICE_TABLE = {
      EUR: { winds: 4.99, lifetime: 19.99 },
      USD: { winds: 4.99, lifetime: 19.99 },
      GBP: { winds: 4.99, lifetime: 19.99 },
      CHF: { winds: 4.90, lifetime: 19.90 },
      CAD: { winds: 6.99, lifetime: 27.99 },
      AUD: { winds: 7.99, lifetime: 29.99 },
      JPY: { winds: 500, lifetime: 2000 },
      CNY: { winds: 35, lifetime: 149 },
      INR: { winds: 399, lifetime: 1499 },
      BRL: { winds: 24.90, lifetime: 99.90 },
      MAD: { winds: 49, lifetime: 199 }
    };

    function inferCurrencyFromLocale(locale) {
      const lang = String(locale || navigator.language || 'fr-FR').toUpperCase();
      if (lang.includes('-JP')) return 'JPY';
      if (lang.includes('-CN')) return 'CNY';
      if (lang.includes('-IN')) return 'INR';
      if (lang.includes('-BR')) return 'BRL';
      if (lang.includes('-MA')) return 'MAD';
      if (lang.includes('-CA')) return 'CAD';
      if (lang.includes('-AU')) return 'AUD';
      if (lang.includes('-CH')) return 'CHF';
      if (lang.includes('-GB')) return 'GBP';
      if (lang.includes('-US')) return 'USD';
      if (lang.includes('-IE')) return 'EUR';
      if (lang.includes('-BE')) return 'EUR';
      if (lang.includes('-DE')) return 'EUR';
      if (lang.includes('-ES')) return 'EUR';
      if (lang.includes('-IT')) return 'EUR';
      if (lang.includes('-PT')) return 'EUR';
      return 'EUR';
    }

    function formatRoundedPrice(amount, currency, locale = navigator.language || 'fr-FR') {
      return new Intl.NumberFormat(locale, {
        style: 'currency',
        currency,
        maximumFractionDigits: currency === 'JPY' || Number.isInteger(amount) ? 0 : 2,
        minimumFractionDigits: currency === 'JPY' || Number.isInteger(amount) ? 0 : 2
      }).format(amount);
    }

    function resolveRoundedMicrocosmPrices(locale = navigator.language || 'fr-FR') {
      const currency = inferCurrencyFromLocale(locale);
      const base = MICROCOSM_PRICE_TABLE[currency] || MICROCOSM_PRICE_TABLE.EUR;
      return {
        locale,
        currency,
        winds: base.winds,
        lifetime: base.lifetime,
        windsLabel: formatRoundedPrice(base.winds, currency, locale),
        lifetimeLabel: formatRoundedPrice(base.lifetime, currency, locale)
      };
    }

    function syncProductPricingUi() {
      const prices = resolveRoundedMicrocosmPrices();
      const freeLine = document.getElementById('premium-line-free');
      const lifeLine = document.getElementById('premium-line-life');
      const addonLine = document.getElementById('premium-line-addon');
      const addonFeaturesLine = document.getElementById('premium-line-addon-features');
      const cardsLine = document.getElementById('premium-line-cards');
      const cyclePremiumBtn = document.getElementById('cycle-premium-btn');
      if (freeLine) freeLine.textContent = 'Gratuit : 1 clic / jour pendant 365 jours';
      if (lifeLine) lifeLine.textContent = 'À vie : arbre éternel + mémoire illimitée · ' + prices.lifetimeLabel;
      if (addonLine) addonLine.textContent = 'Add-on “vents & marées” : ' + prices.windsLabel + ' sur gratuit ou à vie';
      if (addonFeaturesLine) addonFeaturesLine.textContent = 'Débloque ' + premiumProductState.windsClicksPerDay + ' clics/jour au départ + outils + artefacts + mode accueil sans HUD';
      if (cardsLine) cardsLine.textContent = 'Cartes : 2D originales en gratuit · version glass collector pour les arbres à vie';
      if (cyclePremiumBtn) cyclePremiumBtn.innerHTML = 'Continuer à vie · ' + escapeMicrocosmCardText(prices.lifetimeLabel) + '<br><small>arbre à vie + carte mémoire illimitée</small>';
    }

    function isMicrocosmMemorySaveUnlocked() {
      return !!(premiumProductState.lifetimeUnlocked || premiumProductState.windsUnlocked || premiumProductState.localTestingOverride);
    }

    function syncPaidMemoryUi() {
      const saveBtn = document.getElementById('save-tree-btn');
      const hint = document.getElementById('save-tree-btn-hint');
      const unlocked = isMicrocosmMemorySaveUnlocked();
      // V8.20 : “Voir ma carte” est une action de jeu gratuite.
      // Seul le CTA “Enregistrer” dans le viewer déclenche la sauvegarde payante.
      if (saveBtn) {
        saveBtn.disabled = false;
        saveBtn.setAttribute('aria-disabled', 'false');
        saveBtn.classList.remove('is-disabled');
      }
      if (hint) hint.textContent = unlocked
        ? 'Viewer flottant · Enregistrer disponible'
        : 'Viewer flottant · sauvegarde autonome à déverrouiller';
      syncFloatingMemoryCardActions?.();
    }

    function unlockMicrocosmWindsAddon() {
      premiumProductState.windsUnlocked = true;
      syncPaidMemoryUi();
      syncProductPricingUi();
    }

    function unlockMicrocosmLifetime() {
      premiumProductState.lifetimeUnlocked = true;
      syncPaidMemoryUi();
      syncProductPricingUi();
    }

    function lockMicrocosmToFreeMode() {
      premiumProductState.windsUnlocked = false;
      premiumProductState.lifetimeUnlocked = false;
      premiumProductState.localTestingOverride = false;
      syncPaidMemoryUi();
      syncProductPricingUi();
    }


    function setMicrocosmProductAccessPreview(mode) {
      const normalized = String(mode || 'Gratuit').toLowerCase();
      premiumProductState.windsUnlocked = false;
      premiumProductState.lifetimeUnlocked = false;
      premiumProductState.localTestingOverride = false;
      if (normalized.includes('vents')) {
        premiumProductState.windsUnlocked = true;
      } else if (normalized.includes('vie')) {
        premiumProductState.lifetimeUnlocked = true;
      } else if (normalized.includes('qa')) {
        premiumProductState.localTestingOverride = true;
      }
      syncPaidMemoryUi();
      syncProductPricingUi();
    }

    window.MicrocosmProductAccess = {
      unlockWindsAddon: unlockMicrocosmWindsAddon,
      unlockLifetime: unlockMicrocosmLifetime,
      lockToFree: lockMicrocosmToFreeMode,
      setPreviewMode: setMicrocosmProductAccessPreview,
      sync: syncPaidMemoryUi
    };

    function resolveCardTier() {
      if (premiumProductState.lifetimeUnlocked) {
        return { tier: 'eternal-glass', tierLabel: 'Carte mémoire à vie', badge: 'Lifetime Glass' };
      }
      if (premiumProductState.windsUnlocked) {
        return { tier: 'winds-addon', tierLabel: 'Artefact vents & marées', badge: 'Artifact Edition' };
      }
      return { tier: 'free-2d', tierLabel: 'Carte 2D originale', badge: '2D Originale' };
    }

    function buildCollectorCardPayload() {
      const dayRaw = growthClicksTree;
      const day = String(dayRaw).padStart(3, '0');
      const snapshot = safeCaptureMicrocosmSnapshot();
      const progressPct = Math.round(growthProgress * 100);
      const stage = getFibonacciStageNameTree(growthClicksTree);
      const place = weatherPlaceLabel();
      const windDeg = Math.round((THREE.MathUtils.radToDeg(microClimate.windDirection) + 360) % 360);
      const editionHash = hashGrassSeedString(`${treeUserSeedString}|${treeGameSeedString}|${growthClicksTree}`);
      const edition = (editionHash % 999999).toString().padStart(6, '0');
      const tierInfo = resolveCardTier();
      const prices = resolveRoundedMicrocosmPrices();
      let rarity = progressPct >= 100 ? 'Cycle complet' : progressPct >= 89 ? 'Canopée rare' : progressPct >= 55 ? 'Floraison' : progressPct >= 18 ? 'Jeune pousse' : 'Graine vivante';
      if (tierInfo.tier === 'eternal-glass') rarity = 'Mémoire éternelle';
      if (tierInfo.tier === 'winds-addon') rarity = 'Artefact climatique';
      return {
        schema: 'microcosm-interactif-9.dynamic-card.v2',
        title: 'Microcosm · Carte mémoire',
        edition,
        rarity,
        tier: tierInfo.tier,
        tierLabel: tierInfo.tierLabel,
        tierBadge: tierInfo.badge,
        day: dayRaw,
        dayLabel: `${day}/365`,
        progressPct,
        stage,
        place,
        generatedAt: new Date().toLocaleString('fr-FR', { dateStyle: 'long', timeStyle: 'short' }),
        seedPublic: String(treeUserSeedString || treeGameSeedString || 'microcosm').slice(0, 24),
        temperature: Math.round(microClimate.airTemperature),
        feelsLike: Math.round(microClimate.liveWeatherApparent ?? microClimate.airTemperature),
        humidity: Math.round(microClimate.humidity),
        pressure: Math.round(microClimate.pressure ?? 1013),
        clouds: Math.round((microClimate.cloudCover ?? 0) * 100),
        windSpeed: Math.round(microClimate.windSpeed),
        windDirection: windDeg,
        sync: microClimate.liveWeatherEnabled && microClimate.lastLiveWeatherFetch ? 'LIVE' : 'AUTO',
        cleanupWarning: getMicrocosmStorageWarningText(),
        prices,
        userAlias: 'Gardien local #' + edition,
        authStampUrl: YGGDRASIL_AUTH_STAMP_URL,
        arLaunchUrl: getMicrocosmArLaunchUrl({ edition, seedPublic: String(treeUserSeedString || treeGameSeedString || 'microcosm').slice(0, 24), day: dayRaw }),
        qrCodeUrl: getQrImageUrl(getMicrocosmArLaunchUrl({ edition, seedPublic: String(treeUserSeedString || treeGameSeedString || 'microcosm').slice(0, 24), day: dayRaw })),
        activity: getMicrocosmActivitySnapshot(tierInfo.tier === 'eternal-glass' ? MICROCOSM_CONFIG.cards.contributionDays * 8 : MICROCOSM_CONFIG.cards.contributionDays),
        snapshot,
        note: 'Carte HTML autonome générée depuis Microcosm Interactif 9. Elle embarque une capture, les statistiques de croissance, le microclimat et la mémoire de restauration.'
      };
    }

    function buildMemoryBundleManifest(payload, state) {
      return {
        schema: 'microcosm-interactif-9.tree-memory-bundle.v1',
        exportedAt: new Date().toISOString(),
        edition: payload.edition,
        tier: payload.tier,
        tierLabel: payload.tierLabel,
        day: payload.day,
        place: payload.place,
        files: {
          memoryCardHtml: `${MICROCOSM_CONFIG.cards.memoryCardFilenamePrefix}-${payload.edition}.html`,
          treeStateJson: `microcosm-tree-memory-${payload.edition}.json`,
          texturePng: `microcosm-card-texture-${payload.edition}.png`,
          textureSvg: `microcosm-card-texture-${payload.edition}.svg`,
          manifestJson: `microcosm-tree-bundle-${payload.edition}.json`
        },
        restore: {
          warning: payload.cleanupWarning,
          instructions: [
            'Ouvre à nouveau Microcosm Interactif 9.',
            'Utilise le bouton “Importer JSON”.',
            'Sélectionne le fichier de mémoire exporté pour retrouver l’arbre.',
            'La carte HTML autonome conserve aussi une copie téléchargeable du JSON.'
          ]
        },
        tree: state.tree,
        activity: payload.activity,
        microClimate: state.microClimate
      };
    }

    function loadImageFromDataUrl(dataUrl) {
      return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => resolve(img);
        img.onerror = reject;
        img.src = dataUrl;
      });
    }

    function getPrestigeCardTitle(payload = {}) {
      const p = Number(payload.progressPct || 0);
      if (p >= 100) return 'Chêne centenaire accompli';
      if (p >= 70) return 'Couronne en formation';
      if (p >= 32) return 'Branches en expansion';
      if (p >= 12) return 'Jeune pousse vivante';
      return 'Graine en éveil';
    }

    /* ─────────────────────────────────────────────────────────────
       CARD_RENDERER_V8_11
       Zone configurable : mise en page de la face avant exportée en PNG.
       Objectif : carte sombre, premium, lisible, proche d'un certificat collector.
       - aucune modification de la croissance de l'arbre ;
       - rendu stable en canvas, sans dépendance externe obligatoire ;
       - le QR scannable réel reste disponible en HTML/SVG pour éviter le taint canvas.
    ───────────────────────────────────────────────────────────── */
    async function renderTransparentCollectorTextureCanvas(payload) {
      const img = await loadImageFromDataUrl(payload.snapshot);
      const canvas = document.createElement('canvas');
      canvas.width = 1080;
      canvas.height = 1600;
      const ctx = canvas.getContext('2d');
      const W = canvas.width;
      const H = canvas.height;

      const layout = {
        outerX: 90,
        outerY: 78,
        outerW: 900,
        outerH: 1444,
        innerPad: 18,
        contentX: 170,
        contentW: 740,
        radiusOuter: 62,
        radiusInner: 48
      };

      const isLifetime = payload.tier === 'eternal-glass';
      const isWinds = payload.tier === 'winds-addon';
      const holoOpacity = isLifetime ? 0.18 : isWinds ? 0.13 : 0.08;
      const title = getPrestigeCardTitle(payload);
      const subtitle = payload.rarity || payload.tierLabel || 'Graine vivante';

      function rr(x, y, w, h, r) {
        ctx.beginPath();
        ctx.moveTo(x + r, y);
        ctx.arcTo(x + w, y, x + w, y + h, r);
        ctx.arcTo(x + w, y + h, x, y + h, r);
        ctx.arcTo(x, y + h, x, y, r);
        ctx.arcTo(x, y, x + w, y, r);
        ctx.closePath();
      }

      function objectCoverDraw(image, x, y, w, h) {
        const iw = image.width || 1;
        const ih = image.height || 1;
        const scale = Math.max(w / iw, h / ih);
        const dw = iw * scale;
        const dh = ih * scale;
        const dx = x + (w - dw) * 0.5;
        const dy = y + (h - dh) * 0.5;
        ctx.drawImage(image, dx, dy, dw, dh);
      }

      function fillTextFit(text, x, y, maxWidth, font, minSize = 16) {
        const m = String(font).match(/(\d+)px/);
        const baseSize = m ? parseInt(m[1], 10) : 24;
        let size = baseSize;
        while (size > minSize) {
          ctx.font = font.replace(/\d+px/, size + 'px');
          if (ctx.measureText(String(text)).width <= maxWidth) break;
          size -= 1;
        }
        ctx.fillText(String(text), x, y);
      }

      function drawWrappedTitle(text, x, y, maxWidth, lineHeight, maxLines = 3) {
        const words = String(text).toUpperCase().split(/\s+/).filter(Boolean);
        const lines = [];
        let line = '';
        ctx.font = '950 76px Inter, Arial Black, system-ui, sans-serif';
        for (const word of words) {
          const test = line ? line + ' ' + word : word;
          if (ctx.measureText(test).width > maxWidth && line) {
            lines.push(line);
            line = word;
          } else {
            line = test;
          }
        }
        if (line) lines.push(line);
        const finalLines = lines.slice(0, maxLines);
        if (lines.length > maxLines) finalLines[maxLines - 1] += '…';
        ctx.fillStyle = 'rgba(255,248,225,0.96)';
        ctx.font = '950 76px Inter, Arial Black, system-ui, sans-serif';
        ctx.textBaseline = 'alphabetic';
        finalLines.forEach((l, idx) => ctx.fillText(l, x, y + idx * lineHeight));
        return y + finalLines.length * lineHeight;
      }

      function drawHolo(x, y, w, h, alpha, radius = 44) {
        ctx.save();
        rr(x, y, w, h, radius);
        ctx.clip();
        const grad = ctx.createLinearGradient(x, y, x + w, y + h);
        grad.addColorStop(0.00, `rgba(255,230,150,${alpha})`);
        grad.addColorStop(0.23, `rgba(132,180,80,${alpha * 0.85})`);
        grad.addColorStop(0.48, `rgba(97,205,255,${alpha * 0.72})`);
        grad.addColorStop(0.71, `rgba(255,74,120,${alpha * 0.56})`);
        grad.addColorStop(1.00, `rgba(255,255,255,${alpha * 0.38})`);
        ctx.globalCompositeOperation = 'screen';
        ctx.fillStyle = grad;
        ctx.fillRect(x, y, w, h);
        ctx.globalAlpha = 0.34;
        ctx.strokeStyle = 'rgba(255,255,255,0.14)';
        ctx.lineWidth = 1;
        for (let i = -h; i < w; i += 28) {
          ctx.beginPath();
          ctx.moveTo(x + i, y + h);
          ctx.lineTo(x + i + h, y);
          ctx.stroke();
        }
        ctx.restore();
      }

      function drawPseudoQr(x, y, size) {
        rr(x, y, size, size, 18);
        ctx.fillStyle = 'rgba(255,248,225,0.92)';
        ctx.fill();
        const n = 13;
        const pad = 13;
        const cell = (size - pad * 2) / n;
        function seedBit(i, j) {
          const h = hashGrassSeedString(`${payload.edition}:${i}:${j}`);
          return (h % 100) > 48;
        }
        ctx.fillStyle = '#151207';
        for (let j = 0; j < n; j++) {
          for (let i = 0; i < n; i++) {
            const finder = (i < 4 && j < 4) || (i > n - 5 && j < 4) || (i < 4 && j > n - 5);
            if (finder || seedBit(i, j)) {
              ctx.fillRect(x + pad + i * cell + 1, y + pad + j * cell + 1, Math.max(2, cell - 2), Math.max(2, cell - 2));
            }
          }
        }
      }

      function statBox(label, value, x, y, w, h) {
        rr(x, y, w, h, 22);
        ctx.fillStyle = 'rgba(11,10,5,0.38)';
        ctx.fill();
        ctx.strokeStyle = 'rgba(255,240,184,0.12)';
        ctx.lineWidth = 1.2;
        ctx.stroke();
        ctx.fillStyle = 'rgba(226,218,188,0.58)';
        ctx.font = '800 17px Inter, system-ui, sans-serif';
        ctx.fillText(label.toUpperCase(), x + 20, y + 30);
        ctx.fillStyle = 'rgba(255,248,225,0.96)';
        fillTextFit(value, x + 20, y + 68, w - 40, '950 28px Inter, system-ui, sans-serif', 18);
      }

      ctx.clearRect(0, 0, W, H);

      // Ombre externe et cadre épais type carte prestige.
      ctx.shadowColor = 'rgba(0,0,0,0.62)';
      ctx.shadowBlur = 44;
      ctx.shadowOffsetY = 24;
      rr(layout.outerX, layout.outerY, layout.outerW, layout.outerH, layout.radiusOuter);
      const frame = ctx.createLinearGradient(layout.outerX, layout.outerY, layout.outerX + layout.outerW, layout.outerY + layout.outerH);
      frame.addColorStop(0.00, 'rgba(255,240,184,0.92)');
      frame.addColorStop(0.18, 'rgba(143,102,49,0.88)');
      frame.addColorStop(0.44, 'rgba(42,52,18,0.82)');
      frame.addColorStop(0.70, 'rgba(198,151,80,0.88)');
      frame.addColorStop(1.00, 'rgba(255,248,225,0.90)');
      ctx.fillStyle = frame;
      ctx.fill();
      ctx.shadowBlur = 0;
      ctx.shadowOffsetY = 0;

      const ix = layout.outerX + layout.innerPad;
      const iy = layout.outerY + layout.innerPad;
      const iw = layout.outerW - layout.innerPad * 2;
      const ih = layout.outerH - layout.innerPad * 2;
      rr(ix, iy, iw, ih, layout.radiusInner);
      const inner = ctx.createLinearGradient(ix, iy, ix, iy + ih);
      inner.addColorStop(0.00, 'rgba(66,49,23,0.94)');
      inner.addColorStop(0.28, 'rgba(25,27,11,0.95)');
      inner.addColorStop(0.70, 'rgba(10,16,6,0.96)');
      inner.addColorStop(1.00, 'rgba(7,6,3,0.98)');
      ctx.fillStyle = inner;
      ctx.fill();
      drawHolo(ix, iy, iw, ih, holoOpacity, layout.radiusInner);

      // Double filet intérieur.
      rr(ix + 18, iy + 18, iw - 36, ih - 36, 38);
      ctx.strokeStyle = 'rgba(255,248,225,0.20)';
      ctx.lineWidth = 2;
      ctx.stroke();
      rr(ix + 34, iy + 34, iw - 68, ih - 68, 30);
      ctx.strokeStyle = 'rgba(198,151,80,0.14)';
      ctx.lineWidth = 1;
      ctx.stroke();

      const cx = layout.contentX;
      const cw = layout.contentW;
      ctx.fillStyle = 'rgba(255,248,225,0.70)';
      ctx.font = '900 18px Inter, system-ui, sans-serif';
      ctx.letterSpacing = '3px';
      ctx.fillText('MICROCOSM INTERACTIF 9', cx, 190);
      rr(782, 152, 134, 50, 25);
      ctx.fillStyle = 'rgba(15,10,5,0.44)';
      ctx.fill();
      ctx.strokeStyle = 'rgba(255,240,184,0.18)';
      ctx.stroke();
      ctx.textAlign = 'center';
      ctx.fillStyle = 'rgba(255,248,225,0.90)';
      ctx.font = '900 18px Inter, system-ui, sans-serif';
      ctx.fillText('#' + payload.edition, 849, 184);
      ctx.textAlign = 'left';

      const titleEndY = drawWrappedTitle(title, cx, 285, cw, 78, 3);
      ctx.fillStyle = 'rgba(244,213,138,0.95)';
      ctx.font = '900 21px Inter, system-ui, sans-serif';
      ctx.letterSpacing = '4px';
      ctx.fillText(String(subtitle).toUpperCase(), cx, Math.max(440, titleEndY + 8));

      const imgX = cx;
      const imgY = 488;
      const imgW = cw;
      const imgH = 342;
      rr(imgX, imgY, imgW, imgH, 34);
      ctx.save();
      ctx.clip();
      objectCoverDraw(img, imgX, imgY, imgW, imgH);
      const vignette = ctx.createRadialGradient(imgX + imgW * 0.5, imgY + imgH * 0.38, imgW * 0.15, imgX + imgW * 0.5, imgY + imgH * 0.48, imgW * 0.72);
      vignette.addColorStop(0, 'rgba(255,255,255,0.00)');
      vignette.addColorStop(0.66, 'rgba(0,0,0,0.10)');
      vignette.addColorStop(1, 'rgba(0,0,0,0.58)');
      ctx.fillStyle = vignette;
      ctx.fillRect(imgX, imgY, imgW, imgH);
      ctx.restore();
      drawHolo(imgX, imgY, imgW, imgH, isLifetime ? 0.09 : 0.055, 34);

      // Barre de progression intégrée à la capture.
      const px = imgX + 54;
      const py = imgY + imgH - 88;
      const pw = imgW - 108;
      const ph = 68;
      rr(px, py, pw, ph, 34);
      ctx.fillStyle = 'rgba(10,9,5,0.72)';
      ctx.fill();
      const trackX = px + 34;
      const trackY = py + 27;
      const trackW = pw - 170;
      rr(trackX, trackY, trackW, 16, 99);
      ctx.fillStyle = 'rgba(255,248,225,0.12)';
      ctx.fill();
      rr(trackX, trackY, trackW * THREE.MathUtils.clamp(payload.progressPct / 100, 0, 1), 16, 99);
      const pg = ctx.createLinearGradient(trackX, trackY, trackX + trackW, trackY);
      pg.addColorStop(0, '#7d9638');
      pg.addColorStop(0.55, '#c79d43');
      pg.addColorStop(1, '#fff0a8');
      ctx.fillStyle = pg;
      ctx.fill();
      ctx.fillStyle = '#fff8e1';
      ctx.font = '950 27px Inter, system-ui, sans-serif';
      ctx.textAlign = 'right';
      ctx.fillText(`${payload.progressPct}%`, px + pw - 36, py + 46);
      ctx.textAlign = 'left';

      const sx = cx;
      const sy = 872;
      const sw = 224;
      const sh = 92;
      const gap = 22;
      statBox('Jour', payload.dayLabel || `${String(payload.day).padStart(3, '0')}/365`, sx, sy, sw, sh);
      statBox('Température', `${payload.temperature}°C`, sx + sw + gap, sy, sw, sh);
      statBox('Vent', `${payload.windSpeed} km/h`, sx + (sw + gap) * 2, sy, sw, sh);
      statBox('Humidité', `${payload.humidity}%`, sx, sy + 112, sw, sh);
      statBox('Nuages', `${payload.clouds}%`, sx + sw + gap, sy + 112, sw, sh);
      statBox('Synchro', payload.sync, sx + (sw + gap) * 2, sy + 112, sw, sh);

      // Certificat bas : QR stylisé + identité de graine + lieux.
      const certX = cx;
      const certY = 1246;
      const certW = cw;
      const certH = 176;
      rr(certX, certY, certW, certH, 30);
      ctx.fillStyle = 'rgba(8,7,4,0.58)';
      ctx.fill();
      ctx.strokeStyle = 'rgba(255,240,184,0.16)';
      ctx.lineWidth = 1.3;
      ctx.stroke();
      drawPseudoQr(certX + 28, certY + 28, 104);
      ctx.fillStyle = 'rgba(255,248,225,0.96)';
      ctx.font = '950 25px Inter, system-ui, sans-serif';
      ctx.fillText(`CERTIFICAT DE GRAINE #${payload.edition}`, certX + 154, certY + 55);
      ctx.fillStyle = 'rgba(226,218,188,0.74)';
      ctx.font = '650 18px Inter, system-ui, sans-serif';
      fillTextFit('Croissance Fibonacci · golden ratio · cycle annuel vivant', certX + 154, certY + 90, 548, '650 18px Inter, system-ui, sans-serif', 13);
      const loc = (payload.activity?.locations || []).map(v => v.place).filter(Boolean).slice(0, 3).join(' · ') || payload.place || 'lieu local';
      fillTextFit('Lieux : ' + loc, certX + 154, certY + 122, 548, '650 18px Inter, system-ui, sans-serif', 12);
      ctx.fillStyle = 'rgba(244,213,138,0.86)';
      ctx.font = '800 15px Inter, system-ui, sans-serif';
      ctx.fillText('SCAN AR/XR', certX + 46, certY + 150);

      // Liseré final et shine diagonal.
      ctx.save();
      rr(ix, iy, iw, ih, layout.radiusInner);
      ctx.clip();
      const shine = ctx.createLinearGradient(ix, iy + 220, ix + iw, iy + 940);
      shine.addColorStop(0, 'rgba(255,255,255,0)');
      shine.addColorStop(0.48, 'rgba(255,248,225,0.18)');
      shine.addColorStop(0.54, 'rgba(255,255,255,0)');
      ctx.globalCompositeOperation = 'screen';
      ctx.fillStyle = shine;
      ctx.fillRect(ix, iy, iw, ih);
      ctx.restore();

      return canvas;
    }

    function canvasToBlob(canvas, type = 'image/png') {
      return new Promise((resolve, reject) => {
        canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error('Blob PNG impossible')), type);
      });
    }

    function splitArUrlForSvg(url) {
      const clean = String(url || '').trim();
      if (!clean) return ['', ''];
      const qIndex = clean.indexOf('?');
      if (qIndex > 0) return [clean.slice(0, qIndex + 1), clean.slice(qIndex + 1)];
      const max = 52;
      if (clean.length <= max) return [clean, ''];
      let cut = clean.lastIndexOf('/', max);
      if (cut < 20) cut = max;
      return [clean.slice(0, cut + 1), clean.slice(cut + 1)];
    }

    function buildTransparentCollectorTextureSvg(payload, texturePngDataUrl = '') {
      const qrUrl = payload.qrCodeUrl || getQrImageUrl(payload.arLaunchUrl || getMicrocosmArLaunchUrl(payload));
      const stampUrl = payload.authStampUrl || YGGDRASIL_AUTH_STAMP_URL;
      const arUrlLines = splitArUrlForSvg(payload.arLaunchUrl || '');
      return `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1600" viewBox="0 0 1080 1600" fill="none">
  <title>Microcosm prestige collector texture #${escapeMicrocosmCardText(payload.edition)}</title>
  <desc>Texture SVG alignée sur la carte PNG prestige. Le QR AR/XR et le logo Yggdrasil sont superposés comme reflet holographique masqué par le SVG vectoriel.</desc>
  <defs>
    <linearGradient id="holoSeal" x1="0" y1="0" x2="1080" y2="1600" gradientUnits="userSpaceOnUse">
      <stop offset="0" stop-color="#fff0b8" stop-opacity="0.12"/>
      <stop offset="0.30" stop-color="#85d861" stop-opacity="0.10"/>
      <stop offset="0.58" stop-color="#61cdff" stop-opacity="0.13"/>
      <stop offset="0.78" stop-color="#ff4a78" stop-opacity="0.10"/>
      <stop offset="1" stop-color="#ffffff" stop-opacity="0.08"/>
    </linearGradient>
    <filter id="softGlow"><feGaussianBlur stdDeviation="8" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
    <mask id="yggdrasilHoloMask" maskUnits="userSpaceOnUse" x="90" y="120" width="900" height="1180">
      <rect x="0" y="0" width="1080" height="1600" fill="black"/>
      <image href="${escapeMicrocosmCardText(stampUrl)}" x="120" y="230" width="840" height="840" preserveAspectRatio="xMidYMid meet" opacity="1"/>
    </mask>
    <linearGradient id="holoSweep" x1="90" y1="78" x2="990" y2="1522" gradientUnits="userSpaceOnUse">
      <stop offset="0" stop-color="#fff0b8" stop-opacity="0"/>
      <stop offset="0.22" stop-color="#fff0b8" stop-opacity="0.38"/>
      <stop offset="0.42" stop-color="#85d861" stop-opacity="0.26"/>
      <stop offset="0.60" stop-color="#61cdff" stop-opacity="0.34"/>
      <stop offset="0.78" stop-color="#ff4a78" stop-opacity="0.24"/>
      <stop offset="1" stop-color="#ffffff" stop-opacity="0"/>
    </linearGradient>
  </defs>
  ${texturePngDataUrl ? `<image href="${texturePngDataUrl}" x="0" y="0" width="1080" height="1600" preserveAspectRatio="none"/>` : ''}
  <g opacity="0.42" style="mix-blend-mode:screen" filter="url(#softGlow)">
    <rect x="90" y="120" width="900" height="1180" rx="62" fill="url(#holoSweep)" mask="url(#yggdrasilHoloMask)"/>
  </g>
  <g opacity="0.12" style="mix-blend-mode:screen">
    <rect x="90" y="78" width="900" height="1444" rx="62" fill="url(#holoSeal)"/>
  </g>
  <g opacity="0.98">
    <rect x="202" y="1274" width="104" height="104" rx="18" fill="rgba(255,248,225,0.92)"/>
    <image href="${escapeMicrocosmCardText(qrUrl)}" x="212" y="1284" width="84" height="84" preserveAspectRatio="xMidYMid meet"/>
    <text x="254" y="1418" text-anchor="middle" fill="#f4d58a" font-family="Inter, Arial, sans-serif" font-size="13" font-weight="900" letter-spacing="1.8">SCAN AR/XR</text>
  </g>
  <text x="324" y="1346" fill="rgba(226,218,188,0.78)" font-family="Inter, Arial, sans-serif" font-size="16" font-weight="650">
    <tspan x="324" dy="0">${escapeMicrocosmCardText(arUrlLines[0])}</tspan>
    <tspan x="324" dy="24">${escapeMicrocosmCardText(arUrlLines[1])}</tspan>
  </text>
</svg>`;
    }

    function buildStandaloneCollectorBundleHtml(bundle) {
      const json = safeMicrocosmCardJson(bundle);
      return `<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Microcosm · Carte mémoire #${escapeMicrocosmCardText(bundle.cardData.edition)}</title>
  <style>
    :root {
      --cream: #fff8e1;
      --cream-soft: rgba(255,248,225,.78);
      --gold: #d6a85a;
      --dark: #080704;
      --rx: -4deg;
      --ry: 5deg;
      --mx: 56%;
      --my: 34%;
      --logo-x: 50%;
      --logo-y: 50%;
      --holo: .18;
      --holo-tilt: 0deg;
      --face-radius: 34px;
      --face-fill:
        radial-gradient(circle at 20% 0%, rgba(255,240,184,.12), transparent 38%),
        radial-gradient(circle at 72% 30%, rgba(85,107,45,.12), transparent 34%),
        linear-gradient(180deg, rgba(38,29,15,.98) 0%, rgba(14,20,9,.985) 52%, rgba(6,5,3,.99) 100%);
      --yggdrasil-mask: url('${escapeMicrocosmCardText(YGGDRASIL_AUTH_STAMP_URL)}');
    }
    * { box-sizing: border-box; }
    html, body {
      min-height: 100%;
      margin: 0;
      font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      color: var(--cream);
      background:
        radial-gradient(circle at 22% 0%, rgba(214,168,90,.14), transparent 38%),
        radial-gradient(circle at 76% 22%, rgba(97,205,255,.08), transparent 30%),
        linear-gradient(180deg, #100d07 0%, #050503 100%);
    }
    .page {
      min-height: 100vh;
      display: grid;
      grid-template-columns: minmax(320px, 620px) minmax(290px, 440px);
      gap: 46px;
      align-items: center;
      justify-content: center;
      padding: 34px;
    }
    .card-stage { perspective: 1700px; }
    .micro-card {
      width: min(100%, 560px);
      aspect-ratio: 1080 / 1600;
      position: relative;
      transform-style: preserve-3d;
      transform: rotateX(var(--rx)) rotateY(var(--ry));
      transition: transform .22s ease;
      border-radius: var(--face-radius);
      box-shadow: 0 42px 90px rgba(0,0,0,.48);
      contain: layout paint;
    }
    .micro-card.is-flipped { transform: rotateX(var(--rx)) rotateY(calc(180deg + var(--ry))); }
    .card-face {
      position: absolute;
      inset: 0;
      overflow: hidden;
      border-radius: var(--face-radius);
      backface-visibility: hidden;
      background: var(--face-fill);
      border: 1px solid rgba(255,248,225,.16);
      box-shadow:
        inset 0 0 0 1px rgba(214,168,90,.22),
        inset 0 0 0 10px rgba(255,248,225,.026),
        inset 0 0 42px rgba(0,0,0,.32);
      isolation: isolate;
    }
    .card-back { transform: rotateY(180deg); }
    .card-face::before,
    .card-face::after,
    .ygg-holo-sweep {
      content: '';
      position: absolute;
      inset: 0;
      pointer-events: none;
    }
    @keyframes microHoloDrift {
      0% { background-position: 0% 0%, var(--mx) var(--my); transform: translate3d(-1.2%, -.7%, 0) rotate(calc(var(--holo-tilt) * .28)); }
      50% { background-position: 100% 55%, var(--mx) var(--my); transform: translate3d(1.2%, .7%, 0) rotate(var(--holo-tilt)); }
      100% { background-position: 0% 0%, var(--mx) var(--my); transform: translate3d(-1.2%, -.7%, 0) rotate(calc(var(--holo-tilt) * .28)); }
    }
    .card-face::before {
      inset: -18%;
      z-index: 7;
      background:
        linear-gradient(112deg, transparent 10%, rgba(255,255,255,.045) 32%, rgba(255,248,225,.24) 47%, rgba(97,205,255,.13) 54%, transparent 72%),
        radial-gradient(circle at var(--mx) var(--my), rgba(255,255,255,.18), transparent 34%);
      background-size: 220% 220%, 100% 100%;
      mix-blend-mode: screen;
      opacity: .62;
      animation: microHoloDrift 7.5s ease-in-out infinite;
    }
    .card-face::after,
    .ygg-holo-sweep {
      z-index: 6;
      background:
        repeating-linear-gradient(128deg,
          rgba(255,240,184,.00) 0%,
          rgba(255,240,184,calc(var(--holo) * .88)) 9%,
          rgba(133,216,97,calc(var(--holo) * .68)) 17%,
          rgba(97,205,255,calc(var(--holo) * .78)) 27%,
          rgba(255,74,120,calc(var(--holo) * .64)) 38%,
          rgba(255,240,184,.00) 52%),
        radial-gradient(circle at var(--mx) var(--my), rgba(255,255,255,.42), transparent 42%);
      background-size: 260% 260%, 100% 100%;
      background-position: var(--mx) var(--my), var(--mx) var(--my);
      -webkit-mask-image: var(--yggdrasil-mask);
      mask-image: var(--yggdrasil-mask);
      -webkit-mask-size: 86% auto;
      mask-size: 86% auto;
      -webkit-mask-position: var(--logo-x) var(--logo-y);
      mask-position: var(--logo-x) var(--logo-y);
      -webkit-mask-repeat: no-repeat;
      mask-repeat: no-repeat;
      mix-blend-mode: plus-lighter;
      opacity: .72;
      filter: saturate(2) brightness(1.25) drop-shadow(0 0 28px rgba(255,248,225,.12));
      animation: microHoloDrift 9.5s ease-in-out infinite reverse;
    }
    .ygg-holo-sweep {
      z-index: 8;
      opacity: .40;
      -webkit-mask-size: 108% auto;
      mask-size: 108% auto;
      filter: saturate(2.4) brightness(1.3) drop-shadow(0 0 32px rgba(244,213,138,.12));
    }
    .prestige-inner {
      position: relative;
      z-index: 2;
      height: 100%;
      padding: 38px 36px 34px;
      border-radius: inherit;
      background: transparent;
      min-height: 0;
    }
    .card-front .prestige-inner,
    .card-back .prestige-inner {
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }
    .card-frame-line,
    .card-frame-line::before {
      position: absolute;
      inset: 26px;
      content: '';
      border-radius: 26px;
      border: 1px solid rgba(255,240,184,.22);
      pointer-events: none;
      z-index: 3;
    }
    .card-frame-line::before { inset: 10px; border-color: rgba(85,107,45,.22); }
    .top-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; flex: 0 0 auto; }
    .kicker { color: rgba(255,248,225,.70); font-size: 10px; font-weight: 950; letter-spacing: .22em; text-transform: uppercase; }
    .edition-pill { padding: 7px 12px; border-radius: 999px; background: rgba(5,5,3,.42); border: 1px solid rgba(255,248,225,.12); color: rgba(255,248,225,.92); font-size: 10px; font-weight: 950; letter-spacing: .12em; white-space: nowrap; }
    .front-title, .back-title { color: var(--cream); font-weight: 950; text-transform: uppercase; letter-spacing: .045em; line-height: .92; }
    .front-title { margin: 30px 0 8px; font-size: clamp(38px, 6.4vw, 62px); max-width: 94%; flex: 0 0 auto; }
    .back-title { margin: 18px 0 10px; font-size: clamp(38px, 6vw, 58px); max-width: 100%; }
    .subline { color: #f4d58a; font-size: 11px; font-weight: 950; letter-spacing: .22em; text-transform: uppercase; margin-bottom: 14px; flex: 0 0 auto; }
    .photo-window { position: relative; height: 210px; flex: 0 0 210px; border-radius: 24px; overflow: hidden; border: 1px solid rgba(255,240,184,.16); background: rgba(255,255,255,.04); box-shadow: inset 0 1px 0 rgba(255,255,255,.10); }
    .photo-window img { display: block; width: 100%; height: 100%; object-fit: cover; }
    .photo-window::after { content: ''; position: absolute; inset: 0; background: radial-gradient(circle at center, transparent 0%, rgba(0,0,0,.12) 58%, rgba(0,0,0,.58) 100%); pointer-events: none; }
    .progress-shell { position: absolute; left: 30px; right: 30px; bottom: 20px; z-index: 2; display: grid; grid-template-columns: 1fr auto; gap: 14px; align-items: center; padding: 11px 15px; border-radius: 999px; background: rgba(8,7,4,.72); border: 1px solid rgba(255,248,225,.10); backdrop-filter: blur(10px); }
    .progress-track { height: 11px; border-radius: 999px; background: rgba(255,248,225,.12); overflow: hidden; }
    .progress-fill { height: 100%; width: 0%; border-radius: inherit; background: linear-gradient(90deg, #6e8f2e, #d6a85a, #fff0b8); box-shadow: 0 0 18px rgba(244,213,138,.42); }
    .progress-num { font-weight: 950; font-size: 15px; color: var(--cream); }
    .stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 16px; flex: 0 0 auto; }
    .stat, .id-line, .meta { padding: 10px 12px; border-radius: 16px; background: rgba(7,6,3,.46); border: 1px solid rgba(255,248,225,.10); box-shadow: inset 0 1px 0 rgba(255,255,255,.07); min-width: 0; }
    .stat small, .id-line small, .meta small { display: block; color: rgba(255,248,225,.50); font-size: 8px; letter-spacing: .13em; text-transform: uppercase; font-weight: 950; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .stat b, .id-line b, .meta b { display: block; margin-top: 5px; color: var(--cream); font-size: 14px; line-height: 1.16; font-weight: 950; overflow-wrap: anywhere; }
    .certificate-band, .auth-band { display: grid; grid-template-columns: 68px 1fr; gap: 12px; align-items: center; margin-top: 16px; padding: 10px; border-radius: 20px; background: rgba(7,6,3,.52); border: 1px solid rgba(255,248,225,.12); flex: 0 0 auto; min-height: 92px; }
    .qr-card-small, .qr-card { background: rgba(255,248,225,.94); border-radius: 16px; padding: 8px; box-shadow: 0 18px 34px rgba(0,0,0,.30); align-self: start; }
    .qr-card-small img, .qr-card img { display: block; width: 100%; aspect-ratio: 1; object-fit: contain; border-radius: 8px; }
    .qr-card-small span, .qr-card span { display: block; margin-top: 6px; text-align: center; color: #151207; font-size: 7px; font-weight: 950; letter-spacing: .12em; text-transform: uppercase; }
    .certificate-band strong, .auth-band strong { display: block; color: var(--cream); font-size: 13px; font-weight: 950; letter-spacing: .10em; text-transform: uppercase; }
    .certificate-band span, .auth-band span, .ar-url { display: block; margin-top: 5px; color: rgba(226,218,188,.70); font-size: 10px; line-height: 1.34; }
    .card-back .prestige-inner { padding-top: 38px; }
    .back-grid { display: grid; grid-template-columns: 1fr 130px; gap: 16px; align-items: start; flex: 0 0 auto; }
    .qr-card { padding: 9px; border-radius: 22px; }
    .qr-card span { font-size: 7px; }
    .back-copy { color: rgba(226,218,188,.78); font-size: 12px; line-height: 1.35; font-weight: 650; max-width: 100%; margin: 0 0 12px; flex: 0 0 auto; }
    .id-lines { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 10px; flex: 0 0 auto; }
    .activity-panel { margin-top: 12px; padding: 11px; border-radius: 20px; background: rgba(7,6,3,.46); border: 1px solid rgba(255,248,225,.12); min-width: 0; flex: 0 0 auto; }
    .activity-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 0 0 9px; }
    .activity-head h2 { margin: 0; font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: rgba(255,248,225,.88); }
    .activity-year-controls { display: none; align-items: center; gap: 6px; color: rgba(255,248,225,.78); font-weight: 950; font-size: 10px; }
    body[data-tier="eternal-glass"] .activity-year-controls { display: inline-flex; }
    .activity-year-controls button { width: 22px; height: 22px; padding: 0; display: grid; place-items: center; border-radius: 999px; font-size: 12px; line-height: 1; }
    .activity-grid { display: grid; grid-template-columns: repeat(53, 6px); grid-auto-rows: 6px; gap: 2px; overflow: hidden; padding-bottom: 0; max-width: 100%; }
    .activity-cell { width: 6px; height: 6px; border-radius: 2px; background: rgba(255,248,225,.08); }
    .activity-cell.l1 { background: rgba(85,107,45,.48); }
    .activity-cell.l2 { background: rgba(95,145,54,.68); }
    .activity-cell.l3 { background: rgba(128,180,74,.84); }
    .activity-cell.l4 { background: rgba(180,213,112,.96); }
    .auth-band { margin-top: 12px; grid-template-columns: 58px 1fr; min-height: 80px; }
    .auth-stamp { width: 58px; height: 58px; border-radius: 50%; background: linear-gradient(135deg, rgba(255,240,184,.84), rgba(133,216,97,.52), rgba(97,205,255,.66), rgba(255,74,120,.42)); -webkit-mask-image: var(--yggdrasil-mask); mask-image: var(--yggdrasil-mask); -webkit-mask-size: contain; mask-size: contain; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; -webkit-mask-position: center; mask-position: center; mix-blend-mode: plus-lighter; filter: drop-shadow(0 0 18px rgba(244,213,138,.18)); }
    .ar-url { color: rgba(244,213,138,.88); word-break: break-word; font-size: 10px; }
    .ar-url span { display: block; overflow-wrap: anywhere; }
    .memory-action-panel { display: grid; gap: 12px; margin-top: 18px; }
    .action-group { display: grid; gap: 8px; padding: 10px; border-radius: 20px; background: rgba(7,6,3,.28); border: 1px solid rgba(255,248,225,.09); }
    .action-group-title { color: rgba(226,218,188,.62); font-size: 9px; font-weight: 950; letter-spacing: .16em; text-transform: uppercase; }
    .action-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
    button { appearance: none; min-height: 44px; border: 1px solid rgba(244,213,138,.18); border-radius: 16px; background: linear-gradient(145deg, rgba(79,110,38,.58), rgba(42,26,18,.48)); color: var(--cream); padding: 11px 14px; font-weight: 850; cursor: pointer; text-align: center; line-height: 1.15; }
    button.primary { min-height: 50px; border-color: rgba(244,213,138,.34); background: radial-gradient(circle at 14% 0%, rgba(244,213,138,.20), transparent 48%), linear-gradient(145deg, rgba(79,110,38,.72), rgba(42,26,18,.58)); }
    button.secondary { background: linear-gradient(145deg, rgba(27,20,12,.64), rgba(52,39,21,.48)); }
    .memory-note { margin: 4px 2px 0; color: rgba(226,218,188,.58); font-size: 11px; line-height: 1.4; }
    .side-panel { padding: 22px; border-radius: 28px; background: linear-gradient(145deg, rgba(27,20,12,.74), rgba(52,39,21,.50)); border: 1px solid rgba(244,213,138,.16); box-shadow: 0 26px 52px rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.08); }
    .side-panel h2 { margin: 0 0 10px; font-size: 28px; line-height: 1.08; }
    .side-panel p, .warning { color: rgba(226,218,188,.76); line-height: 1.55; font-size: 14px; }
    .meta-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 16px; }
    .warning { margin-top: 14px; padding: 14px; border-radius: 18px; background: rgba(7,6,3,.38); border: 1px solid rgba(255,248,225,.10); }
    @media (max-width: 1040px) { .page { grid-template-columns: 1fr; gap: 20px; padding: 16px; } .micro-card { margin: 0 auto; } .side-panel { max-width: 620px; margin: 0 auto; } }
    @media (max-width: 620px) { .page { padding: 10px; } .micro-card { width: min(100%, 430px); } .prestige-inner { padding: 28px 22px 24px; } .front-title, .back-title { font-size: 32px; } .photo-window { height: 168px; flex-basis: 168px; } .back-grid { grid-template-columns: 1fr 104px; gap: 10px; } .qr-card { width: 104px; } .id-lines, .meta-grid, .stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; } .activity-grid { grid-template-columns: repeat(53, 5px); grid-auto-rows: 5px; gap: 1.5px; } .activity-cell { width: 5px; height: 5px; } .action-row { grid-template-columns: 1fr; } button { width: 100%; } .side-panel { padding: 16px; border-radius: 22px; } }
    @media (max-width: 420px) { .prestige-inner { padding: 24px 18px 20px; } .front-title, .back-title { font-size: 28px; } .photo-window { height: 146px; flex-basis: 146px; } .stat, .id-line, .meta { padding: 8px; border-radius: 12px; } .stat b, .id-line b, .meta b { font-size: 12px; } .certificate-band { grid-template-columns: 58px 1fr; min-height: 76px; } .qr-card-small { padding: 6px; border-radius: 12px; } .back-grid { grid-template-columns: 1fr 92px; } .qr-card { width: 92px; padding: 6px; border-radius: 16px; } .activity-grid { grid-template-columns: repeat(53, 4px); grid-auto-rows: 4px; gap: 1px; } .activity-cell { width: 4px; height: 4px; } }
    @media (max-height: 760px) and (min-width: 1041px) { .page { align-items: start; } .micro-card { width: min(48vw, 480px); } .side-panel { align-self: start; max-height: calc(100dvh - 40px); overflow: auto; } }

    /* CARD_LAYOUT_RECOVERY_V8_13 · verrouillage anti-décalage des cartes exportées */
    .card-front .certificate-band {
      margin-top: auto;
      margin-bottom: 0;
    }
    .card-back .auth-band {
      margin-top: auto;
      margin-bottom: 0;
    }
    .card-back .prestige-inner {
      gap: 10px;
    }
    .activity-panel {
      overflow: hidden !important;
    }
    .activity-grid {
      height: 54px !important;
      max-height: 54px !important;
      overflow: hidden !important;
      align-content: start;
    }
    .activity-cell {
      flex: 0 0 auto;
    }
    @media (min-width: 1041px) and (max-height: 900px) {
      .micro-card {
        width: min(100%, 500px);
      }
      .page {
        align-items: center;
      }
    }
  </style>
</head>
<body data-tier="${escapeMicrocosmCardText(bundle.cardData.tier)}">
  <main class="page">
    <section class="card-stage">
      <article class="micro-card" id="micro-card">
        <section class="card-face card-front">
          <div class="card-frame-line" aria-hidden="true"></div>
          <div class="ygg-holo-sweep" aria-hidden="true"></div>
          <div class="prestige-inner">
            <div class="top-row">
              <div class="kicker">Microcosm Interactif 9</div>
              <div class="edition-pill" id="front-edition">#------</div>
            </div>
            <h1 class="front-title" id="front-title">Arbre vivant</h1>
            <div class="subline" id="front-subline">Graine vivante</div>
            <div class="photo-window">
              <img id="world-shot" alt="Capture du microcosme">
              <div class="progress-shell">
                <div class="progress-track"><div class="progress-fill" id="progress-fill"></div></div>
                <div class="progress-num" id="progress-num">0%</div>
              </div>
            </div>
            <div class="stats-grid" id="stats-grid"></div>
            <div class="certificate-band">
              <div class="qr-card-small"><img id="front-qr" alt="QR code AR/XR"><span>Scan AR/XR</span></div>
              <div><strong id="front-certificate">Certificat de graine</strong><span id="front-certificate-copy"></span></div>
            </div>
          </div>
        </section>
        <section class="card-face card-back">
          <div class="card-frame-line" aria-hidden="true"></div>
          <div class="ygg-holo-sweep" aria-hidden="true"></div>
          <div class="prestige-inner">
            <div class="back-grid">
              <div>
                <div class="kicker">Certificat holographique</div>
                <h1 class="back-title" id="back-title">Carte mémoire</h1>
              </div>
              <div class="qr-card"><img id="qr-code" alt="QR code AR / XR"><span>Scan AR / XR</span></div>
            </div>
            <p class="back-copy" id="back-copy"></p>
            <div class="id-lines" id="id-lines"></div>
            <div class="activity-panel">
              <div class="activity-head">
                <h2>Activité annuelle</h2>
                <div class="activity-year-controls" id="activity-year-controls">
                  <button type="button" id="activity-prev-year" aria-label="Année précédente">‹</button>
                  <span id="activity-year-label"></span>
                  <button type="button" id="activity-next-year" aria-label="Année suivante">›</button>
                </div>
              </div>
              <div class="activity-grid" id="activity-grid"></div>
            </div>
            <div class="auth-band"><div class="auth-stamp" aria-hidden="true"></div><div><strong>Authenticité Microcosm · #<span id="edition-inline"></span></strong><span>Le logo Yggdrasil est inclus comme masque vectoriel dans le reflet holographique dynamique, sur le recto et sur le verso.</span><span class="ar-url" id="ar-url"></span></div></div>
          </div>
        </section>
      </article>
    </section>
    <aside class="side-panel">
      <div class="side-kicker">Carte mémoire autonome</div>
      <h2>Actions de la carte</h2>
      <p>Cette page embarque toute la mémoire de l’arbre. Les exports se font ici, un clic à la fois, pour éviter tout blocage du navigateur.</p>
      <div class="memory-action-panel" aria-label="Actions disponibles sur la carte mémoire">
        <div class="action-group">
          <div class="action-group-title">Consulter</div>
          <div class="action-row">
            <button type="button" class="primary" id="flip-card">Recto / verso</button>
            <button type="button" id="open-ar-xr">Ouvrir AR/XR</button>
          </div>
        </div>
        <div class="action-group">
          <div class="action-group-title">Restaurer</div>
          <div class="action-row">
            <button type="button" class="secondary" id="copy-tree-json">Copier JSON</button>
            <button type="button" id="download-tree-json">Télécharger JSON</button>
          </div>
        </div>
        <div class="action-group">
          <div class="action-group-title">Textures & manifeste</div>
          <div class="action-row">
            <button type="button" class="secondary" id="download-texture-png">PNG</button>
            <button type="button" class="secondary" id="download-texture-svg">SVG</button>
          </div>
          <button type="button" class="secondary" id="download-manifest">Télécharger le manifeste</button>
        </div>
        <p class="memory-note">Aucune action secondaire ne se lance automatiquement : chaque export demande un clic utilisateur distinct.</p>
      </div>
      <div class="meta-grid" id="meta-grid"></div>
      <div class="warning" id="cleanup-warning"></div>
    </aside>
  </main>
  <script>
    const BUNDLE = ${json};
    const CARD_DATA = BUNDLE.cardData;
    const TREE_STATE = BUNDLE.treeState;
    const MANIFEST = BUNDLE.manifest;
    const $ = (selector) => document.querySelector(selector);
    const card = $('#micro-card');
    const clamp = (v, min, max) => Math.min(max, Math.max(min, v));

    function getPrestigeCardTitle(data) {
      if (data.progressPct >= 100) return 'Chêne centenaire accompli';
      if (data.progressPct >= 70) return 'Couronne en formation';
      if (data.progressPct >= 32) return 'Branches en expansion';
      if (data.progressPct >= 12) return 'Jeune pousse vivante';
      return 'Graine en éveil';
    }
    function meta(label, value) { const item = document.createElement('div'); item.className = 'meta'; item.innerHTML = '<small></small><b></b>'; item.querySelector('small').textContent = label; item.querySelector('b').textContent = value; return item; }
    function idLine(label, value) { const item = document.createElement('div'); item.className = 'id-line'; item.innerHTML = '<small></small><b></b>'; item.querySelector('small').textContent = label; item.querySelector('b').textContent = value; return item; }
    function stat(label, value) { const item = document.createElement('div'); item.className = 'stat'; item.innerHTML = '<small></small><b></b>'; item.querySelector('small').textContent = label; item.querySelector('b').textContent = value; return item; }
    function downloadBlob(name, blob, type) { const finalBlob = blob instanceof Blob ? blob : new Blob([blob], { type: type || 'application/octet-stream' }); const href = URL.createObjectURL(finalBlob); const a = document.createElement('a'); a.href = href; a.download = name; document.body.appendChild(a); a.click(); setTimeout(() => URL.revokeObjectURL(href), 2000); a.remove(); }
    function downloadDataUrl(name, dataUrl) { const a = document.createElement('a'); a.href = dataUrl; a.download = name; document.body.appendChild(a); a.click(); a.remove(); }
    function fallbackQrSvgDataUrl(label) { const safe = String(label || 'AR/XR').slice(0, 96).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c] || c)); const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 220"><rect width="220" height="220" rx="18" fill="#fff8e1"/><rect x="18" y="18" width="52" height="52" fill="#17110a"/><rect x="150" y="18" width="52" height="52" fill="#17110a"/><rect x="18" y="150" width="52" height="52" fill="#17110a"/><path d="M96 30h18v18H96zm32 0h10v10h-10zm-26 38h16v16h-16zm36 16h48v16h-48zM92 112h22v22H92zm42-2h16v16h-16zm30 18h22v22h-22zM90 158h14v14H90zm28 22h58v14h-58z" fill="#17110a"/><text x="110" y="116" text-anchor="middle" font-family="Inter,Arial,sans-serif" font-size="15" font-weight="900" fill="#17110a">QR AR/XR</text><text x="110" y="136" text-anchor="middle" font-family="Inter,Arial,sans-serif" font-size="8" fill="#17110a">' + safe + '</text></svg>'; return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg); }
    function installQrFallback(img) { if (!img) return; img.onerror = () => { img.onerror = null; img.src = fallbackQrSvgDataUrl(CARD_DATA.arLaunchUrl || CARD_DATA.edition); }; }
    function flipCard() { card.classList.toggle('is-flipped'); }
    function syncPointer(event) {
      const x = event.clientX ?? window.innerWidth * .58;
      const y = event.clientY ?? window.innerHeight * .36;
      const rect = card.getBoundingClientRect();
      const localX = clamp(((x - rect.left) / rect.width) - .5, -.5, .5);
      const localY = clamp(((y - rect.top) / rect.height) - .5, -.5, .5);
      const mx = clamp(((x - rect.left) / rect.width) * 100, 0, 100);
      const my = clamp(((y - rect.top) / rect.height) * 100, 0, 100);
      document.documentElement.style.setProperty('--ry', (localX * 14).toFixed(2) + 'deg');
      document.documentElement.style.setProperty('--rx', (localY * -10).toFixed(2) + 'deg');
      document.documentElement.style.setProperty('--mx', mx.toFixed(2) + '%');
      document.documentElement.style.setProperty('--my', my.toFixed(2) + '%');
      document.documentElement.style.setProperty('--logo-x', (50 + localX * 10).toFixed(2) + '%');
      document.documentElement.style.setProperty('--logo-y', (50 + localY * 8).toFixed(2) + '%');
      document.documentElement.style.setProperty('--holo-tilt', (localX * 7).toFixed(2) + 'deg');
    }
    function activityYearFromDate(value) {
      const date = new Date(value);
      return Number.isFinite(date.getFullYear()) ? date.getFullYear() : new Date().getFullYear();
    }
    const activityYears = Array.from(new Set((CARD_DATA.activity?.days || []).map(item => activityYearFromDate(item.date)))).sort((a, b) => a - b);
    let selectedActivityYear = activityYears.includes(new Date().getFullYear()) ? new Date().getFullYear() : (activityYears[activityYears.length - 1] || new Date().getFullYear());
    function renderActivityGrid() {
      const grid = $('#activity-grid');
      grid.innerHTML = '';
      const days = CARD_DATA.activity?.days || [];
      const filtered = CARD_DATA.tier === 'eternal-glass'
        ? days.filter(item => activityYearFromDate(item.date) === selectedActivityYear)
        : days.slice(Math.max(0, days.length - 371));
      const list = filtered.slice(-371);
      for (let i = 0; i < 371; i++) {
        const item = list[i] || { date: '', visits: 0, clicks: 0, level: 0, places: [] };
        const cell = document.createElement('i');
        const level = Math.max(0, Math.min(4, Number(item.level || 0)));
        const places = Array.isArray(item.places) ? item.places.join(', ') : (item.place || '');
        cell.className = 'activity-cell l' + level;
        cell.title = [item.date, places, 'visites ' + (item.visits || 0), 'clics ' + (item.clicks || 0)].filter(Boolean).join(' · ');
        grid.appendChild(cell);
      }
      const label = $('#activity-year-label');
      if (label) label.textContent = String(selectedActivityYear);
    }
    function changeActivityYear(delta) {
      if (!activityYears.length) return;
      const currentIndex = Math.max(0, activityYears.indexOf(selectedActivityYear));
      const nextIndex = clamp(currentIndex + delta, 0, activityYears.length - 1);
      selectedActivityYear = activityYears[nextIndex];
      renderActivityGrid();
    }
    function hydrateActivity() {
      renderActivityGrid();
    }
    function hydrateCard() {
      document.title = 'Microcosm · Carte prestige #' + CARD_DATA.edition;
      const title = getPrestigeCardTitle(CARD_DATA);
      $('#world-shot').src = CARD_DATA.snapshot;
      installQrFallback($('#front-qr'));
      installQrFallback($('#qr-code'));
      $('#front-qr').src = CARD_DATA.qrCodeUrl || fallbackQrSvgDataUrl(CARD_DATA.arLaunchUrl || CARD_DATA.edition);
      $('#qr-code').src = CARD_DATA.qrCodeUrl || fallbackQrSvgDataUrl(CARD_DATA.arLaunchUrl || CARD_DATA.edition);
      $('#front-title').textContent = title;
      $('#front-subline').textContent = CARD_DATA.rarity;
      $('#front-edition').textContent = '#' + CARD_DATA.edition;
      $('#front-certificate').textContent = 'Certificat de graine #' + CARD_DATA.edition;
      $('#front-certificate-copy').textContent = 'Croissance Fibonacci · golden ratio · cycle annuel vivant · lieu : ' + CARD_DATA.place;
      $('#progress-num').textContent = CARD_DATA.progressPct + '%';
      requestAnimationFrame(() => { $('#progress-fill').style.width = clamp(CARD_DATA.progressPct, 0, 100) + '%'; });
      const stats = $('#stats-grid');
      stats.append(stat('Jour', CARD_DATA.dayLabel), stat('Température', CARD_DATA.temperature + '°C'), stat('Vent', CARD_DATA.windSpeed + ' km/h'), stat('Humidité', CARD_DATA.humidity + '%'), stat('Nuages', CARD_DATA.clouds + '%'), stat('Synchro', CARD_DATA.sync));
      $('#back-title').textContent = title;
      $('#back-copy').textContent = CARD_DATA.tierLabel + ' · ' + CARD_DATA.rarity + ' · jour ' + CARD_DATA.dayLabel + '. Cette face arrière sert de certificat holographique et de point d’entrée AR/XR.';
      $('#edition-inline').textContent = CARD_DATA.edition;
      const arText = CARD_DATA.arLaunchUrl || '';
      const arLines = arText.includes('?') ? [arText.slice(0, arText.indexOf('?') + 1), arText.slice(arText.indexOf('?') + 1)] : [arText, ''];
      $('#ar-url').innerHTML = '<span></span><span></span>';
      $('#ar-url').children[0].textContent = arLines[0];
      $('#ar-url').children[1].textContent = arLines[1];
      $('#cleanup-warning').textContent = CARD_DATA.cleanupWarning + ' Télécharge la mémoire JSON pour restaurer l’arbre après nettoyage du navigateur.';
      const ids = $('#id-lines');
      ids.append(idLine('Édition', '#' + CARD_DATA.edition), idLine('Graine', CARD_DATA.seedPublic), idLine('Lieu récent', CARD_DATA.place), idLine('Microclimat', CARD_DATA.temperature + '°C · vent ' + CARD_DATA.windSpeed + ' km/h · ' + CARD_DATA.humidity + '%'), idLine('Prix à vie', CARD_DATA.prices.lifetimeLabel), idLine('Add-on vents & marées', CARD_DATA.prices.windsLabel));
      const metaGrid = $('#meta-grid');
      metaGrid.append(meta('Rareté', CARD_DATA.rarity), meta('Tier', CARD_DATA.tierLabel), meta('Synchro', CARD_DATA.sync), meta('Générée', CARD_DATA.generatedAt));
      hydrateActivity();
    }
    hydrateCard();
    syncPointer({ clientX: window.innerWidth * .58, clientY: window.innerHeight * .36 });
    document.body.addEventListener('pointermove', syncPointer, { passive: true });
    $('#flip-card').addEventListener('click', flipCard);
    $('#open-ar-xr').addEventListener('click', () => { const url = CARD_DATA.arLaunchUrl || BUNDLE.arLaunchUrl; if (url) window.open(url, '_blank', 'noopener'); });
    $('#copy-tree-json').addEventListener('click', async () => { const json = JSON.stringify(TREE_STATE, null, 2); try { await navigator.clipboard.writeText(json); $('#copy-tree-json').textContent = 'JSON copié'; setTimeout(() => $('#copy-tree-json').textContent = 'Copier JSON', 1300); } catch (_) { downloadBlob('microcosm-tree-memory-' + CARD_DATA.edition + '.json', json, 'application/json'); } });
    $('#download-tree-json').addEventListener('click', () => downloadBlob('microcosm-tree-memory-' + CARD_DATA.edition + '.json', JSON.stringify(TREE_STATE, null, 2), 'application/json'));
    $('#download-texture-png').addEventListener('click', () => downloadDataUrl('microcosm-card-texture-' + CARD_DATA.edition + '.png', BUNDLE.texturePngDataUrl));
    $('#download-texture-svg').addEventListener('click', () => downloadBlob('microcosm-card-texture-' + CARD_DATA.edition + '.svg', BUNDLE.textureSvg, 'image/svg+xml'));
    $('#download-manifest').addEventListener('click', () => downloadBlob('microcosm-tree-bundle-' + CARD_DATA.edition + '.json', JSON.stringify(MANIFEST, null, 2), 'application/json'));
    $('#activity-prev-year')?.addEventListener('click', () => changeActivityYear(-1));
    $('#activity-next-year')?.addEventListener('click', () => changeActivityYear(1));
  <\/script>
</body>
</html>`;
    }

    async function saveBundleFiles(files, useDirectoryPicker = false) {
      // V8.14_EXPORT_SINGLE_MEMORY_CARD : ne jamais lancer plusieurs téléchargements.
      // Le fichier HTML autonome embarque JSON, SVG, PNG dataURL, manifeste, activité, QR et URL AR/XR.
      const file = Array.isArray(files) ? files[0] : files;
      if (!file || !file.blob) throw new Error('Aucun fichier HTML de carte mémoire à télécharger.');
      const href = URL.createObjectURL(file.blob);
      const a = document.createElement('a');
      a.href = href;
      a.download = file.name;
      document.body.appendChild(a);
      a.click();
      setTimeout(() => URL.revokeObjectURL(href), 2400);
      a.remove();
      return 'single-html';
    }

    async function buildTreeMemoryBundle(fullBundle = false) {
      const payload = buildCollectorCardPayload();
      const state = collectMicrocosmState();
      let texturePngDataUrl = payload.snapshot;
      let texturePngBlob = null;
      try {
        const textureCanvas = await renderTransparentCollectorTextureCanvas(payload);
        texturePngDataUrl = textureCanvas.toDataURL('image/png');
        texturePngBlob = await canvasToBlob(textureCanvas, 'image/png');
      } catch (err) {
        console.warn('Texture PNG collector remplacée par la capture de secours :', err);
        texturePngBlob = await fetch(texturePngDataUrl).then(r => r.blob());
      }
      const textureSvg = buildTransparentCollectorTextureSvg(payload, texturePngDataUrl);
      const manifest = buildMemoryBundleManifest(payload, state);
      const bundle = {
        schema: 'microcosm-interactif-9.embedded-memory-card.v2.single-html',
        exportMode: 'single-memory-card-html',
        cardData: payload,
        treeState: state,
        manifest,
        textureSvg,
        texturePngDataUrl,
        snapshot: payload.snapshot,
        activity: payload.activity,
        locations: payload.activity?.locations || [],
        arLaunchUrl: payload.arLaunchUrl,
        qrCodeUrl: payload.qrCodeUrl
      };
      const html = buildStandaloneCollectorBundleHtml(bundle);
      const memoryCardFile = {
        name: `${MICROCOSM_CONFIG.cards.memoryCardFilenamePrefix}-${payload.edition}.html`,
        blob: new Blob([html], { type: 'text/html;charset=utf-8' })
      };
      return { payload, state, bundle, file: memoryCardFile, files: [memoryCardFile] };
    }

    function buildStandaloneCollectorCardHtml(data) {
      const json = safeMicrocosmCardJson(data);
      return `<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Microcosm · Carte mémoire #${escapeMicrocosmCardText(data.edition)}</title>
  <style>
    :root {
      --soil-0: #080604;
      --soil-1: #17110a;
      --soil-2: #2f2112;
      --soil-3: #6f4c25;
      --olive-1: #293311;
      --olive-2: #556b2d;
      --cream: #fff8e1;
      --cream-soft: rgba(255, 248, 225, 0.74);
      --gold: #f4d58a;
      --gold-soft: rgba(244, 213, 138, 0.34);
      --x: 50vw;
      --y: 50vh;
      --xp: 0.5;
      --yp: 0.5;
      --rx: 0deg;
      --ry: 0deg;
      --shine: 50%;
      color-scheme: dark;
    }
    * { box-sizing: border-box; }
    html, body {
      width: 100%;
      min-height: 100%;
      margin: 0;
      background:
        radial-gradient(circle at calc(var(--xp) * 100%) calc(var(--yp) * 100%), rgba(244, 213, 138, 0.16), transparent 34vw),
        radial-gradient(circle at 82% 12%, rgba(85, 107, 45, 0.22), transparent 34vw),
        linear-gradient(145deg, #080604 0%, #17110a 45%, #060805 100%);
      color: var(--cream);
      font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      overflow-x: hidden;
    }
    body {
      display: grid;
      place-items: center;
      padding: clamp(18px, 4vw, 52px);
    }
    .ambient-grain,
    .ambient-orb {
      position: fixed;
      inset: 0;
      pointer-events: none;
    }
    .ambient-grain {
      opacity: 0.18;
      mix-blend-mode: soft-light;
      background-image:
        radial-gradient(circle at 20% 20%, rgba(255,255,255,0.18) 0 1px, transparent 1.4px),
        radial-gradient(circle at 70% 80%, rgba(244,213,138,0.16) 0 1px, transparent 1.5px);
      background-size: 21px 23px, 31px 29px;
    }
    .ambient-orb {
      background:
        radial-gradient(circle at 18% 10%, rgba(244,213,138,0.18), transparent 24vw),
        radial-gradient(circle at 88% 72%, rgba(85,107,45,0.18), transparent 30vw);
      animation: breathe 9s ease-in-out infinite alternate;
    }
    @keyframes breathe {
      from { opacity: 0.55; transform: scale(1); }
      to { opacity: 0.95; transform: scale(1.04); }
    }
    .page {
      width: min(1180px, 100%);
      display: grid;
      grid-template-columns: minmax(320px, 520px) minmax(280px, 1fr);
      gap: clamp(18px, 4vw, 48px);
      align-items: center;
      position: relative;
      z-index: 1;
    }
    .card-stage {
      perspective: 1400px;
      display: grid;
      place-items: center;
      min-height: min(88vh, 860px);
    }
    .micro-card {
      width: min(500px, 92vw);
      aspect-ratio: 1080 / 1600;
      position: relative;
      transform-style: preserve-3d;
      transform: rotateX(var(--rx)) rotateY(var(--ry));
      transition: transform 0.18s ease;
      border-radius: 36px;
      filter: drop-shadow(0 34px 60px rgba(0,0,0,0.54));
    }
    .micro-card.is-flipped {
      transform: rotateX(var(--rx)) rotateY(calc(180deg + var(--ry)));
    }
    .card-face {
      position: absolute;
      inset: 0;
      overflow: hidden;
      border-radius: inherit;
      backface-visibility: hidden;
      border: 1px solid rgba(255, 248, 225, 0.20);
      background:
        radial-gradient(circle at 22% 0%, rgba(244, 213, 138, 0.22), transparent 28%),
        radial-gradient(circle at 80% 12%, rgba(85, 107, 45, 0.20), transparent 32%),
        linear-gradient(145deg, rgba(111, 76, 37, 0.96), rgba(23, 17, 10, 0.98) 28%, rgba(25, 31, 13, 0.98) 62%, rgba(8, 6, 4, 0.98));
      box-shadow:
        inset 0 1px 0 rgba(255, 248, 225, 0.24),
        inset 0 -1px 0 rgba(0,0,0,0.52),
        0 0 0 10px rgba(244, 213, 138, 0.035);
    }
    .card-face::before {
      content: "";
      position: absolute;
      inset: 10px;
      border-radius: 28px;
      padding: 2px;
      background: linear-gradient(135deg, rgba(255,248,225,0.9), rgba(157,106,50,0.26), rgba(85,107,45,0.36), rgba(244,213,138,0.82));
      -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
      -webkit-mask-composite: xor;
      mask-composite: exclude;
      pointer-events: none;
      opacity: 0.88;
    }
    .card-face::after {
      content: "";
      position: absolute;
      inset: 0;
      background:
        linear-gradient(115deg, transparent 0 37%, rgba(255,255,255,0.24) 47%, transparent 58% 100%),
        repeating-linear-gradient(-38deg, rgba(244,213,138,0.00) 0 16px, rgba(244,213,138,0.08) 17px 18px, rgba(85,107,45,0.06) 19px 26px);
      background-position: calc(var(--shine) * 1) center, center;
      background-size: 210% 210%, 100% 100%;
      opacity: 0.46;
      mix-blend-mode: screen;
      pointer-events: none;
      animation: foil 6s ease-in-out infinite alternate;
    }
    @keyframes foil {
      from { opacity: 0.28; background-position: 0% center, center; }
      to { opacity: 0.62; background-position: 100% center, center; }
    }
    .card-back {
      transform: rotateY(180deg);
      padding: 42px;
      display: grid;
      align-content: center;
      gap: 20px;
    }
    .front-content {
      position: relative;
      z-index: 2;
      height: 100%;
      padding: 42px;
      display: grid;
      grid-template-rows: auto auto 1fr auto auto;
      gap: 18px;
    }
    .card-kicker {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
      color: rgba(255, 248, 225, 0.76);
      font-size: 12px;
      font-weight: 850;
      letter-spacing: 0.16em;
      text-transform: uppercase;
    }
    .edition-pill {
      border: 1px solid rgba(244,213,138,0.28);
      border-radius: 999px;
      padding: 8px 12px;
      background: rgba(15, 10, 5, 0.34);
      white-space: nowrap;
    }
    h1 {
      margin: 0;
      font-size: clamp(36px, 7vw, 58px);
      line-height: 0.92;
      letter-spacing: -0.06em;
      text-transform: uppercase;
      color: var(--cream);
      text-shadow: 0 1px 0 rgba(255,255,255,0.08), 0 16px 26px rgba(0,0,0,0.28);
    }
    .rarity {
      color: var(--gold);
      font-size: 13px;
      letter-spacing: 0.14em;
      text-transform: uppercase;
      font-weight: 900;
      margin-top: 7px;
    }
    .image-window {
      position: relative;
      min-height: 0;
      border-radius: 28px;
      overflow: hidden;
      border: 1px solid rgba(244,213,138,0.34);
      background: rgba(15,10,5,0.44);
      box-shadow: inset 0 0 0 1px rgba(255,248,225,0.08), 0 18px 40px rgba(0,0,0,0.26);
    }
    .image-window img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      display: block;
      filter: saturate(1.08) contrast(1.04) brightness(0.92);
      transform: scale(1.018);
    }
    .image-window .vignette {
      position: absolute;
      inset: 0;
      background: radial-gradient(circle at center, transparent 45%, rgba(0,0,0,0.55) 100%);
    }
    .progress-shell {
      position: absolute;
      left: 20px;
      right: 20px;
      bottom: 20px;
      padding: 12px 14px;
      border-radius: 999px;
      display: grid;
      grid-template-columns: 1fr auto;
      gap: 14px;
      align-items: center;
      background: rgba(8,6,4,0.66);
      border: 1px solid rgba(244,213,138,0.24);
      backdrop-filter: blur(18px);
    }
    .progress-track {
      height: 10px;
      border-radius: 999px;
      overflow: hidden;
      background: rgba(255,248,225,0.13);
    }
    .progress-fill {
      height: 100%;
      width: 0%;
      border-radius: inherit;
      background: linear-gradient(90deg, #6e8f2e, #c79d43 58%, #fff0a8);
      box-shadow: 0 0 18px rgba(244,213,138,0.52);
      transition: width 1.2s cubic-bezier(.16,1,.3,1);
    }
    .progress-num {
      font-size: 14px;
      font-weight: 950;
      color: var(--cream);
      min-width: 46px;
      text-align: right;
    }
    .stats-grid {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 10px;
    }
    .stat {
      border-radius: 18px;
      padding: 12px;
      background: rgba(15, 10, 5, 0.38);
      border: 1px solid rgba(244,213,138,0.16);
      min-width: 0;
    }
    .stat small {
      display: block;
      color: rgba(226,218,188,0.58);
      font-size: 10px;
      font-weight: 800;
      text-transform: uppercase;
      letter-spacing: 0.09em;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .stat b {
      display: block;
      margin-top: 4px;
      color: var(--cream);
      font-size: 18px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .certificate {
      display: grid;
      grid-template-columns: 76px 1fr;
      gap: 16px;
      align-items: center;
      padding: 14px;
      border-radius: 22px;
      background: rgba(8,6,4,0.44);
      border: 1px solid rgba(244,213,138,0.18);
    }
    .seed-grid {
      display: grid;
      grid-template-columns: repeat(9, 1fr);
      gap: 2px;
      width: 76px;
      height: 76px;
      padding: 8px;
      border-radius: 18px;
      background: rgba(255,248,225,0.06);
      box-shadow: inset 0 0 0 1px rgba(244,213,138,0.16);
    }
    .seed-grid i {
      border-radius: 2px;
      background: rgba(255,248,225,0.14);
    }
    .seed-grid i.on:nth-child(3n) { background: rgba(244,213,138,0.92); }
    .seed-grid i.on:nth-child(3n+1) { background: rgba(146,166,82,0.90); }
    .seed-grid i.on:nth-child(3n+2) { background: rgba(255,248,225,0.82); }
    .certificate strong {
      display: block;
      font-size: 17px;
      color: var(--cream);
      text-transform: uppercase;
    }
    .certificate span {
      display: block;
      margin-top: 5px;
      font-size: 12px;
      color: rgba(226,218,188,0.66);
      line-height: 1.35;
    }
    .back-title {
      font-size: 30px;
      margin: 0;
      line-height: 1.05;
      letter-spacing: -0.04em;
    }
    .back-list {
      display: grid;
      gap: 12px;
      margin: 0;
      padding: 0;
      list-style: none;
    }
    .back-list li {
      padding: 14px 16px;
      border-radius: 18px;
      background: rgba(15,10,5,0.36);
      border: 1px solid rgba(244,213,138,0.14);
      color: rgba(255,248,225,0.82);
      line-height: 1.38;
      font-size: 14px;
    }
    .back-list b {
      color: var(--gold);
      display: block;
      margin-bottom: 4px;
      text-transform: uppercase;
      font-size: 11px;
      letter-spacing: 0.12em;
    }
    .panel {
      position: relative;
      z-index: 2;
      padding: clamp(18px, 3vw, 30px);
      border-radius: 30px;
      background:
        radial-gradient(circle at 18% 0%, rgba(244,213,138,0.16), transparent 34%),
        linear-gradient(145deg, rgba(27,20,12,0.78), rgba(52,39,21,0.48));
      border: 1px solid rgba(216,171,97,0.22);
      box-shadow: 0 26px 58px rgba(7,10,5,0.28), inset 0 1px 0 rgba(255,238,193,0.14);
      backdrop-filter: blur(20px);
    }
    .panel h2 {
      margin: 0 0 10px;
      font-size: clamp(28px, 5vw, 46px);
      line-height: 1;
      letter-spacing: -0.05em;
    }
    .panel p {
      color: rgba(226,218,188,0.76);
      line-height: 1.55;
      margin: 0 0 18px;
    }
    .controls, .card-actions-clean {
      display: grid;
      gap: 10px;
      margin-top: 18px;
    }
    .card-actions-clean {
      padding: 10px;
      border-radius: 20px;
      background: rgba(8,6,4,0.30);
      border: 1px solid rgba(244,213,138,0.12);
    }
    .card-actions-label {
      color: rgba(226,218,188,0.58);
      font-size: 10px;
      letter-spacing: 0.14em;
      text-transform: uppercase;
      font-weight: 900;
    }
    .action-row {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 8px;
    }
    button, a.button {
      border: 1px solid rgba(216,171,97,0.22);
      border-radius: 16px;
      min-height: 42px;
      padding: 0 16px;
      color: rgba(255,248,225,0.92);
      background: linear-gradient(145deg, rgba(79,110,38,0.54), rgba(38,69,28,0.36));
      font: 850 13px Inter, system-ui, sans-serif;
      cursor: pointer;
      text-decoration: none;
      text-align: center;
    }
    button.primary, a.button.primary {
      min-height: 50px;
      border-color: rgba(244,213,138,0.34);
      background: radial-gradient(circle at 14% 0%, rgba(244,213,138,0.18), transparent 46%), linear-gradient(145deg, rgba(79,110,38,0.66), rgba(38,69,28,0.42));
    }
    button.secondary, a.button.secondary {
      background: linear-gradient(145deg, rgba(74,53,30,0.54), rgba(22,17,10,0.42));
    }
    .meta-grid {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 10px;
      margin-top: 18px;
    }
    .meta {
      border-radius: 18px;
      padding: 13px 14px;
      background: rgba(8,6,4,0.32);
      border: 1px solid rgba(244,213,138,0.12);
    }
    .meta small {
      display: block;
      color: rgba(226,218,188,0.56);
      font-size: 10px;
      letter-spacing: 0.10em;
      text-transform: uppercase;
      font-weight: 850;
    }
    .meta b {
      display: block;
      margin-top: 5px;
      overflow-wrap: anywhere;
    }
    @media (max-width: 920px) {
      body { padding: 18px; }
      .page { grid-template-columns: 1fr; }
      .card-stage { min-height: auto; }
      .panel { order: -1; }
    }
    @media (max-width: 520px) {
      .front-content { padding: 30px; gap: 12px; }
      .stats-grid { grid-template-columns: repeat(2, 1fr); }
      .action-row { grid-template-columns: 1fr; }
      .card-kicker { font-size: 10px; }
      .certificate { grid-template-columns: 62px 1fr; }
      .seed-grid { width: 62px; height: 62px; }
      .card-back { padding: 30px; }
    }
    @media (prefers-reduced-motion: reduce) {
      *, *::before, *::after { animation: none !important; transition: none !important; }
      .micro-card, .micro-card.is-flipped { transform: none !important; }
    }
  </style>
</head>
<body>
  <div class="ambient-orb"></div>
  <div class="ambient-grain"></div>

  <main class="page">
    <section class="card-stage" aria-label="Carte collector dynamique Microcosm">
      <article class="micro-card" id="micro-card">
        <section class="card-face card-front">
          <div class="front-content">
            <div class="card-kicker">
              <span>MICROCOSM INTERACTIF 9</span>
              <span class="edition-pill" data-role="edition">#------</span>
            </div>
            <div>
              <h1 data-role="stage">Arbre vivant</h1>
              <div class="rarity" data-role="rarity">Graine vivante</div>
            </div>
            <div class="image-window">
              <img id="world-shot" alt="Vue du microcosme capturée au moment de la génération">
              <div class="vignette"></div>
              <div class="progress-shell">
                <div class="progress-track"><div class="progress-fill" id="progress-fill"></div></div>
                <div class="progress-num" data-role="progress">0%</div>
              </div>
            </div>
            <div class="stats-grid" id="stats-grid"></div>
            <div class="certificate">
              <div class="seed-grid" id="seed-grid" aria-hidden="true"></div>
              <div>
                <strong data-role="certificate">Certificat de graine</strong>
                <span>Croissance Fibonacci · golden ratio · cycle annuel vivant · carte HTML autonome générée depuis le microcosme local.</span>
              </div>
            </div>
          </div>
        </section>
        <section class="card-face card-back">
          <h2 class="back-title">Mémoire vivante de l’arbre</h2>
          <ul class="back-list" id="back-list"></ul>
          <div class="card-actions-clean">
            <div class="card-actions-label">Actions rapides</div>
            <div class="action-row">
              <button type="button" class="primary" id="flip-front">Recto / verso</button>
              <button type="button" class="secondary" id="copy-json">Copier JSON</button>
            </div>
          </div>
        </section>
      </article>
    </section>

    <aside class="panel">
      <h2>Carte mémoire, pas une image figée.</h2>
      <p>
        Cette page est autonome : elle embarque la capture du microcosme, les données de croissance,
        le microclimat et l’identité de graine. La carte réagit au pointeur, se retourne et reste consultable
        sans le projet original.
      </p>
      <div class="card-actions-clean" aria-label="Actions de la carte dynamique">
        <div class="card-actions-label">Carte dynamique</div>
        <button type="button" class="primary" id="flip-card">Voir recto / verso</button>
        <button type="button" class="secondary" id="download-json">Télécharger le JSON</button>
      </div>
      <div class="meta-grid" id="meta-grid"></div>
    </aside>
  </main>

  <script>
    const CARD_DATA = ${json};

    const $ = (selector) => document.querySelector(selector);
    const card = $('#micro-card');
    const clamp = (v, min, max) => Math.min(max, Math.max(min, v));

    function hashString(value) {
      let hash = 2166136261;
      const str = String(value || 'microcosm');
      for (let i = 0; i < str.length; i++) {
        hash ^= str.charCodeAt(i);
        hash = Math.imul(hash, 16777619);
      }
      return hash >>> 0;
    }

    function setText(role, value) {
      const node = document.querySelector('[data-role="' + role + '"]');
      if (node) node.textContent = value;
    }

    function stat(label, value) {
      const item = document.createElement('div');
      item.className = 'stat';
      item.innerHTML = '<small></small><b></b>';
      item.querySelector('small').textContent = label;
      item.querySelector('b').textContent = value;
      return item;
    }

    function meta(label, value) {
      const item = document.createElement('div');
      item.className = 'meta';
      item.innerHTML = '<small></small><b></b>';
      item.querySelector('small').textContent = label;
      item.querySelector('b').textContent = value;
      return item;
    }

    function hydrateCard(data) {
      document.title = 'Microcosm · Carte mémoire #' + data.edition;
      $('#world-shot').src = data.snapshot;
      setText('edition', '#' + data.edition);
      setText('stage', data.stage);
      setText('rarity', data.rarity);
      setText('progress', data.progressPct + '%');
      setText('certificate', 'Certificat de graine #' + data.edition);
      requestAnimationFrame(() => {
        $('#progress-fill').style.width = clamp(data.progressPct, 0, 100) + '%';
      });

      const stats = $('#stats-grid');
      stats.append(
        stat('Jour', String(data.day).padStart(3, '0') + '/365'),
        stat('Température', data.temperature + '°C'),
        stat('Vent', data.windSpeed + ' km/h'),
        stat('Humidité', data.humidity + '%'),
        stat('Nuages', data.clouds + '%'),
        stat('Synchro', data.sync)
      );

      const back = $('#back-list');
      const rows = [
        ['Microclimat', data.place + ' · ressenti ' + data.feelsLike + '°C · pression ' + data.pressure + ' hPa'],
        ['Graine', data.seedPublic + ' · variante unique localStorage'],
        ['Cycle', data.stage + ' · ' + data.progressPct + '% · ' + data.generatedAt],
        ['Vent global', data.windDirection + '° · un seul vent agit sur océan, herbe, feuilles, rivage et nuages']
      ];
      rows.forEach(([label, value]) => {
        const li = document.createElement('li');
        li.innerHTML = '<b></b><span></span>';
        li.querySelector('b').textContent = label;
        li.querySelector('span').textContent = value;
        back.appendChild(li);
      });

      const metaGrid = $('#meta-grid');
      metaGrid.append(
        meta('Édition', '#' + data.edition),
        meta('Rareté', data.rarity),
        meta('Lieu', data.place),
        meta('Générée', data.generatedAt)
      );

      const seed = $('#seed-grid');
      for (let i = 0; i < 81; i++) {
        const cell = document.createElement('i');
        const n = hashString(data.edition + ':' + i);
        const edge = i < 9 || i >= 72 || i % 9 === 0 || i % 9 === 8;
        if ((n % 100) > (edge ? 35 : 54)) cell.className = 'on';
        seed.appendChild(cell);
      }
    }

    function syncPointer(event) {
      const x = event.clientX ?? window.innerWidth / 2;
      const y = event.clientY ?? window.innerHeight / 2;
      const xp = x / window.innerWidth;
      const yp = y / window.innerHeight;
      document.documentElement.style.setProperty('--x', x + 'px');
      document.documentElement.style.setProperty('--y', y + 'px');
      document.documentElement.style.setProperty('--xp', xp.toFixed(3));
      document.documentElement.style.setProperty('--yp', yp.toFixed(3));
      document.documentElement.style.setProperty('--shine', (xp * 100).toFixed(1) + '%');
      const rect = card.getBoundingClientRect();
      const localX = (x - rect.left) / rect.width - 0.5;
      const localY = (y - rect.top) / rect.height - 0.5;
      document.documentElement.style.setProperty('--ry', clamp(localX * 14, -10, 10).toFixed(2) + 'deg');
      document.documentElement.style.setProperty('--rx', clamp(localY * -12, -8, 8).toFixed(2) + 'deg');
    }

    function flipCard() {
      card.classList.toggle('is-flipped');
    }

    function downloadJson() {
      const blob = new Blob([JSON.stringify(CARD_DATA, null, 2)], { type: 'application/json' });
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = 'microcosm-card-' + CARD_DATA.edition + '.json';
      document.body.appendChild(a);
      a.click();
      URL.revokeObjectURL(a.href);
      a.remove();
    }

    async function copyJson() {
      try {
        await navigator.clipboard.writeText(JSON.stringify(CARD_DATA, null, 2));
        $('#copy-json').textContent = 'Données copiées';
        setTimeout(() => $('#copy-json').textContent = 'Copier les données', 1300);
      } catch (err) {
        downloadJson();
      }
    }

    hydrateCard(CARD_DATA);
    document.body.addEventListener('pointermove', syncPointer, { passive: true });
    $('#flip-card').addEventListener('click', flipCard);
    $('#flip-front').addEventListener('click', flipCard);
    $('#download-json').addEventListener('click', downloadJson);
    $('#copy-json').addEventListener('click', copyJson);
  <\/script>
</body>
</html>`;
    }


    async function openCurrentMemoryCardViewer() {
      const button = document.getElementById('save-tree-btn');
      const previousHtml = button ? button.innerHTML : '';
      try {
        if (button) {
          button.disabled = true;
          button.innerHTML = 'Ouverture de la carte…<small>génération du même HTML autonome</small>';
        }
        const { file, payload, bundle } = await buildTreeMemoryBundle(false);
        openFloatingMemoryCard(file, payload, bundle);
      } catch (err) {
        console.warn('Prévisualisation de la carte mémoire HTML impossible :', err);
        createShareCardPng();
      } finally {
        if (button) {
          button.innerHTML = previousHtml;
          syncPaidMemoryUi();
        }
      }
    }

    async function createShareCardHtml() {
      return openCurrentMemoryCardViewer();
    }

    let currentMemoryCardPreview = null;

    function revokeCurrentMemoryCardPreview() {
      currentMemoryCardPreview = null;
    }

    function downloadMemoryCardFile(file) {
      if (!file?.blob) throw new Error('Aucun fichier HTML à télécharger.');
      const href = URL.createObjectURL(file.blob);
      const a = document.createElement('a');
      a.href = href;
      a.download = file.name;
      document.body.appendChild(a);
      a.click();
      setTimeout(() => URL.revokeObjectURL(href), 2400);
      a.remove();
    }

    function setMemoryPopupText(id, value) {
      const node = document.getElementById(id);
      if (node) node.textContent = value == null || value === '' ? '--' : String(value);
    }

    // V8.19 · Les titres de stade peuvent être longs : on adapte la taille
    // au contenu au lieu de couper la carte. À utiliser pour le viewer popup,
    // sans toucher à la croissance Fibonacci validée.
    function fitMemoryCardTitle(id, value, options = {}) {
      const node = document.getElementById(id);
      if (!node) return;
      const text = String(value || 'Graine en éveil').trim();
      node.textContent = text || 'Graine en éveil';
      const len = text.length;
      const max = Number(options.max || 42);
      const min = Number(options.min || 22);
      let size = max;
      if (len > 14) size = Math.min(size, 37);
      if (len > 19) size = Math.min(size, 33);
      if (len > 26) size = Math.min(size, 29);
      if (len > 34) size = Math.min(size, 25);
      size = Math.max(min, size);
      node.style.fontSize = `${size}px`;
      node.style.lineHeight = len > 24 ? '0.94' : '0.90';
      node.style.letterSpacing = len > 24 ? '-0.025em' : '0.015em';
      node.style.maxWidth = '100%';
      node.style.overflowWrap = 'anywhere';
    }

    function toggleMemoryCardFace() {
      const viewer = document.getElementById('memory-card-viewer');
      if (!viewer) return;
      viewer.classList.toggle('is-flipped');
      syncMemoryCardFlipButton();
    }

    function syncFloatingMemoryCardActions() {
      const saveAllowed = isMicrocosmMemorySaveUnlocked();
      const downloadBtn = document.getElementById('memory-card-download-btn');
      const note = document.getElementById('memory-card-more-options-note');
      if (downloadBtn) {
        downloadBtn.disabled = false;
        downloadBtn.setAttribute('aria-disabled', saveAllowed ? 'false' : 'true');
        downloadBtn.classList.toggle('is-soft-locked', !saveAllowed);
        downloadBtn.textContent = saveAllowed ? 'Enregistrer' : 'Déverrouiller la mémoire';
        downloadBtn.title = saveAllowed
          ? 'Télécharger le fichier HTML autonome de la carte mémoire.'
          : 'Ouvrir les options de mémoire autonome.';
      }
      if (note) note.textContent = saveAllowed
        ? 'HTML autonome prêt'
        : 'consultation gratuite · export autonome verrouillé';
    }

    function openFloatingMemoryCard(file, payload = {}, bundle = null) {
      const modal = document.getElementById('memory-card-modal');
      const subtitle = document.getElementById('memory-card-modal-subtitle');
      const viewer = document.getElementById('memory-card-viewer');
      if (!modal || !file?.blob) return;
      revokeCurrentMemoryCardPreview();
      currentMemoryCardPreview = { file, payload, bundle };

      const texture = bundle?.texturePngDataUrl || payload?.snapshot || '';
      const encodedTextureUrl = texture ? `url("${String(texture).replace(/"/g, '%22')}")` : '';
      const frontFace = document.querySelector('.memory-card-face.memory-card-front');
      if (frontFace) {
        if (encodedTextureUrl) {
          frontFace.classList.add('export-card-front');
          frontFace.style.setProperty('--memory-card-export-texture', encodedTextureUrl);
        } else {
          frontFace.classList.remove('export-card-front');
          frontFace.style.removeProperty('--memory-card-export-texture');
        }
      }
      const shot = document.getElementById('memory-card-view-shot');
      if (shot) {
        shot.style.setProperty('--memory-card-shot', texture ? encodedTextureUrl : 'radial-gradient(circle at 50% 55%, rgba(79,110,38,0.55), rgba(8,7,4,0.90))');
      }
      if (viewer) {
        viewer.classList.remove('is-flipped');
        viewer.style.setProperty('--tilt-x', '0deg');
        viewer.style.setProperty('--tilt-y', '0deg');
        viewer.style.setProperty('--shine-x', '45%');
        viewer.style.setProperty('--shine-y', '18%');
      }
      const flipBtn = document.getElementById('memory-card-flip-btn');
      if (flipBtn) flipBtn.textContent = 'Voir verso';

      const progress = Math.max(0, Math.min(100, Number(payload?.progressPct || Math.round(growthProgress * 100) || 0)));
      const qrSrc = payload?.qrCodeUrl || bundle?.qrCodeUrl || '';
      const qrFront = document.getElementById('memory-card-view-qr');
      const qrBack = document.getElementById('memory-card-back-qr');
      for (const qr of [qrFront, qrBack]) {
        if (!qr) continue;
        qr.hidden = !qrSrc;
        if (qrSrc) qr.src = qrSrc;
        qr.onerror = () => { qr.hidden = true; };
      }

      if (subtitle) subtitle.textContent = payload?.edition
        ? 'Édition #' + payload.edition + ' · viewer 3D flottant'
        : 'Carte HTML autonome prête à sauvegarder';
      setMemoryPopupText('memory-card-edition-meta', payload?.edition ? 'Édition #' + payload.edition + ' · fichier HTML autonome' : 'Carte HTML autonome');
      fitMemoryCardTitle('memory-card-title-line', payload?.stageName || payload?.stage || 'Carte mémoire', { max: 40, min: 22 });
      setMemoryPopupText('memory-card-popup-summary', 'Le recto reprend la texture PNG/SVG intégrée au HTML autonome payant. Clique ou touche la carte pour voir le verso certificat.');
      setMemoryPopupText('memory-card-popup-day', `${String(payload?.day ?? growthClicksTree).padStart(3, '0')}/365`);
      setMemoryPopupText('memory-card-popup-tier', payload?.tierLabel || resolveCardTier().tierLabel);
      setMemoryPopupText('memory-card-popup-weather', `${Math.round(microClimate.airTemperature)}°C · vent ${Math.round(microClimate.windSpeed)} km/h · humidité ${Math.round(microClimate.humidity)}%`);
      setMemoryPopupText('memory-card-popup-seed', payload?.seedPublic || String(treeUserSeedString || treeGameSeedString || 'microcosm').slice(0, 24));

      setMemoryPopupText('memory-card-view-edition', payload?.edition ? '#' + payload.edition : '#------');
      fitMemoryCardTitle('memory-card-view-title', getPrestigeCardTitle(payload), { max: 42, min: 22 });
      setMemoryPopupText('memory-card-view-rarity', payload?.rarity || payload?.tierLabel || 'Mémoire vivante');
      const progressBar = document.getElementById('memory-card-view-progress-bar');
      if (progressBar) progressBar.style.width = progress + '%';
      setMemoryPopupText('memory-card-view-progress-label', progress + '%');
      setMemoryPopupText('memory-card-view-day', payload?.dayLabel || `${String(payload?.day ?? growthClicksTree).padStart(3, '0')}/365`);
      setMemoryPopupText('memory-card-view-temp', `${Math.round(payload?.temperature ?? microClimate.airTemperature)}°C`);
      setMemoryPopupText('memory-card-view-wind', `${Math.round(payload?.windSpeed ?? microClimate.windSpeed)} km/h`);
      setMemoryPopupText('memory-card-view-humidity', `${Math.round(payload?.humidity ?? microClimate.humidity)}%`);
      setMemoryPopupText('memory-card-view-clouds', `${Math.round(payload?.clouds ?? ((microClimate.cloudCover || 0) * 100))}%`);
      setMemoryPopupText('memory-card-view-sync', payload?.sync || (microClimate.liveWeatherEnabled ? 'LIVE' : 'AUTO'));
      setMemoryPopupText('memory-card-view-cert', `Certificat de graine #${payload?.edition || '------'}`);

      fitMemoryCardTitle('memory-card-back-title', payload?.tierLabel || 'Identité de l’arbre', { max: 34, min: 20 });
      setMemoryPopupText('memory-card-back-seed', payload?.seedPublic || String(treeUserSeedString || treeGameSeedString || 'microcosm').slice(0, 24));
      setMemoryPopupText('memory-card-back-place', payload?.place || weatherPlaceLabel());
      setMemoryPopupText('memory-card-back-generated', payload?.generatedAt || new Date().toLocaleString('fr-FR', { dateStyle: 'medium', timeStyle: 'short' }));
      const activity = payload?.activity || bundle?.activity || {};
      const visits = Number(activity.totalVisits || 0);
      const clicks = Number(activity.totalClicks || growthClicksTree || 0);
      setMemoryPopupText('memory-card-back-activity', `${visits} visites · ${clicks} clics`);
      setMemoryPopupText('memory-card-back-note', payload?.note || 'Même données, même texture et même certificat que la carte HTML autonome.');
      setMemoryPopupText('memory-card-back-url', String(payload?.arLaunchUrl || bundle?.arLaunchUrl || MICROCOSM_CONFIG.urls.arBaseUrl || '').replace(/^https?:\/\//, '').slice(0, 64));

      syncFloatingMemoryCardActions();
      document.body.classList.add('microcosm-memory-card-open');
      modal.classList.add('open');
      modal.removeAttribute('hidden');
      modal.setAttribute('aria-hidden', 'false');
    }

    function closeFloatingMemoryCard() {
      const modal = document.getElementById('memory-card-modal');
      const shot = document.getElementById('memory-card-view-shot');
      const viewer = document.getElementById('memory-card-viewer');
      for (const qr of [document.getElementById('memory-card-view-qr'), document.getElementById('memory-card-back-qr')]) {
        if (qr) qr.removeAttribute('src');
      }
      const frontFace = document.querySelector('.memory-card-face.memory-card-front');
      if (frontFace) {
        frontFace.classList.remove('export-card-front');
        frontFace.style.removeProperty('--memory-card-export-texture');
      }
      if (shot) shot.style.removeProperty('--memory-card-shot');
      if (viewer) {
        viewer.classList.remove('is-flipped');
        viewer.style.setProperty('--tilt-x', '0deg');
        viewer.style.setProperty('--tilt-y', '0deg');
      }
      if (modal) {
        modal.classList.remove('open');
        modal.setAttribute('hidden', '');
        modal.setAttribute('aria-hidden', 'true');
      }
      document.body.classList.remove('microcosm-memory-card-open');
      revokeCurrentMemoryCardPreview();
    }

    async function saveTreeMemoryBundle() {
      const button = document.getElementById('save-tree-btn');
      const hint = document.getElementById('save-tree-btn-hint');
      if (!isMicrocosmMemorySaveUnlocked()) {
        document.getElementById('premium-panel')?.classList.add('open');
        return;
      }
      const prevHtml = button ? button.innerHTML : '';
      try {
        if (button) {
          button.disabled = true;
          button.innerHTML = 'Préparation de la carte…<small>génération du HTML autonome</small>';
        }
        const { file, payload, bundle } = await buildTreeMemoryBundle(true);
        openFloatingMemoryCard(file, payload, bundle);
        if (button) {
          button.innerHTML = 'Carte prête<small>clique sur Enregistrer dans la popup</small>';
          setTimeout(() => { button.innerHTML = prevHtml; syncPaidMemoryUi(); }, 1800);
        }
        if (hint) hint.textContent = 'Carte ouverte en popup glass ; téléchargement via Enregistrer';
      } catch (err) {
        console.warn('Sauvegarde de la carte mémoire HTML impossible :', err);
        if (button) {
          button.innerHTML = 'Export impossible<small>réessaie après actualisation</small>';
          setTimeout(() => { button.innerHTML = prevHtml; syncPaidMemoryUi(); }, 1800);
        }
      } finally {
        syncPaidMemoryUi();
      }
    }

    async function createShareCardPng() {

      try {
        const day = String(growthClicksTree).padStart(3, '0');
        const shot = safeCaptureMicrocosmSnapshot();
        const img = await new Promise((resolve, reject) => {
          const im = new Image();
          im.onload = () => resolve(im);
          im.onerror = reject;
          im.src = shot;
        });

        const card = document.createElement('canvas');
        card.width = 1080;
        card.height = 1600;
        const ctx = card.getContext('2d');
        const W = card.width;
        const H = card.height;
        const safe = 54;
        const progressPct = Math.round(growthProgress * 100);
        const stage = getFibonacciStageNameTree(growthClicksTree);
        const place = weatherPlaceLabel();
        const windDeg = (THREE.MathUtils.radToDeg(microClimate.windDirection) + 360) % 360;
        const editionHash = hashGrassSeedString(`${treeUserSeedString}|${treeGameSeedString}|${growthClicksTree}`);
        const edition = (editionHash % 999999).toString().padStart(6, '0');
        const rarity = progressPct >= 100 ? 'Cycle complet' : progressPct >= 89 ? 'Canopée rare' : progressPct >= 55 ? 'Floraison' : progressPct >= 18 ? 'Jeune pousse' : 'Graine vivante';

        function rr(x, y, w, h, r) {
          ctx.beginPath();
          ctx.roundRect(x, y, w, h, r);
        }
        function fillTextFit(text, x, y, maxWidth, font, minSize = 20) {
          const parts = font.match(/^(.*?)(\d+)px(.*)$/);
          let size = parts ? parseInt(parts[2], 10) : 32;
          const before = parts ? parts[1] : '';
          const after = parts ? parts[3] : ' system-ui, sans-serif';
          ctx.font = font;
          while (ctx.measureText(text).width > maxWidth && size > minSize) {
            size -= 2;
            ctx.font = `${before}${size}px${after}`;
          }
          ctx.fillText(text, x, y);
        }
        function objectCoverDraw(image, x, y, w, h) {
          const s = Math.max(w / image.width, h / image.height);
          const iw = image.width * s;
          const ih = image.height * s;
          ctx.drawImage(image, x + (w - iw) / 2, y + (h - ih) / 2, iw, ih);
        }
        function drawHolographicLayer(x, y, w, h, alpha = 0.18) {
          ctx.save();
          rr(x, y, w, h, 54);
          ctx.clip();
          const holo = ctx.createLinearGradient(x - w * 0.2, y, x + w * 1.2, y + h);
          holo.addColorStop(0.00, 'rgba(255,244,199,0.00)');
          holo.addColorStop(0.13, `rgba(244,213,138,${alpha * 0.85})`);
          holo.addColorStop(0.27, `rgba(116,145,73,${alpha * 0.72})`);
          holo.addColorStop(0.42, `rgba(255,255,255,${alpha * 0.62})`);
          holo.addColorStop(0.58, `rgba(191,121,69,${alpha * 0.68})`);
          holo.addColorStop(0.74, `rgba(142,169,91,${alpha * 0.74})`);
          holo.addColorStop(1.00, 'rgba(255,244,199,0.00)');
          ctx.globalCompositeOperation = 'screen';
          ctx.fillStyle = holo;
          ctx.fillRect(x, y, w, h);
          ctx.globalAlpha = 0.16;
          ctx.strokeStyle = 'rgba(255,248,225,0.40)';
          ctx.lineWidth = 2;
          for (let i = -h; i < w; i += 34) {
            ctx.beginPath();
            ctx.moveTo(x + i, y + h);
            ctx.lineTo(x + i + h * 0.72, y);
            ctx.stroke();
          }
          ctx.restore();
        }
        function drawFoilDots(x, y, w, h, count = 360) {
          ctx.save();
          rr(x, y, w, h, 54);
          ctx.clip();
          for (let i = 0; i < count; i++) {
            const n = hashGrassSeedString(`${edition}-${i}`);
            const px = x + ((n & 0xffff) / 0xffff) * w;
            const py = y + (((n >>> 16) & 0xffff) / 0xffff) * h;
            const r = 0.7 + ((n >>> 9) & 7) * 0.18;
            ctx.globalAlpha = 0.10 + ((n >>> 5) & 7) * 0.012;
            ctx.fillStyle = i % 3 === 0 ? '#f4d58a' : (i % 3 === 1 ? '#fff8e1' : '#9db66a');
            ctx.beginPath();
            ctx.arc(px, py, r, 0, Math.PI * 2);
            ctx.fill();
          }
          ctx.restore();
          ctx.globalAlpha = 1;
        }
        function drawSeedGlyph(x, y, size) {
          ctx.save();
          ctx.translate(x, y);
          const cells = 9;
          const gap = size * 0.018;
          const cell = (size - gap * (cells - 1)) / cells;
          rr(-10, -10, size + 20, size + 20, 24);
          ctx.fillStyle = 'rgba(15,10,5,0.40)';
          ctx.fill();
          for (let gy = 0; gy < cells; gy++) {
            for (let gx = 0; gx < cells; gx++) {
              const edge = gx === 0 || gy === 0 || gx === cells - 1 || gy === cells - 1;
              const bit = (hashGrassSeedString(`${edition}:${gx}:${gy}`) % 100) > (edge ? 35 : 54);
              if (bit) {
                const warm = hashGrassSeedString(`${edition}:warm:${gx}:${gy}`) % 3;
                ctx.fillStyle = warm === 0 ? 'rgba(244,213,138,0.94)' : warm === 1 ? 'rgba(146,166,82,0.92)' : 'rgba(255,248,225,0.88)';
                ctx.fillRect(gx * (cell + gap), gy * (cell + gap), cell, cell);
              }
            }
          }
          ctx.restore();
        }
        function statBox(label, value, x, y, w, h) {
          rr(x, y, w, h, 22);
          ctx.fillStyle = 'rgba(23,18,11,0.44)';
          ctx.fill();
          ctx.strokeStyle = 'rgba(216,171,97,0.20)';
          ctx.lineWidth = 1.4;
          ctx.stroke();
          ctx.fillStyle = 'rgba(221,210,174,0.68)';
          ctx.font = '700 20px Inter, system-ui, sans-serif';
          ctx.fillText(label.toUpperCase(), x + 22, y + 31);
          ctx.fillStyle = 'rgba(255,248,225,0.96)';
          fillTextFit(value, x + 22, y + 67, w - 44, '850 30px Inter, system-ui, sans-serif', 18);
        }

        ctx.clearRect(0, 0, W, H);
        const bg = ctx.createRadialGradient(W * 0.2, H * 0.05, 80, W * 0.52, H * 0.48, H * 0.92);
        bg.addColorStop(0, '#6f4c25');
        bg.addColorStop(0.25, '#2f2414');
        bg.addColorStop(0.62, '#1a1f10');
        bg.addColorStop(1, '#090704');
        ctx.fillStyle = bg;
        ctx.fillRect(0, 0, W, H);

        ctx.save();
        ctx.globalAlpha = 0.42;
        const aura = ctx.createRadialGradient(W * 0.52, H * 0.42, 40, W * 0.52, H * 0.42, W * 0.72);
        aura.addColorStop(0, '#f4d58a');
        aura.addColorStop(0.25, 'rgba(125,145,68,0.80)');
        aura.addColorStop(1, 'rgba(0,0,0,0)');
        ctx.fillStyle = aura;
        ctx.fillRect(0, 0, W, H);
        ctx.restore();

        const outerX = safe;
        const outerY = 48;
        const outerW = W - safe * 2;
        const outerH = H - 88;
        ctx.shadowColor = 'rgba(0,0,0,0.62)';
        ctx.shadowBlur = 38;
        ctx.shadowOffsetY = 24;
        rr(outerX, outerY, outerW, outerH, 58);
        const frameGrad = ctx.createLinearGradient(outerX, outerY, outerX + outerW, outerY + outerH);
        frameGrad.addColorStop(0, '#fff0b8');
        frameGrad.addColorStop(0.18, '#9d6a32');
        frameGrad.addColorStop(0.42, '#293311');
        frameGrad.addColorStop(0.70, '#d6a85a');
        frameGrad.addColorStop(1, '#fff4cf');
        ctx.fillStyle = frameGrad;
        ctx.fill();
        ctx.shadowBlur = 0;
        ctx.shadowOffsetY = 0;

        rr(outerX + 10, outerY + 10, outerW - 20, outerH - 20, 50);
        const innerGrad = ctx.createLinearGradient(0, outerY, 0, outerY + outerH);
        innerGrad.addColorStop(0, 'rgba(45,33,17,0.97)');
        innerGrad.addColorStop(0.52, 'rgba(18,25,11,0.96)');
        innerGrad.addColorStop(1, 'rgba(42,26,14,0.98)');
        ctx.fillStyle = innerGrad;
        ctx.fill();
        drawHolographicLayer(outerX + 10, outerY + 10, outerW - 20, outerH - 20, 0.20);
        drawFoilDots(outerX + 10, outerY + 10, outerW - 20, outerH - 20, 460);

        // Top collector header
        ctx.fillStyle = 'rgba(255,248,225,0.78)';
        ctx.font = '800 24px Inter, system-ui, sans-serif';
        ctx.letterSpacing = '0px';
        ctx.fillText('MICROCOSM INTERACTIF 9', 106, 132);
        ctx.fillStyle = 'rgba(244,213,138,0.96)';
        ctx.font = '950 62px Inter, system-ui, sans-serif';
        ctx.fillText('ARBRE VIVANT', 104, 196);

        rr(786, 96, 176, 104, 30);
        ctx.fillStyle = 'rgba(15,10,5,0.42)';
        ctx.fill();
        ctx.strokeStyle = 'rgba(244,213,138,0.42)';
        ctx.stroke();
        ctx.textAlign = 'center';
        ctx.fillStyle = '#fff4cf';
        ctx.font = '950 42px Inter, system-ui, sans-serif';
        ctx.fillText(day, 874, 143);
        ctx.font = '700 18px Inter, system-ui, sans-serif';
        ctx.fillStyle = 'rgba(226,218,188,0.72)';
        ctx.fillText('/365 JOURS', 874, 171);
        ctx.textAlign = 'left';

        // Main image window
        const imgX = 104;
        const imgY = 238;
        const imgW = 872;
        const imgH = 735;
        rr(imgX - 12, imgY - 12, imgW + 24, imgH + 24, 44);
        ctx.fillStyle = 'rgba(255,248,225,0.08)';
        ctx.fill();
        ctx.strokeStyle = 'rgba(244,213,138,0.36)';
        ctx.lineWidth = 2;
        ctx.stroke();
        ctx.save();
        rr(imgX, imgY, imgW, imgH, 34);
        ctx.clip();
        objectCoverDraw(img, imgX, imgY, imgW, imgH);
        const vignette = ctx.createRadialGradient(imgX + imgW * 0.5, imgY + imgH * 0.46, imgW * 0.15, imgX + imgW * 0.5, imgY + imgH * 0.48, imgW * 0.78);
        vignette.addColorStop(0, 'rgba(255,255,255,0.00)');
        vignette.addColorStop(0.70, 'rgba(0,0,0,0.10)');
        vignette.addColorStop(1, 'rgba(0,0,0,0.58)');
        ctx.fillStyle = vignette;
        ctx.fillRect(imgX, imgY, imgW, imgH);
        ctx.restore();
        drawHolographicLayer(imgX, imgY, imgW, imgH, 0.10);

        // Progress capsule over image
        rr(132, 902, 816, 48, 24);
        ctx.fillStyle = 'rgba(15,10,5,0.58)';
        ctx.fill();
        ctx.strokeStyle = 'rgba(244,213,138,0.24)';
        ctx.stroke();
        rr(150, 920, 620, 13, 99);
        ctx.fillStyle = 'rgba(255,248,225,0.12)';
        ctx.fill();
        rr(150, 920, 620 * THREE.MathUtils.clamp(growthProgress, 0, 1), 13, 99);
        const pg = ctx.createLinearGradient(150, 920, 770, 920);
        pg.addColorStop(0, '#6e8f2e');
        pg.addColorStop(0.58, '#c79d43');
        pg.addColorStop(1, '#fff0a8');
        ctx.fillStyle = pg;
        ctx.fill();
        ctx.fillStyle = '#fff8e1';
        ctx.font = '850 23px Inter, system-ui, sans-serif';
        ctx.textAlign = 'right';
        ctx.fillText(`${progressPct}%`, 922, 934);
        ctx.textAlign = 'left';

        // Identity section
        ctx.fillStyle = '#fff8e1';
        ctx.font = '950 48px Inter, system-ui, sans-serif';
        fillTextFit(stage, 104, 1058, 660, '950 48px Inter, system-ui, sans-serif', 28);
        rr(780, 1002, 196, 82, 25);
        ctx.fillStyle = 'rgba(79,110,38,0.28)';
        ctx.fill();
        ctx.strokeStyle = 'rgba(216,171,97,0.20)';
        ctx.stroke();
        ctx.fillStyle = 'rgba(244,213,138,0.96)';
        ctx.font = '850 24px Inter, system-ui, sans-serif';
        ctx.textAlign = 'center';
        ctx.fillText(rarity, 878, 1052);
        ctx.textAlign = 'left';

        ctx.fillStyle = 'rgba(226,218,188,0.72)';
        ctx.font = '650 24px Inter, system-ui, sans-serif';
        fillTextFit(`Microclimat · ${place}`, 104, 1100, 830, '650 24px Inter, system-ui, sans-serif', 16);

        const sx = 104;
        const sy = 1142;
        const sw = 268;
        const sh = 92;
        statBox('Température', `${Math.round(microClimate.airTemperature)}°C`, sx, sy, sw, sh);
        statBox('Vent', `${Math.round(microClimate.windSpeed)} km/h`, sx + 294, sy, sw, sh);
        statBox('Humidité', `${Math.round(microClimate.humidity)}%`, sx + 588, sy, sw, sh);
        statBox('Nuages', `${Math.round(microClimate.cloudCover * 100)}%`, sx, sy + 112, sw, sh);
        statBox('Direction', `${Math.round(windDeg)}°`, sx + 294, sy + 112, sw, sh);
        statBox('Synchro', microClimate.liveWeatherEnabled && microClimate.lastLiveWeatherFetch ? 'LIVE' : 'AUTO', sx + 588, sy + 112, sw, sh);

        // Bottom certificate band
        rr(104, 1382, 872, 108, 30);
        ctx.fillStyle = 'rgba(15,10,5,0.46)';
        ctx.fill();
        ctx.strokeStyle = 'rgba(216,171,97,0.18)';
        ctx.stroke();
        drawSeedGlyph(126, 1400, 70);
        ctx.fillStyle = 'rgba(255,248,225,0.94)';
        ctx.font = '900 24px Inter, system-ui, sans-serif';
        ctx.fillText(`CERTIFICAT DE GRAINE #${edition}`, 222, 1425);
        ctx.fillStyle = 'rgba(226,218,188,0.72)';
        ctx.font = '650 19px Inter, system-ui, sans-serif';
        ctx.fillText('Croissance Fibonacci · golden ratio · cycle annuel vivant', 222, 1458);
        ctx.fillText('Carte générée depuis ton microcosme local', 222, 1484);

        // Final foil shine and border
        ctx.save();
        rr(outerX + 10, outerY + 10, outerW - 20, outerH - 20, 50);
        ctx.clip();
        const shine = ctx.createLinearGradient(0, 220, W, 1020);
        shine.addColorStop(0.00, 'rgba(255,255,255,0.00)');
        shine.addColorStop(0.42, 'rgba(255,255,255,0.00)');
        shine.addColorStop(0.50, 'rgba(255,248,225,0.22)');
        shine.addColorStop(0.58, 'rgba(255,255,255,0.00)');
        shine.addColorStop(1.00, 'rgba(255,255,255,0.00)');
        ctx.globalCompositeOperation = 'screen';
        ctx.fillStyle = shine;
        ctx.fillRect(0, 0, W, H);
        ctx.restore();
        rr(outerX + 22, outerY + 22, outerW - 44, outerH - 44, 44);
        ctx.strokeStyle = 'rgba(255,248,225,0.18)';
        ctx.lineWidth = 2;
        ctx.stroke();

        const a = document.createElement('a');
        a.href = card.toDataURL('image/png');
        a.download = `microcosm-collector-card-jour-${day}.png`;
        a.click();
      } catch (err) {
        console.warn('Création de card collector impossible :', err);
        captureCurrentViewPng();
      }
    }

    document.getElementById('water-tree-btn')?.addEventListener('click', (event) => {
      event.stopPropagation();
      advanceFibonacciGrowthTree();
    });
    document.getElementById('save-tree-btn')?.addEventListener('click', (event) => {
      event.stopPropagation();
      openCurrentMemoryCardViewer();
    });
    document.getElementById('export-settings-btn')?.addEventListener('click', exportMicrocosmSettings);
    document.getElementById('import-settings-btn')?.addEventListener('click', () => document.getElementById('import-settings-file')?.click());
    document.getElementById('import-settings-file')?.addEventListener('change', (event) => importMicrocosmSettingsFile(event.target.files?.[0]));
    document.getElementById('capture-card-btn')?.addEventListener('click', createShareCardHtml);
    document.getElementById('open-premium-options-btn')?.addEventListener('click', () => {
      const panel = document.getElementById('premium-panel');
      if (!panel) return;
      panel.classList.add('open');
      panel.setAttribute('aria-hidden', 'false');
    });
    function resetMemoryCardViewerTilt() {
      const viewer = document.getElementById('memory-card-viewer');
      if (!viewer) return;
      viewer.style.setProperty('--tilt-x', '0deg');
      viewer.style.setProperty('--tilt-y', '0deg');
      viewer.style.setProperty('--shine-x', '45%');
      viewer.style.setProperty('--shine-y', '18%');
    }

    function updateMemoryCardViewerTilt(event) {
      const viewer = document.getElementById('memory-card-viewer');
      if (!viewer || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
      const rect = viewer.getBoundingClientRect();
      if (!rect.width || !rect.height) return;
      const x = (event.clientX - rect.left) / rect.width;
      const y = (event.clientY - rect.top) / rect.height;
      viewer.style.setProperty('--tilt-y', `${(x - 0.5) * 16}deg`);
      viewer.style.setProperty('--tilt-x', `${(0.5 - y) * 12}deg`);
      viewer.style.setProperty('--shine-x', `${Math.round(x * 100)}%`);
      viewer.style.setProperty('--shine-y', `${Math.round(y * 100)}%`);
    }

    function syncMemoryCardFlipButton() {
      const viewer = document.getElementById('memory-card-viewer');
      const btn = document.getElementById('memory-card-flip-btn');
      if (viewer && btn) btn.textContent = viewer.classList.contains('is-flipped') ? 'Voir recto' : 'Voir verso';
    }

    document.getElementById('memory-card-viewer')?.addEventListener('pointermove', updateMemoryCardViewerTilt);
    document.getElementById('memory-card-viewer')?.addEventListener('pointerleave', resetMemoryCardViewerTilt);
    document.getElementById('memory-card-preview-panel')?.addEventListener('click', (event) => {
      if (event.target.closest?.('button, a, input, textarea, select, summary')) return;
      toggleMemoryCardFace();
    });
    document.getElementById('memory-card-viewer')?.addEventListener('click', (event) => {
      if (event.target.closest?.('button, a, input, textarea, select, summary')) return;
      event.stopPropagation();
      toggleMemoryCardFace();
    });
    document.getElementById('memory-card-flip-btn')?.addEventListener('click', (event) => {
      event.stopPropagation();
      toggleMemoryCardFace();
    });
    document.getElementById('memory-card-close-btn')?.addEventListener('click', closeFloatingMemoryCard);
    document.getElementById('memory-card-close-x')?.addEventListener('click', closeFloatingMemoryCard);
    document.getElementById('memory-card-download-btn')?.addEventListener('click', () => {
      if (!isMicrocosmMemorySaveUnlocked()) {
        document.getElementById('premium-panel')?.classList.add('open');
        syncFloatingMemoryCardActions();
        return;
      }
      if (currentMemoryCardPreview?.file) {
        downloadMemoryCardFile(currentMemoryCardPreview.file);
        const btn = document.getElementById('memory-card-download-btn');
        if (btn) {
          const prev = btn.textContent;
          btn.textContent = 'Mémoire enregistrée';
          setTimeout(() => { btn.textContent = prev || 'Enregistrer'; syncFloatingMemoryCardActions(); }, 1500);
        }
      }
    });
    document.getElementById('memory-card-open-ar-btn')?.addEventListener('click', () => {
      const url = currentMemoryCardPreview?.payload?.arLaunchUrl || currentMemoryCardPreview?.bundle?.arLaunchUrl;
      if (url) window.open(url, '_blank', 'noopener');
    });
    document.getElementById('memory-card-modal')?.addEventListener('click', (event) => {
      if (event.target?.id === 'memory-card-modal') closeFloatingMemoryCard();
    });
    window.addEventListener('keydown', (event) => {
      if (event.key === 'Escape' && document.getElementById('memory-card-modal')?.classList.contains('open')) closeFloatingMemoryCard();
    });
    syncProductPricingUi();
    syncPaidMemoryUi();
    updateStorageWarningText();
    document.getElementById('cycle-keep-btn')?.addEventListener('click', () => {
      closeCycleModalTree({ remember: true });
    });
    document.getElementById('cycle-card-btn')?.addEventListener('click', () => {
      closeCycleModalTree({ remember: true });
      document.getElementById('capture-card-btn')?.click();
    });
    document.getElementById('cycle-cut-btn')?.addEventListener('click', () => {
      resetFibonacciGrowthTree();
    });
    document.getElementById('cycle-premium-btn')?.addEventListener('click', () => {
      closeCycleModalTree({ remember: true });
      document.getElementById('premium-panel')?.classList.add('open');
    });
    document.getElementById('cycle-modal')?.addEventListener('click', (event) => {
      if (event.target?.id === 'cycle-modal') closeCycleModalTree({ remember: true });
    });

    document.getElementById('premium-cta')?.addEventListener('click', () => {
      document.getElementById('premium-panel')?.classList.toggle('open');
    });
    document.getElementById('presentation-fullscreen-btn')?.addEventListener('click', requestMicrocosmFullscreenWithoutHud);


    // Cadrage initial : splashscreen dans l'herbe. Après le premier clic,
    // la caméra reprend le cadrage de croissance normal.
    if (openingGrassSplashActive) setOpeningGrassSplashCamera();
    else updateTreeCameraFrame(0.016, true);

    // Mouse interaction for grass
    const raycaster = new THREE.Raycaster();
    const mouseNDC = new THREE.Vector2();
    const grassPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
    const hitPoint = new THREE.Vector3();
    window.addEventListener('mousemove', (e) => {
      mouseNDC.set((e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
      raycaster.setFromCamera(mouseNDC, camera);
      // Interaction herbe en coordonnées locales de l'îlot : même si le micro-monde flotte,
      // le vent/push restent centrés au bon endroit sur la surface.
      grassPlane.constant = -islandRoot.position.y;
      if (raycaster.ray.intersectPlane(grassPlane, hitPoint)) {
        islandRoot.worldToLocal(hitPoint);
        mouseWorld.value.copy(hitPoint);
      }
    });
    window.addEventListener('mouseleave', () => {
      mouseWorld.value.set(99999, 0, 99999);
    });
    // ───── Mise à jour du soleil (océan et ciel) ─────
    function updateSun(elevation, azimuth) {
      const phi = THREE.MathUtils.degToRad(90 - elevation);
      const theta = THREE.MathUtils.degToRad(azimuth);
      const pos = new THREE.Vector3().setFromSphericalCoords(1, phi, theta);
      sunLight.position.copy(pos).multiplyScalar(100);
      sunDir.value.copy(pos).normalize();
      if (sky?.sunPosition?.value) sky.sunPosition.value.copy(pos);
    }
    updateSun(params.sunElevation, params.sunAzimuth);
    await renderer.computeAsync(initWaves);
    await renderer.computeAsync(computeInitGrass);
    updateMicroClimateFromBrowserClock(new Date());
    applyMicroClimateToWorld(0);
    updateWeatherHUD(true);
    recordMicrocosmVisit(new Date());
    function updateGrassIslandScale() {
      const p = THREE.MathUtils.clamp(growthProgress, 0, 1);
      const eased = p * p * (3 - 2 * p);
      // La terre doit rester légèrement plus ample que l'herbe.
      // On sépare donc le rayon du sol (terre) de celui de la canopée d'herbe.
      const earthRadius = THREE.MathUtils.lerp(14.5, 26.0, eased) * grassSeedConfig.size;
      // L'herbe va presque jusqu'au bord de la terre : il ne reste qu'une fine lèvre organique.
      const grassRadius = earthRadius * MICROCOSM_CONFIG.island.grassToEarthRatio;
      grassVisibleRadius.value = grassRadius;
      const prevRadius = groundRadiusGrass.value;
      groundRadiusGrass.value = earthRadius;
      // La longueur et la largeur de l'herbe grandissent avec l'arbre,
      // tout en gardant les réglages utilisateur comme multiplicateurs.
      bladeHeight.value = grassBladeLengthUser * THREE.MathUtils.lerp(0.38, 1.12, eased);
      bladeWidth.value = grassBladeWidthUser * THREE.MathUtils.lerp(0.58, 1.08, eased);
      // Courbure de l'îlot réduite : le kokedama reste organique, sans faire plonger
      // la terre sous l'océan à l'âge adulte.
      islandCurveStrength.value = THREE.MathUtils.lerp(0.0035, 0.0018, eased);
      oceanMatchedCurveStrength.value = islandCurveStrength.value;
      // On découpe un peu au-delà de la terre : l'eau vient mourir au bord au lieu de passer dessous/dedans.
      // Collision océan/terre : rayon moyen aligné sur l'îlot organique + un offset de rivage.
      // Le liseré visible est non circulaire via les particules rebuildShoreOrganicFoamParticles().
      oceanIslandCutRadius.value = earthRadius + MICROCOSM_CONFIG.island.earthEdgeMargin + MICROCOSM_CONFIG.island.shoreMargin * 0.72;
      oceanIslandCutFeather.value = THREE.MathUtils.lerp(1.35, 2.25, eased);
      oceanShoreCalmDistance.value = THREE.MathUtils.lerp(3.8, 6.4, eased);
      if (!islandEarthMesh || Math.abs(prevRadius - earthRadius) > 0.85) {
        rebuildIslandEarth(earthRadius);
        if (grassGround?.geometry) grassGround.geometry.dispose();
        grassGround.geometry = buildCurvedGrassGroundGeometry();
      }
    }
    updateGrassIslandScale();
    // ───── Animation loop unifiée ─────
    const clock = new THREE.Clock();
    async function animate() {
      const dt = Math.min(clock.getDelta(), 0.05);
      if (document.hidden && !renderer.xr.isPresenting) return;
      updateMicroClimateFromBrowserClock(new Date());
      if (microClimate.liveWeatherEnabled && Date.now() - microClimate.lastLiveWeatherFetch > 10 * 60 * 1000) {
        requestLiveMicroWeather();
      }
      applyMicroClimateToWorld(clock.elapsedTime);
      updateWeatherHUD();
      const windRT = getWindRuntime();
      const season = getSeasonFromDayTree(Math.max(1, growthClicksTree));
      if (seasonModeTree) Object.assign(treeSeasonState, season);
      else Object.assign(treeSeasonState, { name: 'neutre', leafVisibility: 1, leafScale: 1, leafHue: new THREE.Color(0x2f7d32), fallingLeaves: 0.03 });
      if (oceanCycleState.tideEnabled) {
        const dayPhase = (growthClicksTree / FIB_GROWTH_TOTAL_CLICKS_TREE) * Math.PI * 2 * PHI;
        const wind01 = THREE.MathUtils.clamp(globalWind.speed / 50, 0, 1);
        const tide = Math.sin(clock.elapsedTime * oceanCycleState.tideSpeed * PHI + dayPhase)
          * oceanCycleState.tideAmplitude * (0.65 + wind01 * 0.45);
        oceanCycleState.lastTide = tide;
        islandMotionState.lastTide = tide;
        oceanMesh.position.y = oceanCycleState.baseLevel + tide;
      } else {
        oceanCycleState.lastTide = 0;
        islandMotionState.lastTide = 0;
        oceanMesh.position.y = oceanCycleState.baseLevel;
      }
      updateFloatingIslandMotion(clock.elapsedTime, windRT);
      if (shoreFadePoints) {
        shoreFadePoints.material.opacity = shoreFadeState.enabled
          ? shoreFadeState.opacity * (0.76 + 0.24 * Math.sin(clock.elapsedTime * (0.8 + globalWind.speed * 0.025) + microClimate.tide01))
          : 0;
        // Pas de rotation indépendante : le rivage reste attaché au tracé de l’îlot.
        shoreFadePoints.rotation.set(0, 0, 0);
      }
      vrPlayableRing.visible = vrPlayableState.visible;
      updateIcosahedron(dt);
      // Compute updates for grass and ocean. V8.21: on espace les passes les plus coûteuses.
      if ((frameCountTree % MICROCOSM_RUNTIME_PERF.grassUpdateEvery) === 0) renderer.compute(computeUpdateGrass);
      renderer.compute(computeOceanCoarse);
      if ((frameCountTree % MICROCOSM_RUNTIME_PERF.oceanFineUpdateEvery) === 0) renderer.compute(computeOceanFine);
      // Update tree growth
      if (isGrowing && growthProgress < targetGrowth) {
        growthProgress += dt * growthSpeedTree;
        if (growthProgress >= targetGrowth) {
          growthProgress = targetGrowth;
          if (growthProgress >= 1) isGrowing = false;
        }
      }
      updateGrassIslandScale();
      progressBarTree.style.width = `${growthProgress * 100}%`;
      if (growthSliderTree && !manualGrowthModeTree) growthSliderTree.value = Math.round(growthProgress * 1000) / 10;
      updateTreeCameraFrame(dt);
      controls.update();
      updateSaplingStagesTree(growthProgress, clock.elapsedTime);
      const oakScaleTree = matureOakScaleTree(growthProgress);
      treeGroup.scale.setScalar(oakScaleTree);
      // Update tree branches
      for (let bi = 0; bi < branches.length; bi++) {
        const b = branches[bi];
        const branchT = (growthProgress - b.growthStart) / (b.growthEnd - b.growthStart);
        if (branchT <= 0) {
          if (b.mesh.visible) b.mesh.visible = false;
          continue;
        }
        if (!b.mesh.visible) b.mesh.visible = true;
        const clamped = branchT > 1 ? 1 : branchT;
        const eased = 1 - Math.pow(1 - clamped, 2.5);
        const visibleSegments = Math.max(0, Math.floor(eased * b.segments));
        const indexCount = visibleSegments * b.radialSegments * 6;
        if (indexCount <= 0) {
          b.mesh.visible = false;
          continue;
        }
        b.mesh.geometry.setDrawRange(0, indexCount);
        if (eased > 0.5 && b.depth >= 2) {
          const timeT = clock.elapsedTime;
          const phaseZ = timeT * (0.38 + windRT.speedMul * 0.42) + b.depth * 1.2 + b.growthStart * 10;
          const ownSwayZ = Math.sin(phaseZ) * 0.004 * b.depth * windRT.branchAmp * (0.45 + Math.abs(windRT.dirX));
          const ownSwayX = Math.sin(timeT * (0.32 + windRT.speedMul * 0.36) + b.growthStart * 5) * 0.002 * b.depth * windRT.branchAmp * (0.45 + Math.abs(windRT.dirZ));
          let totalSwayZ = ownSwayZ;
          let totalSwayX = ownSwayX;
          let parent = b.parentBranch;
          while (parent && parent.depth >= 2) {
            const pPhaseZ = timeT * (0.38 + windRT.speedMul * 0.42) + parent.depth * 1.2 + parent.growthStart * 10;
            totalSwayZ += Math.sin(pPhaseZ) * 0.004 * parent.depth * windRT.branchAmp * (0.45 + Math.abs(windRT.dirX));
            totalSwayX += Math.sin(timeT * (0.32 + windRT.speedMul * 0.36) + parent.growthStart * 5) * 0.002 * parent.depth * windRT.branchAmp * (0.45 + Math.abs(windRT.dirZ));
            parent = parent.parentBranch;
          }
          const sp = b.startPos;
          b.mesh.position.set(0, 0, 0);
          b.mesh.rotation.set(0, 0, 0);
          b.mesh.position.sub(sp);
          b.mesh.rotation.z = totalSwayZ;
          b.mesh.rotation.x = totalSwayX;
          b.mesh.position.applyEuler(b.mesh.rotation);
          b.mesh.position.add(sp);
        } else if (b.depth >= 2) {
          b.mesh.position.set(0, 0, 0);
          b.mesh.rotation.set(0, 0, 0);
        }
      }
      // Update guaranteed canopy patches (attached to branches, season-aware).
      for (let ci = 0; ci < canopyPatchesArr.length; ci++) {
        const cp = canopyPatchesArr[ci];
        const b = cp.branch;
        const branchLocalRaw = (growthProgress - b.growthStart) / Math.max(0.0001, b.growthEnd - b.growthStart);
        const branchLocal = THREE.MathUtils.clamp(branchLocalRaw, 0, 1);
        const easedBranchLocal = 1 - Math.pow(1 - branchLocal, 2.5);
        if (branchLocal <= 0.025 || easedBranchLocal < cp.branchT - 0.075) {
          cp.mesh.visible = false;
          continue;
        }
        const visibleT = Math.min(cp.branchT, Math.max(0.08, easedBranchLocal - 0.012));
        cp.mesh.visible = true;
        const pos = b.curve.getPointAt(visibleT).add(cp.localOffset);
        if (b.depth >= 2) {
          pos.sub(b.startPos).applyEuler(b.mesh.rotation).add(b.startPos);
        }
        const timeT = clock.elapsedTime;
        pos.x += Math.sin(timeT * 0.7 * windRT.speedMul + cp.swayOffset) * 0.018 * windRT.leafAmp * windRT.dirX;
        pos.z += Math.cos(timeT * 0.6 * windRT.speedMul + cp.swayOffset) * 0.016 * windRT.leafAmp * windRT.dirZ;
        cp.mesh.position.copy(pos);
        cp.mesh.rotation.set(
          Math.sin(timeT * 0.35 + cp.swayOffset) * 0.08 * windRT.leafAmp,
          cp.ringAngle + timeT * 0.05 * windRT.speedMul,
          Math.cos(timeT * 0.31 + cp.swayOffset) * 0.06 * windRT.leafAmp
        );
        let seasonGate = 1;
        if (seasonModeTree) {
          seasonGate = cp.seasonSeed <= treeSeasonState.leafVisibility ? 1 : 0;
          if (cp.priority >= 2) seasonGate = Math.max(seasonGate, treeSeasonState.name === 'hiver' ? 0.82 : 1.0);
          else seasonGate = Math.max(seasonGate, treeSeasonState.name === 'hiver' ? 0.64 : 0.92);
        }
        const growScale = THREE.MathUtils.smoothstep(branchLocal, Math.max(0, cp.branchT - 0.10), cp.branchT + 0.03);
        const s = cp.baseScale * treeSeasonState.leafScale * seasonGate * Math.max(0.18, growScale);
        cp.mesh.scale.setScalar(s);
        if (cp.mesh.material?.color) cp.mesh.material.color.lerp(treeSeasonState.leafHue, 0.16);
      }

      // Update leaves
      const leafDirtyFlagsTree = new Uint8Array(leafInstancedMeshesTree.length);
      if ((frameCountTree & 15) === 0) {
        for (let mi = 0; mi < leafInstanceMatsTree.length; mi++) {
          leafInstanceMatsTree[mi].color.lerp(treeSeasonState.leafHue, 0.18);
        }
      }
      for (let li = 0; li < leavesArr.length; li++) {
        const l = leavesArr[li];
        let visibleBranchTForLeafTree = l.branchT;
        if (l.branch) {
          const branchLocalRaw = (growthProgress - l.branch.growthStart) / Math.max(0.0001, l.branch.growthEnd - l.branch.growthStart);
          const branchLocal = THREE.MathUtils.clamp(branchLocalRaw, 0, 1);
          const easedBranchLocal = 1 - Math.pow(1 - branchLocal, 2.5);
          // Feuilles terminales : elles restent collées au bout actuellement visible.
          // Les autres feuilles attendent encore que leur segment existe, pour éviter le flottement.
          if (l.terminalFollower) {
            if (branchLocal <= 0.035) {
              if (l.currentScale > 0) {
                l.currentScale = 0;
                _leafMatrixTree.makeScale(0.001, 0.001, 0.001);
                _leafMatrixTree.setPosition(l.originalPos.x, l.originalPos.y, l.originalPos.z);
                l.instancedMesh.setMatrixAt(l.instanceIdx, _leafMatrixTree);
                leafDirtyFlagsTree[l.colorIdx] = 1;
              }
              continue;
            }
            visibleBranchTForLeafTree = Math.min(l.branchT, Math.max(0.055, easedBranchLocal - 0.018));
          } else if (branchLocal < l.branchT - 0.055) {
            if (l.currentScale > 0) {
              l.currentScale = 0;
              _leafMatrixTree.makeScale(0.001, 0.001, 0.001);
              _leafMatrixTree.setPosition(l.originalPos.x, l.originalPos.y, l.originalPos.z);
              l.instancedMesh.setMatrixAt(l.instanceIdx, _leafMatrixTree);
              leafDirtyFlagsTree[l.colorIdx] = 1;
            }
            continue;
          }
        }
        const leafT = (growthProgress - l.growthStart) * 10.0;
        if (leafT <= 0) {
          if (l.currentScale > 0) {
            l.currentScale = 0;
            _leafMatrixTree.makeScale(0.001, 0.001, 0.001);
            _leafMatrixTree.setPosition(l.originalPos.x, l.originalPos.y, l.originalPos.z);
            l.instancedMesh.setMatrixAt(l.instanceIdx, _leafMatrixTree);
            leafDirtyFlagsTree[l.colorIdx] = 1;
          }
          continue;
        }
        const t = leafT > 1 ? 1 : leafT;
        const omt = 1 - t;
        const elastic = t < 1 ? 1 - omt * omt * omt * Math.cos(t * 2.5133) : 1;
        let seasonalGate = 1;
        let prioritySeasonScale = 1;
        if (seasonModeTree) {
          seasonalGate = l.seasonSeed <= treeSeasonState.leafVisibility ? 1 : 0;
          if (l.tipPriority) {
            // Garantie légère : une branche garde des bourgeons/quelques feuilles
            // terminales, mais l'automne/hiver restent réellement moins feuillus.
            let minTipGate = 0;
            if (treeSeasonState.name === 'printemps') minTipGate = l.tipPriority >= 2 ? 0.55 : 0.42;
            else if (treeSeasonState.name === 'été') minTipGate = l.tipPriority >= 2 ? 0.70 : 0.55;
            else if (treeSeasonState.name === 'automne') minTipGate = l.tipPriority >= 2 ? 0.34 : 0.22;
            else minTipGate = l.tipPriority >= 2 ? 0.18 : 0.10;
            seasonalGate = Math.max(seasonalGate, minTipGate);

            if (treeSeasonState.name === 'hiver') prioritySeasonScale = 0.58;
            else if (treeSeasonState.name === 'automne') prioritySeasonScale = 0.76;
          }
        }
        const scale = elastic * l.targetScale * treeSeasonState.leafScale * prioritySeasonScale * seasonalGate;
        if (scale < 0.001) {
          if (l.currentScale > 0) {
            l.currentScale = 0;
            _leafMatrixTree.makeScale(0.001, 0.001, 0.001);
            _leafMatrixTree.setPosition(l.originalPos.x, l.originalPos.y, l.originalPos.z);
            l.instancedMesh.setMatrixAt(l.instanceIdx, _leafMatrixTree);
            leafDirtyFlagsTree[l.colorIdx] = 1;
          }
          continue;
        }
        if (t >= 1 && l.currentScale === scale && (li & 3) !== (frameCountTree & 3)) continue;
        const timeT = clock.elapsedTime;
        const phase = timeT * l.swaySpeed * windRT.speedMul + l.swayOffset;
        _tempEulerTree.set(
          l.baseRotX + Math.sin(phase * 0.5) * 0.1 * windRT.leafAmp,
          l.baseRotY + timeT * 0.15 * windRT.speedMul,
          l.baseRotZ + Math.cos(phase * 0.4) * 0.06 * windRT.leafAmp
        );
        _tempQuatTree.setFromEuler(_tempEulerTree);
        _tempScaleTree.set(scale, scale, scale);
        if (l.branch) {
          const branchPoint = l.branch.curve.getPointAt(visibleBranchTForLeafTree);
          l.originalPos.copy(branchPoint).add(l.localOffset);
          if (l.branch.depth >= 2) {
            l.originalPos.sub(l.branch.startPos).applyEuler(l.branch.mesh.rotation).add(l.branch.startPos);
          }
        }
        _tempVecTree.set(
          l.originalPos.x + Math.sin(phase) * 0.032 * windRT.leafAmp * windRT.dirX,
          l.originalPos.y + Math.sin(phase * 0.7) * 0.016 * windRT.leafAmp,
          l.originalPos.z + Math.cos(phase * 0.85) * 0.026 * windRT.leafAmp * windRT.dirZ
        );
        _leafMatrixTree.compose(_tempVecTree, _tempQuatTree, _tempScaleTree);
        l.instancedMesh.setMatrixAt(l.instanceIdx, _leafMatrixTree);
        l.currentScale = scale;
        leafDirtyFlagsTree[l.colorIdx] = 1;
      }
      for (let i = 0; i < leafInstancedMeshesTree.length; i++) {
        if (leafDirtyFlagsTree[i]) leafInstancedMeshesTree[i].instanceMatrix.needsUpdate = true;
      }
      // Update flowers
      const flowerDirtyFlagsTree = new Uint8Array(flowerInstancedMeshesTree.length);
      let centerDirtyTree = false;
      const petalAngleStep = Math.PI * 2 / 5;
      const petalCos = [1, 0.309, -0.809, -0.809, 0.309];
      const petalSin = [0, 0.951, 0.588, -0.588, -0.951];
      for (let fi = 0; fi < flowersArr.length; fi++) {
        const f = flowersArr[fi];
        const flowerT = (growthProgress - f.growthStart) * 8.3333;
        const t = flowerT <= 0 ? 0 : (flowerT > 1 ? 1 : flowerT);
        if (t >= 1 && f.currentScale === f.targetScale && (fi & 3) !== (frameCountTree & 3)) continue;
        const omt = 1 - t;
        const elastic = t > 0 && t < 1 ? 1 - omt * omt * omt * Math.cos(t * 1.885) : t;
        const scale = elastic * f.targetScale;
        const s = scale < 0.001 ? 0.001 : scale;
        const rotY = f.baseRotY + clock.elapsedTime * 0.15 * windRT.speedMul;
        for (let pi = 0; pi < f.petalIndices.length; pi++) {
          _tempVecTree.set(f.position.x + petalCos[pi] * 0.1 * s, f.position.y, f.position.z + petalSin[pi] * 0.1 * s);
          _tempEulerTree.set(f.baseRotX, rotY - pi * petalAngleStep, f.baseRotZ + 0.5);
          _tempQuatTree.setFromEuler(_tempEulerTree);
          _tempScaleTree.set(s, s, s);
          _leafMatrixTree.compose(_tempVecTree, _tempQuatTree, _tempScaleTree);
          f.instancedMesh.setMatrixAt(f.petalIndices[pi], _leafMatrixTree);
        }
        flowerDirtyFlagsTree[f.colorIdx] = 1;
        _tempVecTree.copy(f.position);
        _tempEulerTree.set(f.baseRotX, rotY, f.baseRotZ);
        _tempQuatTree.setFromEuler(_tempEulerTree);
        _tempScaleTree.set(s, s, s);
        _leafMatrixTree.compose(_tempVecTree, _tempQuatTree, _tempScaleTree);
        flowerCenterIMTree.setMatrixAt(f.centerIdx, _leafMatrixTree);
        centerDirtyTree = true;
        f.currentScale = scale;
      }
      for (let i = 0; i < flowerInstancedMeshesTree.length; i++) {
        if (flowerDirtyFlagsTree[i]) flowerInstancedMeshesTree[i].instanceMatrix.needsUpdate = true;
      }
      if (centerDirtyTree) flowerCenterIMTree.instanceMatrix.needsUpdate = true;
      // Falling leaves for tree
      if (growthProgress > 0.18 && treeSeasonState.fallingLeaves > 0.02) {
        leafTimerTree += dt * (0.4 + treeSeasonState.fallingLeaves * 3.0);
        if (leafTimerTree > 1.2) {
          leafTimerTree = 0;
          spawnFallingLeafTree();
        }
      }
      let fallingDirtyTree = false;
      let fallingColorDirtyTree = false;
      for (let i = 0; i < FALLING_LEAF_POOL_SIZE_TREE; i++) {
        const fl = fallingLeafPoolTree[i];
        if (!fl.active) continue;
        const pos = fl.position;
        const vel = fl.velocity;
        pos.x += vel.x + Math.sin(clock.elapsedTime * 2 * windRT.speedMul + fl.swayOffset) * 0.01 * windRT.fallAmp * windRT.dirX;
        pos.y += vel.y;
        pos.z += vel.z + Math.cos(clock.elapsedTime * 1.7 * windRT.speedMul + fl.swayOffset) * 0.01 * windRT.fallAmp * windRT.dirZ;
        fl.rotation.x += fl.rotSpeed.x;
        fl.rotation.y += fl.rotSpeed.y;
        fl.rotation.z += fl.rotSpeed.z;
        vel.y -= 0.0001;
        if (pos.y < 0.1) {
          fl.life -= dt * 0.5;
          if (fl.life <= 0) {
            fl.active = false;
            _leafMatrixTree.makeScale(0.001, 0.001, 0.001);
            _leafMatrixTree.setPosition(0, -100, 0);
            fallingLeafIMTree.setMatrixAt(fl.idx, _leafMatrixTree);
            fallingDirtyTree = true;
            continue;
          } else {
            fl.opacity = fl.life;
            const colorIdx = Math.floor(treeRandom() * leafColorsArr.length);
            const c = new THREE.Color(leafColorsArr[colorIdx]).multiplyScalar(fl.life);
            fallingLeafIMTree.setColorAt(fl.idx, c);
            fallingColorDirtyTree = true;
          }
        }
        _tempEulerTree.set(fl.rotation.x, fl.rotation.y, fl.rotation.z);
        _tempQuatTree.setFromEuler(_tempEulerTree);
        _tempScaleTree.set(fl.scale, fl.scale, fl.scale);
        _tempVecTree.copy(pos);
        _leafMatrixTree.compose(_tempVecTree, _tempQuatTree, _tempScaleTree);
        fallingLeafIMTree.setMatrixAt(fl.idx, _leafMatrixTree);
        fallingDirtyTree = true;
      }
      if (fallingDirtyTree) fallingLeafIMTree.instanceMatrix.needsUpdate = true;
      if (fallingColorDirtyTree) fallingLeafIMTree.instanceColor.needsUpdate = true;
      // Update pollen particles for tree
      if ((frameCountTree & 3) === 0) {
        const positions = particlesTree.geometry.attributes.position.array;
        for (let i = 0; i < particleCountTree; i++) {
          const pd = particleDataTree[i];
          const ang = clock.elapsedTime * pd.speed * 0.1 * windRT.speedMul + pd.offset;
          positions[i * 3] = Math.cos(ang) * pd.radius + windRT.dirX * Math.sin(clock.elapsedTime * 0.6 * windRT.speedMul + pd.offset) * windRT.leafAmp * 0.18;
          positions[i * 3 + 1] = pd.baseY + Math.sin(clock.elapsedTime * pd.speed * 0.5 * windRT.speedMul + pd.offset) * 1.5;
          positions[i * 3 + 2] = Math.sin(ang) * pd.radius + windRT.dirZ * Math.cos(clock.elapsedTime * 0.6 * windRT.speedMul + pd.offset) * windRT.leafAmp * 0.18;
        }
        particlesTree.geometry.attributes.position.needsUpdate = true;
      }
      particleMatTree.opacity = 0.15 + Math.sin(clock.elapsedTime * 0.5) * 0.1 + growthProgress * 0.25;
      pointLight.intensity = 0.2 + growthProgress * 0.3 + Math.sin(clock.elapsedTime * 1.5) * 0.08;
      pointLight.position.y = 7 + growthProgress * 22;
      frameCountTree++;
      renderer.render(scene, camera);
      if ((frameCountTree % MICROCOSM_RUNTIME_PERF.statsUpdateEvery) === 0) stats.update();
    }
    renderer.setAnimationLoop(animate);
    window.addEventListener('resize', () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, renderer.xr.isPresenting ? vrRuntimeState.comfortPixelRatio : MICROCOSM_RUNTIME_PERF.maxPixelRatio));
      renderer.setSize(window.innerWidth, window.innerHeight);
      requestAnimationFrame(updateHudStackLayout);
    });
    requestAnimationFrame(updateHudStackLayout);
  </script>
				</div>
				</div>
				</div>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/36446-2/">//*</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Paper plane</title>
		<link>https://presentcomposedesign.fr/paper-plane-mode_for_arrival-space/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Fri, 27 Mar 2026 01:19:08 +0000</pubDate>
				<category><![CDATA[[ia]]]></category>
		<category><![CDATA[[VR]]]></category>
		<category><![CDATA[Design d'espace]]></category>
		<category><![CDATA[Design produit]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=36135</guid>

					<description><![CDATA[<p>Click here &#x1F861; from your VR headset Paper Plane in VR ᯅ Plein écran &#x1F5D6; Paper Plane Mon tout premier plugin Arrival.Space. Un avatar universel pour le web immersif. Le concept Paper Plane est parti d&#8217;une question simple : et si le meilleur avatar était aussi le plus universel ? Pas de personnage genré, pas de corps humanoïde. Juste un avion en papier, instinctif et léger, qui traverse librement chaque monde d&#8217;Arrival.Space. Pourquoi un avion en papier ? Tout le monde en a plié un. Tout le monde en a lancé un. C&#8217;est peut-être l&#8217;objet le plus démocratique qui existe. En tant qu&#8217;avatar, il efface toute friction d&#8217;identification : pas d&#8217;âge, pas de genre, pas de skin à choisir. Du mouvement pur, et une liberté totale d&#8217;exploration. De SplatGate à Paper Plane Ce projet s&#8217;appuie sur toute l&#8217;expertise en Gaussian Splatting que j&#8217;ai construite avec SplatGate. Paper Plane, c&#8217;est mon premier pas dans l&#8217;écosystème Arrival.Space, un nouveau terrain pour pousser les limites du web immersif, du spatial computing et du vibe coding. 🧊 Gaussian Splatting 🌐 Web Immersif Full VR available Experience -ᯅ- ✔️ Explorer les univers gagnants Essayer Paper Plane Soutenir / Remercier Organisé par Arrival.Space &#038; CHOICE DAO</p>
<p>Cet article <a href="https://presentcomposedesign.fr/paper-plane-mode_for_arrival-space/">Paper plane</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="36135" class="elementor elementor-36135">
						<header class="elementor-section elementor-top-section elementor-element elementor-element-f1e5d23 elementor-section-full_width elementor-section-height-default elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no" data-id="f1e5d23" data-element_type="section" data-e-type="section">
						<div class="elementor-container elementor-column-gap-no">
					<header class="elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-8a6a45b" data-id="8a6a45b" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<div class="elementor-element elementor-element-035d3b0 elementor-widget__width-inherit elementor-widget elementor-widget-html" data-id="035d3b0" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!--
// PAPER PLANE MODE for Arrival space, © All rights reserved.
//
//                                                                              ++==--=+++-=
//                                                                     +++++-:::::-=++=:. .=
//                                                        +++++=++============++*+=:....  .=
//                                              =+++++===================+**#*=-:::.... .:-=
//                                 =+++++++++===========++++**********###*==-::::::..:-====
//                          %%%##********************************##%%%#**+==-::::-=========
//                     **************************************#%%%%%*+**++++==--=+++++++++=-
//               **************************************###%%%%##*************#*++++++++++=
//            ****************########################%%%%%**************######*+++++++++=
//                 ####***#######################%%%%%##*************############*+++++++
//                       ####################%%%%%##**************#################*++++=
//                              @#######%%%%%%%***************######################***+-
//                                   @%%%%##**************############################*+
//                                    %%%###########**#################################*
//                                    %%%#########*####################################
//                                    @%%%####**-     @%#%###########################
//                                    @%%%#+                 %%##################%
//                                                                %%%%%%%%%%%%#%
//                                                                    @%%%%%%@
//
// Dev-Xprmnts · VR_Xprmnts, Present Compound Design           ║
// ║ Alban DESBARAX - Designer 360° / Creative Technologist    ║
// ║ Toulouse, FRANCE                                          ║
// ║                                                           ║
// ║   · Expertise & Support 2D, 3D, AR, VR, XR, AI            ║
// ║   · Product Design & Industrial Design                    ║
// ║   · Digital communication                                 ║
// ║   · Immersive experiences  -ᯅ- ✔️                       ║
// ║                                                           ║
// · 🡺 https://presentcomposedesign.fr/
//
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#*++==------===**%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#++-::::::::::::::::::::::-=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+-::::::::::::::::::::::::::::::::=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@#+::::::::::::::::::::::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@%+:::::::::::::::::=++***+=-::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@#=::::::::::::::::+%@@@@@@@@@@%+-:::::::::::::::::::=#@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@#-::::::::::::::::#@@@@@@@@@@@@@@@@%-:::::::::::::::::::=#@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@%=::::::::::::::::*@@@@@@**==-=+##@@@@@*-:::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@=:::::::::::::::::%@@@@@+::::::::::-*@@@@#-::::::::::::::::::::=@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@#::::::::::::::::::%@@@@=::::::::::::::-%@@@#-:::::::::::::::::::::#@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@=::::::::::::::::::*@@@@=::::::::::::::::-%@@@*::::::::::::::::::::::=@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@%-::::::::::::::::::-@@@@=::::::::::::::::::-%@@@-::::::::::::::::::::::-%@@@@@@@@@@@@@
// @@@@@@@@@@@@#::::::::::::::::::::*@@@#::::::::::::::::::::=@@@*::::::::::::::::::::::::*@@@@@@@@@@@@
// @@@@@@@@@@@#:::::::::::::::::::::%@@@+::::::::::::::::::::-@@@%:::::::::::::::::::::::::*@@@@@@@@@@@
// @@@@@@@@@@+::::::::::::::::::::::@@@@-:::::::::::::::::::::%@@@::::::::::::::::::::::::::*@@@@@@@@@@
// @@@@@@@@@*:::::::::::::::::::::::@@@@=:::::::::::::::::::::@@@@:::::::::::::::::::::::::::*@@@@@@@@@
// @@@@@@@@%::::::::::::::::::::::::@@@@*:::::::::::::::::::::@@@%::::::::::::::::::::::::::::*@@@@@@@@
// @@@@@@@%:::::::::::::::::::::::::@@@@%::::::::::::::::::::+@@@*:::::::::::::::::::::::::::::%@@@@@@@
// @@@@@@@::::::::::::::::::::::::::@@@@@*::::::::::::::::::-@@@@::::::::::::::::::::::::::::::-@@@@@@@
// @@@@@@=::::::::::::::::::::::::::@@@@@@*::::::::::::::::-@@@@+:::::::::::::::::::::::::::::::*@@@@@@
// @@@@@%:::::::::::::::::::::::::::@@@@@@@*::::::::::::::-%@@@#:::::::::::::::::::::::::::::::::*@@@@@
// @@@@@::::::::::::::::::::::::::::@@@@@@@@@*::::::::::+@@@@@*::::::::::::::::::::::::::::::::::-@@@@@
// @@@@*::::::::::::::::::::::::::::@@@#=@@@@@@%#*+=**%@@@@@@=::::::::::::::::::::::::::::::::::::*@@@@
// @@@@:::::::::::::::::::::::::::::@@@#::*@@@@@@@@@@@@@@@@*:::::::::::::::::::::::::::::::::::::::@@@@
// @@@*:::::::::::::::::::::::::::::@@@#::::=*@@@@@@@@@%#=:::::::::::::::::::::::::::::::::::::::::#@@@
// @@@=:::::::::::::::::::::::::::::@@@#::::::::-==+=-:::::::::::::::::::::::::::::::::::::::::::::=@@@
// @@@::::::::::::::::::::::::::::::@@@#:::::::::::::::::::::::::::::::::::::::::::::::::--:::::::::@@@
// @@*::::::::::::::::::::::::::::::@@@#:::::::::+######*:::::::::::::::::::::::::::::::+@=:::::::::#@@
// @@*::::::::::::::::::::::::::::::@@@#:::--::::===+*+==:::::=-:::::::=-::::::-::::::::+@=:::::::::+@@
// @@:::::::::::::::::::::::::::::::@@@#::+@#@@::-#@@%@%=::=@@@@+:::+@@@@@+-::@@@@%*-::#@@%#:::::::::@@
// @@:::::::::::::::::+*##%%#*=-::::@@@#::+@@=::-%@:::-*@=:%@-:=:::*@*:::-@*::@@-:=@%::=*@*=:::::::::@@
// @@::::::::::::::+%@@@@@@@@@@@%*-:@@@#::+@*:::*@@@@@@@@#:+@%*-:::@@@@@@@@@-:@%:::#@:::+@=::::::::::@@
// @%::::::::::::*@@@@@@@@@@@@@@@@@#+@@#::+@+:::+@=-------:::=%@*::@%-------::@%:::#@:::+@=::::::::::%@
// @%::::::::::-@@@@@@++-::::-=#@@@@@+*#::+@+:::-@%=-:+%%-:-=::#@::*@*-::+%=::@%:::#@:::+@=::::::::::#@
// @%:::::::::=@@@@%+::::::::::::#@@@@**::+@+::::-#@@@@%=::#@%%@+:::+@@@@@*:::@%:::#@:::+@=::::::::::%@
// @%::::::::-@@@@+:::::::::::::::-@@@@+:::-::::::::--:::::::=-:::::::-=-:::::--:::-=----=-::::::::::%@
// @@:::::::-@@@@*:::::::::::::::::-%%%*:::::::::::::::::::::::::::::::::::::::::::+%%%%%%#::::::::::@@
// @@:::::::*@@@#:::::::+#@%*-::::::=+*#*=+#*=-:::::+#%%#-::::::=#%%#=::::-#@@#-::::=##%#+-::::::::::@@
// @@-::::::@@@@-:::::-%@#+*%@*:::::%@#+#@@+*@*::::%@+==#@*::::%@#+=%@*-::%@-=*=:::*@*+=+@%-::::::::-@@
// @@+:::::-@@@@::::::#@=::::#@=::::%@::=@*::#@-::*@=::::-@+::*@*::::*@*::*@*-::::-@@####%@#::::::::*@@
// @@#:::::-@@@*::::::@@:::::=@*::::%@::=@*::#@-::%@::::::@*::%@-::::-@%:::=%@%-::*@#******+::::::::*@@
// @@@-::::-@@@%::::::#@+::::%@-::::%@::=@*::#@-::@@-::::=@-::*@*::::*@*:::-::@%::=@*::::-=-::::::::@@@
// @@@+:::::@@@@-::::::%@%*#@@+:::::%@::=@*::#@-::@@@#=+#@+::::*@%*+@@*:::#@*+@*:::*@%*+%@%::::::::-@@@
// @@@#:::::#@@@+:::::::=*##+-::::::+*::-*=::+*:::@%=*%#*-::::::=*%#*-:::::=*#+:::::-*##*=--=::::::*@@@
// @@@@-::::=@@@@-::::::::::::::::::=**+::::::::::@%:::::::::::::::::::::::-==++**##%%@@@@@@@-::::-@@@@
// @@@@*:::::*@@@@-::::::::::::::::+@@@#::::::::::::::::::::-==++**##=-#@@@@@@@@@@@@@@@@@@@@@=::::*@@@@
// @@@@@-:::::#@@@@+-:::::::::::::*@@@@::::::-==++--##%@@@@@@@@@#*#@@%%@@@@@@@@@@@#@@@@@*#@@@+:::-@@@@@
// @@@@@%::::::*@@@@@+-:::::::::+@@@@%:=@@@@@@@@@@==@@@**%@@@@*-#@*-@@*@@@*-+**@@@:%@@==@++@@*:::%@@@@@
// @@@@@@*::::::=@@@@@@@%%**##@@@@@@=::=@@@@#-:--=-=@@=+@+=@@@:%@@@=+@-*@*-@@@+:@@=#@:*@@%-@@#::+@@@@@@
// @@@@@@@-:::::::+%@@@@@@@@@@@@@@#-::::@@@+-%@@@*--@@:@@=*@@@%=*%@@@@*=@-*@@@*:+@=+=#@@@%:@@%:-@@@@@@@
// @@@@@@@@-::::::::=*%@@@@@@@##-:::::::@@@:#@@@@@*:@@:%==@@@@@@%+-%@@#-@*+@@@:+:@*:*@@@@@:%@@-@@@@@@@@
// @@@@@@@@#-:::::::::::::-:::::::::::::@@%:%@@@@@*-@@-#@@@#-@*%@@@-#@@:@@#=+:%@-#@:@@@@@@*=@@%@@@@@@@@
// @@@@@@@@@*:::::::::::::::::::::::::::#@@--@@@@%:*@@%*+*=*@@=+@@@=+@@-#@@@@@@@=#@#@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@*-:::::::::::::::::::::::::#@@@+-==-:#@@@@@@@@@@@@#++*#@@@@@@@#@@@%:@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@*-::::::::::::::::::::::::+@@@@@%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@-=-:+**++==--::%@@@@@@@@@@@
// @@@@@@@@@@@@#-:::::::::::::::::::::::=@@@@@@@@@@@@@@@@@@@@@%%##**++=---::::::::::::::::%@@@@@@@@@@@@
// @@@@@@@@@@@@@@-::::::::::::::::::::::-@@@@@@%###**++=--::::::::::::::::::::::::::::::-%@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@+::::::::::::::::::::::--::::::::::::::::::::::::::::::::::::::::::::+@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@%-::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::-%@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@+-:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::+@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@%=::::::::::::::::::::::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@%=::::::::::::::::::::::::::::::::::::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@%=-:::::::::::::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@*-:::::::::::::::::::::::::::::::::::::::::::::#@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@%+-:::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%*=-:::::::::::::::::::::::::::::::+#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%**=-:::::::::::::::::::::=+#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%**++=====+++*##@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 
-->
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<div class="paperplane-container">
    <iframe
        id="arrival-space-iframe-pp"
        src="https://arrival.space/82998375_8446?utm=embed_82998375"
        width="100%"
        height="860"
        allow="camera; microphone; vr; xr; xr-spatial-tracking; fullscreen"
        allowfullscreen
        loading="lazy">
    </iframe>
    <div class="pp-buttons-container">
        <div class="pp-vr-instruction-text">
            Click here &#x1F861; from your VR headset
        </div>
        <a
            href="https://arrival.space/82998375_8446"
            target="_blank"
            class="pp-direct-link pp-vr-button"
        >
            <i class="fa-regular fa-paper-plane"></i> Paper Plane in VR ᯅ
        </a>
        <a
            id="pp-fullscreen-link"
            class="pp-direct-link pp-fullscreen-button"
        >
            Plein écran &#x1F5D6;
        </a>
    </div>
</div>
<style>
    .paperplane-container {
        position: relative;
        width: 100vw;
        max-width: 100%;
        margin: 0;
        padding: 0;
        height: 860px;
        overflow: hidden;
    }
    .pp-buttons-container {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        pointer-events: none;
    }
    .pp-vr-instruction-text {
        position: absolute;
        top: 64px;
        left: 50%;
        transform: translateX(-50%);
        color: white;
        font-size: 14px;
        text-align: center;
        z-index: 1001;
        text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
        pointer-events: auto;
    }
    .pp-direct-link {
        position: absolute;
        padding: 8px 15px;
        background: #00BFFF;
        color: white;
        text-decoration: none;
        font-weight: bold;
        font-size: 14px;
        border-radius: 8px;
        border: 2px solid white;
        box-shadow: 0 0 8px rgba(0, 191, 255, 0.7);
        z-index: 1000;
        pointer-events: auto;
        cursor: pointer;
        transition: all 0.3s;
        font-family: sans-serif;
    }
    .pp-vr-button {
        top: 16px;
        left: 50%;
        transform: translateX(-50%);
    }
    .pp-fullscreen-button {
        bottom: 20px;
        right: 20px;
    }
    .pp-direct-link:hover {
        background: #1E90FF;
        box-shadow: 0 0 12px rgba(0, 191, 255, 0.9);
        z-index: 1001 !important;
    }
    #arrival-space-iframe-pp {
        border: none;
        display: block;
        width: 100vw;
        margin: 0;
    }
</style>
<script>
    document.getElementById('pp-fullscreen-link').addEventListener('click', function(e) {
        e.preventDefault();
        var iframe = document.getElementById('arrival-space-iframe-pp');
        if (iframe.requestFullscreen) iframe.requestFullscreen();
        else if (iframe.webkitRequestFullscreen) iframe.webkitRequestFullscreen();
        else if (iframe.msRequestFullscreen) iframe.msRequestFullscreen();
    });
</script>
				</div>
				</div>
					</div>
		</header>
					</div>
		</header>
				<section class="elementor-section elementor-top-section elementor-element elementor-element-a5e22ff elementor-section-full_width elementor-section-stretched elementor-section-height-default elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no" data-id="a5e22ff" data-element_type="section" data-e-type="section" data-settings="{&quot;stretch_section&quot;:&quot;section-stretched&quot;,&quot;background_background&quot;:&quot;classic&quot;}">
						<div class="elementor-container elementor-column-gap-default">
					<header class="elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-8d89f52" data-id="8d89f52" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<div class="elementor-element elementor-element-2954048 elementor-widget__width-inherit elementor-widget elementor-widget-html" data-id="2954048" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!--
// PAPER PLANE MODE for Arrival space, © All rights reserved.
//
//                                                                              ++==--=+++-=
//                                                                     +++++-:::::-=++=:. .=
//                                                        +++++=++============++*+=:....  .=
//                                              =+++++===================+**#*=-:::.... .:-=
//                                 =+++++++++===========++++**********###*==-::::::..:-====
//                          %%%##********************************##%%%#**+==-::::-=========
//                     **************************************#%%%%%*+**++++==--=+++++++++=-
//               **************************************###%%%%##*************#*++++++++++=
//            ****************########################%%%%%**************######*+++++++++=
//                 ####***#######################%%%%%##*************############*+++++++
//                       ####################%%%%%##**************#################*++++=
//                              @#######%%%%%%%***************######################***+-
//                                   @%%%%##**************############################*+
//                                    %%%###########**#################################*
//                                    %%%#########*####################################
//                                    @%%%####**-     @%#%###########################
//                                    @%%%#+                 %%##################%
//                                                                %%%%%%%%%%%%#%
//                                                                    @%%%%%%@
//
// Dev-Xprmnts · VR_Xprmnts, Present Compound Design           ║
// ║ Alban DESBARAX - Designer 360° / Creative Technologist    ║
// ║ Toulouse, FRANCE                                          ║
// ║                                                           ║
// ║   · Expertise & Support 2D, 3D, AR, VR, XR, AI            ║
// ║   · Product Design & Industrial Design                    ║
// ║   · Digital communication                                 ║
// ║   · Immersive experiences  -ᯅ- ✔️                       ║
// ║                                                           ║
// · 🡺 https://presentcomposedesign.fr/
//
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#****+++++****##%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@**+-::::::::::::::::::::==*#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*+-::::::::::::::::::::::::::::::==#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+-::::::::::::::::::::::::::::::::::::::=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@*-::::::::::::::::::::::::::::::::::::::::::::=*%@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@#+:::::::::::::::++****%%@@@@@%%##**=--:::::::::::::=#@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@*=:::::::::::::+*%@@@@@@@@@@@@@@@@@@@@@@@#*=-:::::::::::=#@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@#=:::::::::::+*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#=-::::::::::=#@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@%=::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*=::::::::::=%@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@*::::::::::*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+-:::::::::*%@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@%=::::::::=*@@@@@@@@@@@@@@%#*********#%%@@@@@@@@@@@@@@@@@@@%+-::::::::=%@@@@@@@@@@@@@@
// @@@@@@@@@@@@@#:::::::::#@@@@@@@@@@@**+-::::::::::::::::-+#%@%@@@@@@@@@@@@@@%+-::::::::*@@@@@@@@@@@@@
// @@@@@@@@@@@@+::::::::+@@@@@@@@@@*-::::::::::::::::::::::#@@@##%%@@@@@@@@@@@@@@+::::::::*%@@@@@@@@@@@
// @@@@@@@@@@@=:::::::=#@@@@@@@@@+::::::::::::::::::::::::-*@@@@@@#%%@@@@@@@@@@@@@*-:::::::=%@@@@@@@@@@
// @@@@@@@@@@=:::::::*@@@@@@@@@%=::::::::::::::::::::::::::*@@@@@@@@##%@@@@@@@@@@@@%+:::::::=%@@@@@@@@@
// @@@@@@@@@%=:::::::#@@@@@@@@@%:::::::::::::::::::::::::::+@@@@@@@@@@@%%@@@@@@@@@@@@@*-::::::-%@@@@@@@
// @@@@@@@@@=::::::-#@@@@@@@@@%-:::::::::::::::::::::::::==#@@@@@@@@@@@@%#@@@@@@@@@@@@@#-::::::=%@@@@@@
// @@@@@@@@=::::::-%@@@@@@@@@@=:::::::::::::::::::::::::*=#%@@@@@@@@@@@@@%#@@@@@@@@@@@@@#-::::::*@@@@@@
// @@@@@@@*::::::-#@@@@@@@@@@*::::::::::::::::::::::::*=#+%@@@%@@@@@@@@@@@%%@@@@@@@@@@@@@%-::::::*@@@@@
// @@@@@@%:::::::#@@@@@@@@@@%:::::::::::::::::::---:::-:-+*@%%@@@@@@@@@@@@@*%@@@@@@@@@@@@@%-::::::%@@@@
// @@@@@@-::::::#@@@@@@@@@@@-::::::::::::::::::::+=#-=#*#=+%%@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@#-:::::-%@@@
// @@@@@*::::::*@@@@@@@@@@@#:::::::::::::::::::::-++=#==#+-%%%@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@*::::::*@@@
// @@@@%::::::-@@@@@@@@@@@@=::::::::::::::::::::::::+=++#%=%*@@@@@@@@@@@@@@@@*@@@@@@@@@@@@@@@-::::::%@@
// @@@@*::::::#@@@@@@@@@@@%:::::::::::::::::::::::::+#:+@%+%@@@@@@@@@@@@@@@@@@#@@@@@@@@@@@@@@%-:::::*@@
// @@@%::::::=@@@@@@@@@@@@*::::::::::::::::::::::::-#*-#+=@@%@%@@@@@@@@@@@@@@@#@@@@@@@@@@@@@@@*::::::%@
// @@@*::::::%@@@@@@@@@@@@=:::::::::::::::::::::::+=#@++=+%@@@@@@@@@@@@@@#@@@+-%@@@@@@@@@@@@@@@-:::::*@
// @@::::::*@@@@@@@@@@@@@-:::::::::::::::::::::::-**+@+*=%@%@@@@@@@@@@@#@@%=::*@@@@@@@@@@@@@@@*::::::%@
// @%::::::#@@@@@@@@@@@@@::::::::::::::::::::::::::==#@%+@%+*%%@@@@@@@#==+::=-#@@@@@@@@@@@@@@@#::::::*@
// @*:::::-@@@@@@@@@@@@@@::::::::::::::::::::::::::::***%-@*@%%@@@@@@@%%=+*%%%@@@@@@@@@@@@@@@@@-:::::+@
// @=:::::*@@@@@@@@@@@@@@-::::::::::::::::::::::::::-+@+=+*%*%@@@@@@@%@@@%*=:*@@@@@@@@@@@@@@@@@+:::::=@
// %::::::*@@@@@@@@@@@@@@*::::::::::::::::::::::-=-=+#**#=%+@@*@@@@@@@@@@@+=**%@@@@@@@@@@@@@@@@#::::::%
// %::::::%@@@@@@@@@@@@@@#:::::::::::::::::::+#@++@%@@#*+*+#=@@@@@@@@@@@@@@*%@#%@@@@@@@@@@@@@@@#::::::#
// *::::::@@@@@@@@@@@@@@@@-:::::::::::::::::*@@@:::%@@@@*---**@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@%::::::*
// *::::::@@@@@@@@@@@@@@@@+:::::::::::::::::@@@@%::*@@@@@+-:=%+@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@::::::*
// *:::::-@@@@@@@@@@@@@@@@#:::::::::::::::::%=%@@-:+@@@@@*::%*@@@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@-:::::*  
// *:::::=@@@@@@@@@@@@@@@@@+::::::::::::::::*:-@*::*-*@@@@-:-=%@%@@@@@@@@@@@@@@@@@#@@@@@@@@@@@@@::::::*
// *:::::-@@@@@@@@@@@@@@@@@%-:::::::::::::::*+:%@@:::-@@@@-:=+%%@%@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@::::::*
// *::::::%@@@@@@@@@@@@@@@@@*::::::::::::::::*=-@*::::%@@@+:+*@@@%@@@@@@@@@@@%@@%#@@@@@@@@@@@@@%::::::*
// #::::::%@@@@@@@@@@@@@@@@@%-::::::::::::::::#+**:::::%@@*:-=%@*@@@@@@@@@@@#**%@@@@@@@@@@@@@@@%::::::#
// @::::::*@@@@@@@@@@@@@@@@@@*::::::::::::::::-%@@@@@**@@%=::=+@@@@@%@@@@@@@@@%@@@@@@@@@@@@@@@@*::::::@
// @-:::::*@@@@@@@@@@@@@@@@@@@=:::::::::::::::::#@@@@@@@@%:::+=%@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@*:::::-@
// @+:::::=@@@@@@@@@@@@@@@@@@@%-:::::::::::::::::*%@@@*%@-:::-*=*%@@@@@@@@@@@@@%@@@@@@@@@@@@@@@::::::*@
// @*::::::%@@@@@@@@@@@@@@@@@@@*:::::::::::::::::::=%%+#@=:::=*++@@@@@@@@@@@@@#@@@@@@@@@@@@@@@%::::::*@
// @@-:::::*@@@@@@@@@@@@@@@@@@@#::::::::::::::::----:=###::::=**%%@@@@@@@@@@@+%@@@@@@@@@@@@@@@@+:::::-@
// @@+:::::-%@@@@@@@@@@@@@@@@@@@-:::::::::::::=@@@@@+::::::::-*#%%%@@@@@@@@@%*%@@@@@@@@@@@@@@%::::::*@@
// @@#::::::*@@@@@@@@@@@@@@@@@@@+::::::::::::::%@@@@@-:::::::+%@%%%@@@@@@@@@%@@@@@@@@@@@@@@@@+::::::%@@
// @@@+::::::%@@@@@@@@@@@@@@@@@@*::::::::::::::*@@@@@-:::::::-+#@%%@%@@@@@@@%@@@@@@@@@@@@@@@%::::::=@@@
// @@@%-:::::*@@@@@@@@@@@@@@@@@@*::::::::::::::#@@@@@::::::::::*@%**%@@@@@@@#@@@@@@@@@@@@@@@=::::::%@@@
// @@@@*::::::%@@@@@@@@@@@@@@@@@*::::::::::::::*@@@@@#-::::::::===+-%%@@@@@@%%@@@@@@@@@@@@@*::::::*@@@@
// @@@@@-:::::-%@@@@@@@@@@@@@@@@+::::::::::::::=#%@@@@*--#=::::::--:=@@%%%@@%@@@@@@@@@@@@@#::::::-@@@@@
// @@@@@#-:::::-%@@@@@@@@@@@@@@@=::::::::::::::==@%@%%%#+%%*:::::::-=#%%*%@#*@@@@@@@@@@@@%:::::::#@@@@@
// @@@@@@*::::::*%@@@@@@@@@@**+-:::::::::::::::@#@@%@@@%#%@*::::-::::==::%%*@@@@@@@@@@@@%=::::::*@@@@@@
// @@@@@@@+::::::*@@@@@@@@*::::::::::::::::::-##%%@#@@@@%%%*-==*%:-::==+**@@@@@@@@@@@@@%=::::::=@@@@@@@
// @@@@@@@@-::::::*%@@@@*::::::::::::::::::::::%%%%%@@@@@@@#%@@%%@@@@@@@@@@@@@@@@@@@@@%=::::::=@@@@@@@@
// @@@@@@@@%-::::::=%@@%:::::::::::::::::::::#@%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#:::::::-@@@@@@@@@
// @@@@@@@@@@-::::::-%@#::::::::::::::::::::+@@@@@%@@@@@@@@@@@%%@@@@@@@@@@@@@@@@@@@@*:::::::=@@@@@@@@@@
// @@@@@@@@@@@-:::::::*@-:::::::::::::::::::-%@@@@@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@=:::::::=@@@@@@@@@@@
// @@@@@@@@@@@@+:::::::==:::::::::::::::::::-%@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@#-:::::::+@@@@@@@@@@@@
// @@@@@@@@@@@@@*-:::::::::::::::::::::::::::=@@@@@@@@@@@@@@%#@@@@@@@@@@@@@@@@@%=::::::::#@@@@@@@@@@@@@
// @@@@@@@@@@@@@@#-:::::::::::::::::::::::::::*@@@@@@@@@@@@#@@@@@@@@@@@@@@@@@@+::::::::=#@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@*-::::::::::::::::::::::::::*%@@@@@@@@@@*@@@@@@@@@@@@@@@@+:::::::::*@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@%=:::::::::::::::::::::::::::*%@@@@@@@@#@@@@@@@@@@@@@*=:::::::::=%@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@#-:::::::::::::::::::::::::::*%@@@@@@@%#@@@@@@@@*+-:::::::::=#@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@#-:::::::::::::::::::::::::::*%@@@@@@@%@@@@*+:::::::::::=#@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@#=-::::::::::::::::::::::::::+%@@@@%**--::::::::::::+#@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@%*-:::::::::::::::::::::::::::::::::::::::::::::*%@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@#*-:::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@##=-:::::::::::::::::::::::::::::::+*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@##*=-::::::::::::::::::::-+**%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%##****+++++****%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
//
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#*++==------===**@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#++-::::::::::::::::::::::-=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+-::::::::::::::::::::::::::::::::=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@#+::::::::::::::::::::::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@%+:::::::::::::::::=++***+=-::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@#=::::::::::::::::+%@@@@@@@@@@%+-:::::::::::::::::::=#@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@#-::::::::::::::::#@@@@@@@@@@@@@@@@%-:::::::::::::::::::=#@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@%=::::::::::::::::*@@@@@@**==-=+##@@@@@*-:::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@=:::::::::::::::::%@@@@@+::::::::::-*@@@@#-::::::::::::::::::::=@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@#::::::::::::::::::%@@@@=::::::::::::::-%@@@#-:::::::::::::::::::::#@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@=::::::::::::::::::*@@@@=::::::::::::::::-%@@@*::::::::::::::::::::::=@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@%-::::::::::::::::::-@@@@=::::::::::::::::::-%@@@-::::::::::::::::::::::-%@@@@@@@@@@@@@
// @@@@@@@@@@@@#::::::::::::::::::::*@@@#::::::::::::::::::::=@@@*::::::::::::::::::::::::*@@@@@@@@@@@@
// @@@@@@@@@@@#:::::::::::::::::::::%@@@+::::::::::::::::::::-@@@%:::::::::::::::::::::::::*@@@@@@@@@@@
// @@@@@@@@@@+::::::::::::::::::::::@@@@-:::::::::::::::::::::%@@@::::::::::::::::::::::::::*@@@@@@@@@@
// @@@@@@@@@*:::::::::::::::::::::::@@@@=:::::::::::::::::::::@@@@:::::::::::::::::::::::::::*@@@@@@@@@
// @@@@@@@@%::::::::::::::::::::::::@@@@*:::::::::::::::::::::@@@%::::::::::::::::::::::::::::*@@@@@@@@
// @@@@@@@%:::::::::::::::::::::::::@@@@%::::::::::::::::::::+@@@*:::::::::::::::::::::::::::::%@@@@@@@
// @@@@@@@::::::::::::::::::::::::::@@@@@*::::::::::::::::::-@@@@::::::::::::::::::::::::::::::-@@@@@@@
// @@@@@@=::::::::::::::::::::::::::@@@@@@*::::::::::::::::-@@@@+:::::::::::::::::::::::::::::::*@@@@@@
// @@@@@%:::::::::::::::::::::::::::@@@@@@@*::::::::::::::-%@@@#:::::::::::::::::::::::::::::::::*@@@@@
// @@@@@::::::::::::::::::::::::::::@@@@@@@@@*::::::::::+@@@@@*::::::::::::::::::::::::::::::::::-@@@@@
// @@@@*::::::::::::::::::::::::::::@@@#=@@@@@@%#*+=**%@@@@@@=::::::::::::::::::::::::::::::::::::*@@@@
// @@@@:::::::::::::::::::::::::::::@@@#::*@@@@@@@@@@@@@@@@*:::::::::::::::::::::::::::::::::::::::@@@@
// @@@*:::::::::::::::::::::::::::::@@@#::::=*@@@@@@@@@%#=:::::::::::::::::::::::::::::::::::::::::#@@@
// @@@=:::::::::::::::::::::::::::::@@@#::::::::-==+=-:::::::::::::::::::::::::::::::::::::::::::::=@@@
// @@@::::::::::::::::::::::::::::::@@@#:::::::::::::::::::::::::::::::::::::::::::::::::--:::::::::@@@
// @@*::::::::::::::::::::::::::::::@@@#:::::::::+######*:::::::::::::::::::::::::::::::+@=:::::::::#@@
// @@*::::::::::::::::::::::::::::::@@@#:::--::::===+*+==:::::=-:::::::=-::::::-::::::::+@=:::::::::+@@
// @@:::::::::::::::::::::::::::::::@@@#::+@#@@::-#@@%@%=::=@@@@+:::+@@@@@+-::@@@@%*-::#@@%#:::::::::@@
// @@:::::::::::::::::+*##%%#*=-::::@@@#::+@@=::-%@:::-*@=:%@-:=:::*@*:::-@*::@@-:=@%::=*@*=:::::::::@@
// @@::::::::::::::+%@@@@@@@@@@@%*-:@@@#::+@*:::*@@@@@@@@#:+@%*-:::@@@@@@@@@-:@%:::#@:::+@=::::::::::@@
// @%::::::::::::*@@@@@@@@@@@@@@@@@#+@@#::+@+:::+@=-------:::=%@*::@%-------::@%:::#@:::+@=::::::::::%@
// @%::::::::::-@@@@@@++-::::-=#@@@@@+*#::+@+:::-@%=-:+%%-:-=::#@::*@*-::+%=::@%:::#@:::+@=::::::::::#@
// @%:::::::::=@@@@%+::::::::::::#@@@@**::+@+::::-#@@@@%=::#@%%@+:::+@@@@@*:::@%:::#@:::+@=::::::::::%@
// @%::::::::-@@@@+:::::::::::::::-@@@@+:::-::::::::--:::::::=-:::::::-=-:::::--:::-=----=-::::::::::%@
// @@:::::::-@@@@*:::::::::::::::::-%%%*:::::::::::::::::::::::::::::::::::::::::::+%%%%%%#::::::::::@@
// @@:::::::*@@@#:::::::+#@%*-::::::=+*#*=+#*=-:::::+#%%#-::::::=#%%#=::::-#@@#-::::=##%#+-::::::::::@@
// @@-::::::@@@@-:::::-%@#+*%@*:::::%@#+#@@+*@*::::%@+==#@*::::%@#+=%@*-::%@-=*=:::*@*+=+@%-::::::::-@@
// @@+:::::-@@@@::::::#@=::::#@=::::%@::=@*::#@-::*@=::::-@+::*@*::::*@*::*@*-::::-@@####%@#::::::::*@@
// @@#:::::-@@@*::::::@@:::::=@*::::%@::=@*::#@-::%@::::::@*::%@-::::-@%:::=%@%-::*@#******+::::::::*@@
// @@@-::::-@@@%::::::#@+::::%@-::::%@::=@*::#@-::@@-::::=@-::*@*::::*@*:::-::@%::=@*::::-=-::::::::@@@
// @@@+:::::@@@@-::::::%@%*#@@+:::::%@::=@*::#@-::@@@#=+#@+::::*@%*+@@*:::#@*+@*:::*@%*+%@%::::::::-@@@
// @@@#:::::#@@@+:::::::=*##+-::::::+*::-*=::+*:::@%=*%#*-::::::=*%#*-:::::=*#+:::::-*##*=--=::::::*@@@
// @@@@-::::=@@@@-::::::::::::::::::=**+::::::::::@%:::::::::::::::::::::::-==++**##%%@@@@@@@-::::-@@@@
// @@@@*:::::*@@@@-::::::::::::::::+@@@#::::::::::::::::::::-==++**##=-#@@@@@@@@@@@@@@@@@@@@@=::::*@@@@
// @@@@@-:::::#@@@@+-:::::::::::::*@@@@::::::-==++--##%@@@@@@@@@#*#@@%%@@@@@@@@@@@#@@@@@*#@@@+:::-@@@@@
// @@@@@%::::::*@@@@@+-:::::::::+@@@@%:=@@@@@@@@@@==@@@**%@@@@*-#@*-@@*@@@*-+**@@@:%@@==@++@@*:::%@@@@@
// @@@@@@*::::::=@@@@@@@%%**##@@@@@@=::=@@@@#-:--=-=@@=+@+=@@@:%@@@=+@-*@*-@@@+:@@=#@:*@@%-@@#::+@@@@@@
// @@@@@@@-:::::::+%@@@@@@@@@@@@@@#-::::@@@+-%@@@*--@@:@@=*@@@%=*%@@@@*=@-*@@@*:+@=+=#@@@%:@@%:-@@@@@@@
// @@@@@@@@-::::::::=*%@@@@@@@##-:::::::@@@:#@@@@@*:@@:%==@@@@@@%+-%@@#-@*+@@@:+:@*:*@@@@@:%@@-@@@@@@@@
// @@@@@@@@#-:::::::::::::-:::::::::::::@@%:%@@@@@*-@@-#@@@#-@*%@@@-#@@:@@#=+:%@-#@:@@@@@@*=@@%@@@@@@@@
// @@@@@@@@@*:::::::::::::::::::::::::::#@@--@@@@%:*@@%*+*=*@@=+@@@=+@@-#@@@@@@@=#@#@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@*-:::::::::::::::::::::::::#@@@+-==-:#@@@@@@@@@@@@#++*#@@@@@@@#@@@%:@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@*-::::::::::::::::::::::::+@@@@@%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@-=-:+**++==--::%@@@@@@@@@@@
// @@@@@@@@@@@@#-:::::::::::::::::::::::=@@@@@@@@@@@@@@@@@@@@@%%##**++=---::::::::::::::::%@@@@@@@@@@@@
// @@@@@@@@@@@@@@-::::::::::::::::::::::-@@@@@@%###**++=--::::::::::::::::::::::::::::::-%@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@+::::::::::::::::::::::--::::::::::::::::::::::::::::::::::::::::::::+@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@%-::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::-%@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@+-:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::+@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@%=::::::::::::::::::::::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@%=::::::::::::::::::::::::::::::::::::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@%=-:::::::::::::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@*-:::::::::::::::::::::::::::::::::::::::::::::#@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@%+-:::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%*=-:::::::::::::::::::::::::::::::+#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%**=-:::::::::::::::::::::=+#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%**++=====+++*##@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 
-->
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<div class="pp-desc-container">
    <div class="pp-desc-wrap">

        <!-- Badge lauréat -->
        <div class="pp-award-row">
            <div class="pp-laurel-badge">
                <img decoding="async"
                    src="https://presentcomposedesign.fr/wp-content/uploads/2026/03/3rd_place_svg_PresentComposedesign_NWSOPC.svg"
                    alt="3rd Place - New Worlds and Splats Open Call"
                    class="pp-laurel-img"
                />
            </div>
        </div>

        <h2 class="pp-title">Paper Plane <i class="fa-regular fa-paper-plane pp-title-icon"></i></h2>
        <p class="pp-subtitle">Mon tout premier plugin Arrival.Space. Un avatar universel pour le web immersif.</p>

        <div class="pp-divider"></div>

        <div class="pp-content">
            <div class="pp-block">
                <h3 class="pp-heading">Le concept</h3>
                <p>Paper Plane est parti d'une question simple : et si le meilleur avatar était aussi le plus universel ? Pas de personnage genré, pas de corps humanoïde. Juste un avion en papier, instinctif et léger, qui traverse librement chaque monde d'Arrival.Space.</p>
            </div>

            <div class="pp-block">
                <h3 class="pp-heading">Pourquoi un avion en papier ?</h3>
                <p>Tout le monde en a plié un. Tout le monde en a lancé un. C'est peut-être l'objet le plus démocratique qui existe. En tant qu'avatar, il efface toute friction d'identification : pas d'âge, pas de genre, pas de skin à choisir. Du mouvement pur, et une liberté totale d'exploration.</p>
            </div>

            <div class="pp-block">
                <h3 class="pp-heading">De SplatGate à Paper Plane</h3>
                <p>Ce projet s'appuie sur toute l'expertise en Gaussian Splatting que j'ai construite avec SplatGate. Paper Plane, c'est mon premier pas dans l'écosystème Arrival.Space, un nouveau terrain pour pousser les limites du web immersif, du spatial computing et du vibe coding.</p>
            </div>

            <!-- Tags compacts + VR gradient -->
            <div class="pp-tags">
                <span class="pp-tag">🧊 Gaussian Splatting</span>
                <span class="pp-tag">🌐 Web Immersif</span>
                <span class="pp-tag pp-tag-vr"><span class="pp-vr-gradient">Full VR available Experience -ᯅ-</span> ✔️</span>
            </div>
        </div>

        <!-- CTAs - 3 boutons sur une ligne -->
        <div class="pp-cta-row">
            <a href="https://arrival.space/83754293_5074?gate=608f2483-802b-4fa7-95bb-920bed1431ad" target="_blank" class="pp-cta-primary">
                Explorer les univers gagnants <i class="fa-regular fa-paper-plane"></i>
            </a>
            <a href="https://arrival.space/82998375_8446" target="_blank" class="pp-cta-secondary">
                Essayer Paper Plane
            </a>
            <a href="https://buymeacoffee.com/presentcomposedesign" target="_blank" class="pp-cta-support">
                <i class="fa-solid fa-heart pp-heart-pulse"></i> Soutenir / Remercier
            </a>
        </div>

        <!-- Partenaires -->
        <div class="pp-partners">
            <span class="pp-partners-label">Organisé par</span>
            <div class="pp-partners-logos">
                <a href="https://arrival.space/" target="_blank" class="pp-partner-block" title="Arrival.Space">
                    <img decoding="async"
                        src="https://presentcomposedesign.fr/wp-content/uploads/2026/03/arrival-space-logo-white.svg"
                        alt="Arrival.Space"
                        class="pp-partner-logo"
                    />
                    <span class="pp-partner-name">Arrival.Space</span>
                </a>
                <span class="pp-partners-sep">&</span>
                <a href="https://www.choice.love/" target="_blank" class="pp-partner-block" title="CHOICE DAO">
                    <img decoding="async"
                        src="https://presentcomposedesign.fr/wp-content/uploads/2026/03/choice-DAO-logo-1.svg"
                        alt="CHOICE DAO"
                        class="pp-partner-logo"
                    />
                    <span class="pp-partner-name">CHOICE DAO</span>
                </a>
            </div>
        </div>

    </div>
</div>
<style>
    /* Animations */
    @keyframes ppHoloFlow {
        0% { background-position: 0% 50%; }
        100% { background-position: 200% 50%; }
    }
    @keyframes ppBronzeFlow {
        0% { background-position: 0% 50%; }
        100% { background-position: 200% 50%; }
    }
    @keyframes ppHeartBeat {
        0% { transform: scale(1); }
        50% { transform: scale(1.15); }
        100% { transform: scale(1); }
    }

    .pp-desc-container {
        width: 100vw;
        position: relative;
        left: 50%;
        right: 50%;
        margin-left: -50vw;
        margin-right: -50vw;
        padding: 0;
        background: linear-gradient(175deg, #0a0e1a 0%, #111827 50%, #0f172a 100%);
        box-sizing: border-box;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif;
        overflow: hidden;
    }
    .pp-desc-wrap {
        max-width: 900px;
        margin: 0 auto;
        padding: 60px 40px 50px;
    }

    /* Badge lauréat */
    .pp-award-row {
        display: flex;
        justify-content: center;
        margin-bottom: 4px;
    }
    .pp-laurel-badge {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 8px;
    }
    .pp-laurel-img {
        width: 160px;
        height: auto;
        filter: drop-shadow(0 0 14px rgba(184, 122, 94, 0.35));
    }
    .pp-award-label {
        font-size: 11px;
        font-weight: 600;
        color: #C4836A;
        letter-spacing: 2.5px;
        text-transform: uppercase;
        text-align: center;
        opacity: 0.85;
    }

    /* Titre & sous-titre */
    .pp-title {
        font-size: 46px;
        font-weight: 800;
        color: #ffffff;
        margin: 0 0 10px 0;
        line-height: 1.1;
        text-align: center;
    }
    .pp-title-icon {
        font-size: 36px;
        vertical-align: middle;
        margin-left: 6px;
        color: #00BFFF;
    }
    .pp-subtitle {
        font-size: 18px;
        color: #94a3b8;
        margin: 0 0 30px 0;
        line-height: 1.5;
        text-align: center;
    }
    .pp-divider {
        width: 60px;
        height: 3px;
        background: linear-gradient(90deg, #00BFFF, #1E90FF);
        border-radius: 2px;
        margin: 0 auto 40px;
    }

    /* Contenu */
    .pp-content {
        margin-bottom: 36px;
    }
    .pp-block {
        margin-bottom: 28px;
    }
    .pp-heading {
        font-size: 18px;
        font-weight: 700;
        color: #e2e8f0;
        margin: 0 0 8px 0;
    }
    .pp-block p {
        font-size: 15px;
        color: #94a3b8;
        line-height: 1.7;
        margin: 0;
    }

    /* Tags compacts */
    .pp-tags {
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
        margin-top: 28px;
        justify-content: center;
    }
    .pp-tag {
        display: inline-flex;
        align-items: center;
        padding: 2px 10px;
        font-size: 11px;
        line-height: 1.4;
        font-weight: 500;
        color: #94a3b8;
        background: rgba(255, 255, 255, 0.03);
        border: 1px solid rgba(255, 255, 255, 0.06);
        border-radius: 20px;
        letter-spacing: 0.3px;
        white-space: nowrap;
    }
    .pp-tag-vr {
        gap: 4px;
    }
    .pp-vr-gradient {
        background: linear-gradient(120deg, #00F7AD, #0010EF 50%, #00F7AD);
        background-size: 200% 200%;
        animation: ppHoloFlow 5s linear infinite;
        -webkit-background-clip: text;
        -webkit-text-fill-color: transparent;
        background-clip: text;
        font-weight: 600;
    }

    /* CTAs - 3 boutons sur une ligne */
    .pp-cta-row {
        display: flex;
        justify-content: center;
        align-items: center;
        gap: 12px;
        flex-wrap: nowrap;
        margin-bottom: 50px;
    }

    /* CTA principal - contour gradient animé bronze/or */
    .pp-cta-primary {
        position: relative;
        display: inline-flex;
        align-items: center;
        gap: 8px;
        padding: 12px 24px;
        background: rgba(255, 255, 255, 0.03);
        backdrop-filter: blur(10px);
        -webkit-backdrop-filter: blur(10px);
        color: #D4967A;
        text-decoration: none;
        font-size: 14px;
        font-weight: 700;
        border-radius: 10px;
        border: 2px solid transparent;
        transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s;
        white-space: nowrap;
    }
    .pp-cta-primary::before {
        content: "";
        position: absolute;
        inset: 0;
        border-radius: 10px;
        padding: 2px;
        background: linear-gradient(120deg, #C9A84C, #B87A5E 30%, #D4967A 60%, #C9A84C);
        background-size: 200% 200%;
        animation: ppBronzeFlow 4s linear infinite;
        -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
        -webkit-mask-composite: xor;
        mask-composite: exclude;
        z-index: -1;
    }
    .pp-cta-primary:hover {
        transform: translateY(-2px);
        box-shadow: 0 5px 20px rgba(184, 122, 94, 0.25);
    }

    .pp-cta-secondary {
        display: inline-flex;
        align-items: center;
        padding: 12px 24px;
        background: transparent;
        color: #00BFFF;
        text-decoration: none;
        font-size: 14px;
        font-weight: 700;
        border-radius: 10px;
        border: 2px solid rgba(0, 191, 255, 0.4);
        transition: all 0.3s ease;
        white-space: nowrap;
    }
    .pp-cta-secondary:hover {
        border-color: #00BFFF;
        background: rgba(0, 191, 255, 0.08);
        transform: translateY(-2px);
    }
    .pp-cta-support {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        padding: 12px 24px;
        background: rgba(255, 255, 255, 0.03);
        color: #fff;
        text-decoration: none;
        font-size: 14px;
        font-weight: 700;
        border-radius: 10px;
        border: 2px solid rgba(255, 255, 255, 0.12);
        transition: all 0.3s ease;
        backdrop-filter: blur(10px);
        -webkit-backdrop-filter: blur(10px);
        white-space: nowrap;
    }
    .pp-cta-support:hover {
        transform: translateY(-2px);
        background: rgba(255, 255, 255, 0.08);
        border-color: rgba(255, 255, 255, 0.25);
        box-shadow: 0 0 20px rgba(255, 64, 129, 0.2);
    }
    .pp-heart-pulse {
        color: #ff4081;
        animation: ppHeartBeat 2s infinite ease-in-out;
    }

    /* Partenaires */
    .pp-partners {
        text-align: center;
        padding-top: 30px;
        border-top: 1px solid rgba(255, 255, 255, 0.06);
    }
    .pp-partners-label {
        display: block;
        font-size: 11px;
        font-weight: 600;
        color: #64748b;
        letter-spacing: 2px;
        text-transform: uppercase;
        margin-bottom: 18px;
    }
    .pp-partners-logos {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 20px;
    }
    .pp-partner-block {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 8px;
        text-decoration: none;
        opacity: 0.7;
        transition: opacity 0.3s;
    }
    .pp-partner-block:hover {
        opacity: 1;
    }
    .pp-partner-name {
        font-size: 12px;
        font-weight: 200;
        color: #94a3b8;
        letter-spacing: 0.5px;
    }
    .pp-partner-logo {
        height: 22px;
        width: auto;
        object-fit: contain;
    }
    .pp-partners-sep {
        color: #64748b;
        font-size: 20px;
        font-weight: 200;
        align-self: center;
        padding: 0 10px;
    }

    /* Responsive */
    @media (max-width: 768px) {
        .pp-desc-wrap {
            padding: 40px 20px 36px;
        }
        .pp-title {
            font-size: 30px;
        }
        .pp-title-icon {
            font-size: 24px;
        }
        .pp-subtitle {
            font-size: 16px;
        }
        .pp-cta-row {
            flex-wrap: wrap;
            flex-direction: column;
            align-items: center;
        }
        .pp-cta-primary,
        .pp-cta-secondary,
        .pp-cta-support {
            width: 100%;
            max-width: 300px;
            text-align: center;
            justify-content: center;
        }
        .pp-partners-logos {
            flex-direction: column;
            width: 80px;
            gap: 14px;
        }
        .pp-partners-sep {
            display: none;
        }
        .pp-laurel-img {
            width: 120px;
        }
    }
</style>
				</div>
				</div>
					</div>
		</header>
					</div>
		</section>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/paper-plane-mode_for_arrival-space/">Paper plane</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Curiosity</title>
		<link>https://presentcomposedesign.fr/curiosity/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Thu, 25 Dec 2025 18:47:39 +0000</pubDate>
				<category><![CDATA[[ia]]]></category>
		<category><![CDATA[Design graphique]]></category>
		<category><![CDATA[Design produit]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=35140</guid>

					<description><![CDATA[<p>Curiosity Viewer ● Curiosity Chat 🧠 Curiosity 🗑️ ⚙️ 👋 Bonjour, Je suis Curiosity, votre oracle culturel français. Posez-moi n&#8217;importe quelle question ! 🎤 ▶️ ⚙️ Réglages Avancés ✕ 🎨 Apparence Thème Choisissez le thème d&#8217;affichage Auto ☀️ Clair 🌙 Sombre Affichage du personnage Montrer/cacher Curiosity Afficher le personnage Curiosity Taille de la fenêtre Ajuster la taille selon vos préférences 100% 🎤 Synthèse Vocale Voix française Sélectionnez la voix de lecture Vitesse de lecture 1.0x Lecture automatique des réponses Lire les sources à voix haute 💬 Conversation Mode de réponse Longueur des réponses de Curiosity Concis (2 phrases)Détaillé (5 phrases)Complet (10+ phrases) Historique conservé Nombre d&#8217;échanges mémorisés 10 Sauvegarder l&#8217;historique (localStorage) 🔧 Avancé URL de l&#8217;API Ne modifiez que si nécessaire Délai d&#8217;affichage des yeux Durée d&#8217;affichage des recherches (secondes) 8s Effet parallax sur le personnage Mode debug (console) 💾 Sauvegarder les paramètres</p>
<p>Cet article <a href="https://presentcomposedesign.fr/curiosity/">Curiosity</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="35140" class="elementor elementor-35140">
				<div class="elementor-element elementor-element-11b78ac e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent" data-id="11b78ac" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-cfacc9d elementor-widget elementor-widget-html" data-id="cfacc9d" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Curiosity Viewer</title>
    <style>
        :root {
            --phi: 1.618;
            --viewer-size: min(500px, 80vmin);
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            background: #f5f7fa;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
        }

        .curiosity-viewer {
            position: relative;
            width: var(--viewer-size);
            height: var(--viewer-size);
            transition: transform 0.1s ease-out;
        }

        /* Base character image */
        .character-base {
            width: 100%;
            height: 100%;
            position: absolute;
            top: 0;
            left: 0;
        }

        .character-base img {
            width: 100%;
            height: 100%;
            object-fit: contain;
            filter: drop-shadow(0 20px 40px rgba(0,0,0,0.15));
        }

        /* Screen container with perfect SVG mask */
        .screen-container {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            -webkit-mask-image: url('https://presentcomposedesign.fr/wp-content/uploads/2025/12/Curiosity_mask-screen-transparent_PresentComposedesign.svg');
            mask-image: url('https://presentcomposedesign.fr/wp-content/uploads/2025/12/Curiosity_mask-screen-transparent_PresentComposedesign.svg');
            -webkit-mask-size: 100% 100%;
            mask-size: 100% 100%;
            -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
            -webkit-mask-position: center;
            mask-position: center;
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 0;
            padding: 0;
            background: #000;
        }

        /* Left Eye - Code */
        .left-eye {
            width: 100%;
            height: 100%;
            background: #000;
            overflow: hidden;
            display: flex;
            flex-direction: column;
            justify-content: flex-start;
            padding: 8%;
            font-family: 'Courier New', monospace;
            font-size: 7px;
            line-height: 1.5;
            color: #fff;
            opacity: 0;
            transition: opacity 0.3s;
        }

        .left-eye.active {
            opacity: 1;
            animation: scrollCode 6s linear infinite;
        }

        @keyframes scrollCode {
            0% { transform: translateY(0); }
            100% { transform: translateY(-30%); }
        }

        .code-line {
            white-space: nowrap;
            text-shadow: 0 0 8px #fff;
            margin-bottom: 1px;
        }

        /* Right Eye - Preview */
        .right-eye {
            width: 100%;
            height: 100%;
            background: #fff;
            overflow: hidden;
            position: relative;
        }

        .eye-preview {
            width: 100%;
            height: 100%;
            display: none;
        }

        .eye-preview.active {
            display: block;
        }

        .eye-preview iframe {
            width: 400%;
            height: 400%;
            border: none;
            transform: scale(0.25);
            transform-origin: top left;
            pointer-events: none;
        }

        .preview-placeholder {
            width: 100%;
            height: 100%;
            display: flex;
            align-items: center;
            justify-content: center;
            color: #fff;
            font-family: monospace;
            font-size: 8px;
            text-align: center;
            background: #000;
        }

        /* Status Indicator */
        .status-indicator {
            position: absolute;
            bottom: 8%;
            right: 12%;
            width: calc(var(--viewer-size) * 0.08);
            height: calc(var(--viewer-size) * 0.08);
            border-radius: 50%;
            background: #4ade80;
            box-shadow: 0 0 0 calc(var(--viewer-size) * 0.02) rgba(74,222,128,0.3);
            z-index: 100;
            transition: all 0.3s ease;
        }

        .status-indicator.thinking {
            background: #fbbf24;
            box-shadow: 0 0 0 calc(var(--viewer-size) * 0.02) rgba(251,191,36,0.3);
            animation: pulse 1.5s infinite;
        }

        .status-indicator.error {
            background: #f87171;
            box-shadow: 0 0 0 calc(var(--viewer-size) * 0.02) rgba(248,113,113,0.3);
        }

        @keyframes pulse {
            0%, 100% { transform: scale(1); }
            50% { transform: scale(1.2); }
        }

        /* Responsive */
        @media (max-width: 768px) {
            :root {
                --viewer-size: min(320px, 85vmin);
            }
            .left-eye {
                font-size: 6px;
            }
        }
    </style>
</head>
<body>
    <div class="curiosity-viewer" id="viewer">
        <!-- Character Base -->
        <div class="character-base">
            <img decoding="async" id="characterImg" 
                 src="https://presentcomposedesign.fr/wp-content/uploads/2025/12/Curiosity_OFF_PresentComposedesign.png"
                 alt="Curiosity">
        </div>

        <!-- Eyes Screen -->
        <div class="screen-container">
            <!-- Left Eye -->
            <div class="left-eye" id="leftEye"></div>
            
            <!-- Right Eye -->
            <div class="right-eye">
                <div id="rightEye" class="eye-preview">
                    <div class="preview-placeholder">●</div>
                </div>
            </div>
        </div>

        <!-- Status -->
        <div id="statusIndicator" class="status-indicator"></div>
    </div>

    <script>
        const STATES = {
            OFF: 'https://presentcomposedesign.fr/wp-content/uploads/2025/12/Curiosity_OFF_PresentComposedesign.png',
            ON: 'https://presentcomposedesign.fr/wp-content/uploads/2025/12/Curiosity_ON_PresentComposedesign.png',
            THINKING: 'https://presentcomposedesign.fr/wp-content/uploads/2025/12/Curiosity_requesting-sample_PresentComposedesign.png',
            GOOD: 'https://presentcomposedesign.fr/wp-content/uploads/2025/12/Curiosity_good_PresentComposedesign.png',
            BAD: 'https://presentcomposedesign.fr/wp-content/uploads/2025/12/Curiosity_bad_PresentComposedesign.png'
        };

        // API pour contrôler le viewer depuis l'extérieur
        window.CuriosityViewer = {
            setState(state) {
                document.getElementById('characterImg').src = STATES[state] || STATES.ON;
            },

            setStatus(status) {
                const indicator = document.getElementById('statusIndicator');
                indicator.className = 'status-indicator ' + (status || '');
            },

            showCode(query) {
                const leftEye = document.getElementById('leftEye');
                const code = [
                    `> web_search("${query}")`,
                    `> Connecting...`,
                    `> fetch wikipedia.org`,
                    `> fetch meteofrance.com`,
                    `> fetch coingecko.com`,
                    `> Parsing HTML...`,
                    `> const data = await parse()`,
                    `> Extracting content...`,
                    `> Processing 12 sources`,
                    `> Analyzing data...`,
                    `> Building response...`,
                    `> ✓ Complete`,
                    ``,
                    `// ${new Date().toLocaleTimeString()}`,
                    `// Query: "${query}"`,
                    `// Confidence: 96%`
                ];
                
                leftEye.innerHTML = code.map(line => 
                    `<div class="code-line">${line}</div>`
                ).join('');
                
                leftEye.classList.add('active');
            },

            showPreview(url) {
                const rightEye = document.getElementById('rightEye');
                if (url && url.startsWith('http')) {
                    rightEye.innerHTML = `<iframe src="${url}" sandbox="allow-same-origin"></iframe>`;
                    rightEye.classList.add('active');
                }
            },

            clearEyes() {
                const leftEye = document.getElementById('leftEye');
                const rightEye = document.getElementById('rightEye');
                
                leftEye.classList.remove('active');
                rightEye.classList.remove('active');
                
                setTimeout(() => {
                    leftEye.innerHTML = '';
                    rightEye.innerHTML = '<div class="preview-placeholder">●</div>';
                }, 300);
            }
        };

        // Parallax
        document.addEventListener('mousemove', e => {
            const x = (e.clientX / window.innerWidth - 0.5) * 12;
            const y = (e.clientY / window.innerHeight - 0.5) * 12;
            document.getElementById('viewer').style.transform = `translate(${x}px, ${y}px)`;
        });

        // Init
        window.CuriosityViewer.setState('OFF');
        setTimeout(() => {
            window.CuriosityViewer.setState('ON');
        }, 1000);

        // Écouter les messages du chat
        window.addEventListener('message', (event) => {
            const { action, data } = event.data;
            
            switch(action) {
                case 'setState':
                    window.CuriosityViewer.setState(data);
                    break;
                case 'setStatus':
                    window.CuriosityViewer.setStatus(data);
                    break;
                case 'showCode':
                    window.CuriosityViewer.showCode(data);
                    break;
                case 'showPreview':
                    window.CuriosityViewer.showPreview(data);
                    break;
                case 'clearEyes':
                    window.CuriosityViewer.clearEyes();
                    break;
            }
        });
    </script>
</body>
</html>				</div>
				</div>
				<div class="elementor-element elementor-element-d5da671 elementor-widget elementor-widget-html" data-id="d5da671" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Curiosity Chat</title>
    <style>
        /* ===================================
           CURIOSITY CHAT v3.2 - Golden Ratio
           © Présent Composé Design
           =================================== */
        
        :root {
            --phi: 1.618;
            --unit: 20px;
            
            /* Espacements selon le nombre d'or */
            --space-xxs: calc(var(--unit) / var(--phi) / var(--phi) / var(--phi));
            --space-xs: calc(var(--unit) / var(--phi) / var(--phi));
            --space-s: calc(var(--unit) / var(--phi));
            --space-m: var(--unit);
            --space-l: calc(var(--unit) * var(--phi));
            --space-xl: calc(var(--unit) * var(--phi) * var(--phi));
            --space-xxl: calc(var(--unit) * var(--phi) * var(--phi) * var(--phi));
            
            /* Tailles selon le nombre d'or */
            --text-xs: calc(var(--unit) / var(--phi) / var(--phi));
            --text-s: calc(var(--unit) / var(--phi));
            --text-m: var(--unit);
            --text-l: calc(var(--unit) * var(--phi));
            --text-xl: calc(var(--unit) * var(--phi) * var(--phi));
            
            --border-radius-s: calc(var(--unit) / var(--phi));
            --border-radius-m: var(--unit);
            --border-radius-l: calc(var(--unit) * var(--phi));
            
            --bg-light: #F5F7FA;
            --bg-dark: #0a0e27;
            --card-light: rgba(255,255,255,0.98);
            --card-dark: rgba(15,20,40,0.98);
            --text-light: #1a1a2e;
            --text-dark: #e8e9f3;
            --accent: #667eea;
            --accent-secondary: #764ba2;
            --success: #4ade80;
            --warning: #fbbf24;
            --error: #f87171;
            
            --chat-width: calc(600px);
            --chat-max-width: calc(var(--chat-width) * var(--phi));
        }

        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif;
            -webkit-font-smoothing: antialiased;
            -moz-osx-font-smoothing: grayscale;
            line-height: var(--phi);
            transition: background-color 0.3s ease;
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: var(--space-m);
        }

        body.light-mode {
            background: var(--bg-light);
            color: var(--text-light);
        }

        body.dark-mode {
            background: var(--bg-dark);
            color: var(--text-dark);
        }

        /* Main Chat Container */
        .chat-container {
            width: 100%;
            max-width: var(--chat-max-width);
        }

        .chat-interface {
            width: 100%;
            backdrop-filter: blur(calc(var(--space-m) * var(--phi)));
            -webkit-backdrop-filter: blur(calc(var(--space-m) * var(--phi)));
            border-radius: var(--border-radius-l);
            padding: var(--space-l);
            box-shadow: 0 var(--space-m) var(--space-xl) rgba(0,0,0,0.15);
            transition: all 0.3s ease;
        }

        .light-mode .chat-interface {
            background: var(--card-light);
            border: 1px solid rgba(0,0,0,0.06);
        }

        .dark-mode .chat-interface {
            background: var(--card-dark);
            border: 1px solid rgba(255,255,255,0.1);
        }

        /* Header */
        .chat-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: var(--space-l);
            padding-bottom: var(--space-s);
            border-bottom: 1px solid rgba(102,126,234,0.1);
        }

        .chat-title {
            font-size: var(--text-xl);
            font-weight: 800;
            background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
            letter-spacing: -0.02em;
        }

        .header-actions {
            display: flex;
            gap: var(--space-xs);
        }

        .icon-btn {
            width: calc(var(--space-l) * var(--phi));
            height: calc(var(--space-l) * var(--phi));
            border-radius: 50%;
            border: none;
            background: rgba(102,126,234,0.1);
            color: var(--accent);
            cursor: pointer;
            display: grid;
            place-items: center;
            transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
            font-size: var(--text-m);
        }

        .icon-btn.danger {
            background: rgba(248,113,113,0.1);
            color: var(--error);
        }

        .icon-btn:hover {
            transform: scale(1.1) rotate(10deg);
            background: rgba(102,126,234,0.2);
        }

        .icon-btn.danger:hover {
            background: rgba(248,113,113,0.2);
        }

        .icon-btn:active {
            transform: scale(0.95);
        }

        /* Messages Area */
        .messages-container {
            height: calc(var(--space-xxl) * 3);
            overflow-y: auto;
            margin-bottom: var(--space-l);
            padding: var(--space-xs);
            scrollbar-width: thin;
            scrollbar-color: var(--accent) transparent;
        }

        .messages-container::-webkit-scrollbar {
            width: calc(var(--space-xs) / 2);
        }

        .messages-container::-webkit-scrollbar-thumb {
            background: var(--accent);
            border-radius: calc(var(--border-radius-s) / 2);
        }

        .message {
            margin-bottom: var(--space-m);
            padding: var(--space-s) var(--space-m);
            border-radius: var(--border-radius-m);
            max-width: 85%;
            animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            word-wrap: break-word;
            line-height: var(--phi);
        }

        @keyframes slideIn {
            from {
                opacity: 0;
                transform: translateY(var(--space-s));
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        .message.user {
            background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
            color: white;
            margin-left: auto;
            text-align: right;
            box-shadow: 0 var(--space-xs) var(--space-m) rgba(102,126,234,0.2);
        }

        .message.bot {
            background: rgba(102,126,234,0.08);
            border: 1px solid rgba(102,126,234,0.15);
        }

        .dark-mode .message.bot {
            background: rgba(102,126,234,0.12);
            border-color: rgba(102,126,234,0.25);
        }

        .message-sources {
            font-size: var(--text-xs);
            opacity: 0.7;
            margin-top: var(--space-xs);
            padding-top: var(--space-xs);
            border-top: 1px solid rgba(102,126,234,0.2);
            font-style: italic;
            display: flex;
            flex-wrap: wrap;
            gap: var(--space-xs);
        }

        .source-link {
            color: var(--accent);
            text-decoration: none;
            transition: all 0.2s;
            padding: var(--space-xxs) var(--space-xs);
            background: rgba(102,126,234,0.1);
            border-radius: var(--border-radius-s);
        }

        .source-link:hover {
            background: rgba(102,126,234,0.2);
            transform: translateY(-1px);
        }

        .read-sources-btn {
            font-size: var(--text-xs);
            padding: var(--space-xxs) var(--space-xs);
            border: 1px solid var(--accent);
            background: transparent;
            color: var(--accent);
            border-radius: var(--border-radius-s);
            cursor: pointer;
            transition: all 0.2s;
            margin-top: var(--space-xxs);
        }

        .read-sources-btn:hover {
            background: var(--accent);
            color: white;
        }

        /* Input Area */
        .input-area {
            display: flex;
            gap: var(--space-s);
            align-items: center;
        }

        .input-field {
            flex: 1;
            padding: var(--space-s) var(--space-m);
            border-radius: calc(var(--space-xxl));
            border: 2px solid transparent;
            background: rgba(102,126,234,0.08);
            font-size: var(--text-m);
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            outline: none;
            font-family: inherit;
            line-height: var(--phi);
        }

        .light-mode .input-field {
            color: var(--text-light);
        }

        .dark-mode .input-field {
            color: var(--text-dark);
        }

        .input-field::placeholder {
            opacity: 0.5;
        }

        .input-field:focus {
            border-color: var(--accent);
            background: rgba(102,126,234,0.12);
            box-shadow: 0 0 0 calc(var(--space-xs) / 2) rgba(102,126,234,0.1);
        }

        .action-btn {
            width: calc(var(--space-l) * var(--phi));
            height: calc(var(--space-l) * var(--phi));
            border-radius: 50%;
            border: none;
            background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
            color: white;
            cursor: pointer;
            display: grid;
            place-items: center;
            font-size: var(--text-l);
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            box-shadow: 0 var(--space-xs) var(--space-m) rgba(102,126,234,0.3);
        }

        .action-btn:hover {
            transform: translateY(calc(var(--space-xxs) * -1));
            box-shadow: 0 var(--space-s) var(--space-l) rgba(102,126,234,0.4);
        }

        .action-btn:active {
            transform: scale(0.95);
        }

        .action-btn.listening {
            animation: pulse 1.5s infinite;
        }

        @keyframes pulse {
            0%, 100% { transform: scale(1); }
            50% { transform: scale(1.15); }
        }

        /* Settings Overlay */
        .settings-overlay {
            position: fixed;
            inset: 0;
            background: rgba(0,0,0,0.6);
            backdrop-filter: blur(var(--space-xs));
            z-index: 999;
            opacity: 0;
            pointer-events: none;
            transition: opacity 0.3s ease;
        }

        .settings-overlay.active {
            opacity: 1;
            pointer-events: all;
        }

        /* Settings Panel */
        .settings-panel {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%) scale(0.9);
            width: min(calc(var(--chat-width) * 0.8), 90vw);
            max-height: 85vh;
            overflow-y: auto;
            padding: var(--space-xl);
            border-radius: var(--border-radius-l);
            backdrop-filter: blur(calc(var(--space-l) * var(--phi)));
            box-shadow: 0 var(--space-l) var(--space-xxl) rgba(0,0,0,0.3);
            z-index: 1000;
            opacity: 0;
            pointer-events: none;
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        }

        .settings-panel.active {
            opacity: 1;
            pointer-events: all;
            transform: translate(-50%, -50%) scale(1);
        }

        .light-mode .settings-panel {
            background: var(--card-light);
            border: 1px solid rgba(0,0,0,0.1);
        }

        .dark-mode .settings-panel {
            background: var(--card-dark);
            border: 1px solid rgba(255,255,255,0.15);
        }

        .settings-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: var(--space-xl);
            padding-bottom: var(--space-m);
            border-bottom: 2px solid rgba(102,126,234,0.2);
        }

        .settings-title {
            font-size: var(--text-l);
            font-weight: 800;
            background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        .close-btn {
            width: calc(var(--space-l) * var(--phi));
            height: calc(var(--space-l) * var(--phi));
            border-radius: 50%;
            border: none;
            background: rgba(248,113,113,0.1);
            color: var(--error);
            cursor: pointer;
            display: grid;
            place-items: center;
            font-size: var(--text-l);
            transition: all 0.2s;
        }

        .close-btn:hover {
            background: rgba(248,113,113,0.2);
            transform: scale(1.15) rotate(90deg);
        }

        .settings-section {
            margin-bottom: var(--space-l);
        }

        .section-title {
            font-size: var(--text-m);
            font-weight: 700;
            color: var(--accent);
            margin-bottom: var(--space-m);
            display: flex;
            align-items: center;
            gap: var(--space-s);
        }

        .setting-row {
            margin-bottom: var(--space-m);
        }

        .setting-label {
            display: block;
            font-size: var(--text-s);
            font-weight: 600;
            margin-bottom: var(--space-xs);
            opacity: 0.9;
        }

        .setting-description {
            font-size: var(--text-xs);
            opacity: 0.6;
            margin-bottom: var(--space-xs);
        }

        .setting-select,
        .setting-input {
            width: 100%;
            padding: var(--space-s) var(--space-m);
            border-radius: var(--border-radius-m);
            border: 1px solid rgba(102,126,234,0.2);
            background: rgba(102,126,234,0.08);
            font-size: var(--text-s);
            cursor: pointer;
            outline: none;
            font-family: inherit;
            transition: all 0.2s;
        }

        .light-mode .setting-select,
        .light-mode .setting-input {
            color: var(--text-light);
        }

        .dark-mode .setting-select,
        .dark-mode .setting-input {
            color: var(--text-dark);
        }

        .setting-select:hover,
        .setting-input:hover {
            border-color: var(--accent);
        }

        .setting-select:focus,
        .setting-input:focus {
            border-color: var(--accent);
            background: rgba(102,126,234,0.12);
        }

        .toggle-group {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(calc(var(--space-xxl) * 1.5), 1fr));
            gap: var(--space-xs);
        }

        .toggle-btn {
            padding: var(--space-s);
            border-radius: var(--border-radius-m);
            border: 2px solid transparent;
            background: rgba(102,126,234,0.08);
            cursor: pointer;
            transition: all 0.2s;
            font-weight: 600;
            font-size: var(--text-s);
            text-align: center;
        }

        .light-mode .toggle-btn {
            color: var(--text-light);
        }

        .dark-mode .toggle-btn {
            color: var(--text-dark);
        }

        .toggle-btn.active {
            border-color: var(--accent);
            background: rgba(102,126,234,0.2);
            color: var(--accent);
            transform: scale(1.02);
        }

        .toggle-btn:hover:not(.active) {
            background: rgba(102,126,234,0.15);
        }

        .checkbox-wrapper {
            display: flex;
            align-items: center;
            gap: var(--space-s);
            padding: var(--space-s);
            background: rgba(102,126,234,0.05);
            border-radius: var(--border-radius-m);
            cursor: pointer;
            transition: all 0.2s;
        }

        .checkbox-wrapper:hover {
            background: rgba(102,126,234,0.1);
        }

        .checkbox-wrapper input[type="checkbox"] {
            width: calc(var(--space-m) * var(--phi));
            height: calc(var(--space-m) * var(--phi));
            cursor: pointer;
        }

        .slider-container {
            display: flex;
            align-items: center;
            gap: var(--space-m);
        }

        .slider {
            flex: 1;
            height: var(--space-xs);
            border-radius: calc(var(--space-xs) / 2);
            background: rgba(102,126,234,0.2);
            outline: none;
            cursor: pointer;
        }

        .slider-value {
            min-width: calc(var(--space-xl));
            text-align: center;
            font-weight: 700;
            color: var(--accent);
        }

        /* Responsive */
        @media (max-width: 768px) {
            :root {
                --unit: 16px;
            }

            .chat-interface {
                padding: var(--space-m);
            }

            .settings-panel {
                padding: var(--space-l);
            }

            .toggle-group {
                grid-template-columns: 1fr;
            }
        }

        /* Accessibility */
        @media (prefers-reduced-motion: reduce) {
            *, *::before, *::after {
                animation-duration: 0.01ms !important;
                animation-iteration-count: 1 !important;
                transition-duration: 0.01ms !important;
            }
        }

        .sr-only {
            position: absolute;
            width: 1px;
            height: 1px;
            padding: 0;
            margin: -1px;
            overflow: hidden;
            clip: rect(0,0,0,0);
            white-space: nowrap;
            border: 0;
        }
    </style>
</head>
<body class="light-mode">
    <div class="chat-container">
        <article class="chat-interface">
            <header class="chat-header">
                <h1 class="chat-title">🧠 Curiosity</h1>
                <div class="header-actions">
                    <button class="icon-btn danger" onclick="clearConversation()" 
                            aria-label="Effacer" title="Effacer la conversation">🗑️</button>
                    <button class="icon-btn" onclick="openSettings()" 
                            aria-label="Paramètres" title="Paramètres">⚙️</button>
                </div>
            </header>

            <div id="messages" class="messages-container" role="log" aria-live="polite">
                <div class="message bot">
                    👋 <strong>Bonjour,</strong> Je suis Curiosity, votre oracle culturel français. 
                    <br>Posez-moi n'importe quelle question !
                </div>
            </div>

            <form class="input-area" onsubmit="event.preventDefault(); sendMessage();">
                <input type="text" id="inputField" class="input-field" 
                       placeholder="Posez votre question..." 
                       aria-label="Question">
                <button type="button" class="action-btn" id="micBtn" 
                        onclick="toggleVoice()" aria-label="Vocal">🎤</button>
                <button type="submit" class="action-btn" aria-label="Envoyer">▶️</button>
            </form>
        </article>
    </div>

    <!-- Settings Overlay -->
    <div class="settings-overlay" id="settingsOverlay" onclick="closeSettings()"></div>
    
    <!-- Settings Panel -->
    <aside class="settings-panel" id="settingsPanel" role="dialog" aria-modal="true">
        <header class="settings-header">
            <h2 class="settings-title">⚙️ Réglages Avancés</h2>
            <button class="close-btn" onclick="closeSettings()" aria-label="Fermer">✕</button>
        </header>

        <!-- SECTION: Apparence -->
        <section class="settings-section">
            <h3 class="section-title">🎨 Apparence</h3>
            
            <div class="setting-row">
                <label class="setting-label">Thème</label>
                <div class="setting-description">Choisissez le thème d'affichage</div>
                <div class="toggle-group">
                    <button class="toggle-btn" id="autoThemeBtn" onclick="setTheme('auto')">Auto</button>
                    <button class="toggle-btn active" id="lightThemeBtn" onclick="setTheme('light')">☀️ Clair</button>
                    <button class="toggle-btn" id="darkThemeBtn" onclick="setTheme('dark')">🌙 Sombre</button>
                </div>
            </div>

            <div class="setting-row">
                <label class="setting-label">Affichage du personnage</label>
                <div class="setting-description">Montrer/cacher Curiosity</div>
                <div class="checkbox-wrapper" onclick="toggleViewerDisplay(this)">
                    <input type="checkbox" id="showViewer" checked>
                    <label for="showViewer">Afficher le personnage Curiosity</label>
                </div>
            </div>

            <div class="setting-row">
                <label class="setting-label">Taille de la fenêtre</label>
                <div class="setting-description">Ajuster la taille selon vos préférences</div>
                <div class="slider-container">
                    <input type="range" class="slider" id="chatSize" min="0.8" max="1.5" step="0.1" value="1" 
                           oninput="updateChatSize(this.value)">
                    <span class="slider-value" id="chatSizeValue">100%</span>
                </div>
            </div>
        </section>

        <!-- SECTION: Voix -->
        <section class="settings-section">
            <h3 class="section-title">🎤 Synthèse Vocale</h3>
            
            <div class="setting-row">
                <label class="setting-label" for="voiceSelect">Voix française</label>
                <div class="setting-description">Sélectionnez la voix de lecture</div>
                <select id="voiceSelect" class="setting-select"></select>
            </div>

            <div class="setting-row">
                <label class="setting-label">Vitesse de lecture</label>
                <div class="slider-container">
                    <input type="range" class="slider" id="speechRate" min="0.5" max="2" step="0.1" value="1" 
                           oninput="updateSpeechRate(this.value)">
                    <span class="slider-value" id="speechRateValue">1.0x</span>
                </div>
            </div>

            <div class="setting-row">
                <div class="checkbox-wrapper" onclick="toggleAutoSpeak(this)">
                    <input type="checkbox" id="autoSpeak" checked>
                    <label for="autoSpeak">Lecture automatique des réponses</label>
                </div>
            </div>

            <div class="setting-row">
                <div class="checkbox-wrapper" onclick="toggleReadSources(this)">
                    <input type="checkbox" id="readSources">
                    <label for="readSources">Lire les sources à voix haute</label>
                </div>
            </div>
        </section>

        <!-- SECTION: Conversation -->
        <section class="settings-section">
            <h3 class="section-title">💬 Conversation</h3>
            
            <div class="setting-row">
                <label class="setting-label" for="modeSelect">Mode de réponse</label>
                <div class="setting-description">Longueur des réponses de Curiosity</div>
                <select id="modeSelect" class="setting-select">
                    <option value="concise">Concis (2 phrases)</option>
                    <option value="detailed">Détaillé (5 phrases)</option>
                    <option value="comprehensive">Complet (10+ phrases)</option>
                </select>
            </div>

            <div class="setting-row">
                <label class="setting-label">Historique conservé</label>
                <div class="setting-description">Nombre d'échanges mémorisés</div>
                <div class="slider-container">
                    <input type="range" class="slider" id="historySize" min="3" max="20" step="1" value="10" 
                           oninput="updateHistorySize(this.value)">
                    <span class="slider-value" id="historySizeValue">10</span>
                </div>
            </div>

            <div class="setting-row">
                <div class="checkbox-wrapper" onclick="togglePersistence(this)">
                    <input type="checkbox" id="persistence" checked>
                    <label for="persistence">Sauvegarder l'historique (localStorage)</label>
                </div>
            </div>
        </section>

        <!-- SECTION: Avancé -->
        <section class="settings-section">
            <h3 class="section-title">🔧 Avancé</h3>
            
            <div class="setting-row">
                <label class="setting-label" for="apiUrl">URL de l'API</label>
                <div class="setting-description">Ne modifiez que si nécessaire</div>
                <input type="text" id="apiUrl" class="setting-input" 
                       value="https://curiosity-silk.vercel.app/api/chat" 
                       placeholder="URL de l'API">
            </div>

            <div class="setting-row">
                <label class="setting-label">Délai d'affichage des yeux</label>
                <div class="setting-description">Durée d'affichage des recherches (secondes)</div>
                <div class="slider-container">
                    <input type="range" class="slider" id="eyesDelay" min="3" max="15" step="1" value="8" 
                           oninput="updateEyesDelay(this.value)">
                    <span class="slider-value" id="eyesDelayValue">8s</span>
                </div>
            </div>

            <div class="setting-row">
                <div class="checkbox-wrapper" onclick="toggleParallax(this)">
                    <input type="checkbox" id="parallax" checked>
                    <label for="parallax">Effet parallax sur le personnage</label>
                </div>
            </div>

            <div class="setting-row">
                <div class="checkbox-wrapper" onclick="toggleDebug(this)">
                    <input type="checkbox" id="debug">
                    <label for="debug">Mode debug (console)</label>
                </div>
            </div>
        </section>

        <button class="action-btn" style="width: 100%; border-radius: var(--border-radius-m); margin-top: var(--space-l);" 
                onclick="saveSettings()">
            💾 Sauvegarder les paramètres
        </button>
    </aside>

    <script>
        const API_URL = 'https://curiosity-silk.vercel.app/api/chat';
        
        let config = {
            theme: 'light',
            showViewer: true,
            chatSize: 1,
            speechRate: 1,
            autoSpeak: true,
            readSources: false,
            mode: 'concise',
            historySize: 10,
            persistence: true,
            apiUrl: API_URL,
            eyesDelay: 8,
            parallax: true,
            debug: false
};
let recognition, isListening = false, history = [], voices = [], selectedVoice;
    let viewerWindow = null;

    // ============================================
    // INIT & PERSISTENCE
    // ============================================
    function loadSettings() {
        try {
            const saved = localStorage.getItem('curiosity-config');
            if (saved) {
                config = { ...config, ...JSON.parse(saved) };
                applySettings();
            }
        } catch (e) {
            console.warn('Could not load settings:', e);
        }
    }

    function saveSettings() {
        try {
            localStorage.setItem('curiosity-config', JSON.stringify(config));
            alert('✅ Paramètres sauvegardés !');
            applySettings();
        } catch (e) {
            console.error('Could not save settings:', e);
            alert('❌ Erreur lors de la sauvegarde');
        }
    }

    function applySettings() {
        // Thème
        setTheme(config.theme);
        
        // Taille chat
        updateChatSize(config.chatSize);
        document.getElementById('chatSize').value = config.chatSize;
        
        // Voix
        document.getElementById('speechRate').value = config.speechRate;
        document.getElementById('speechRateValue').textContent = config.speechRate + 'x';
        
        // Checkboxes
        document.getElementById('autoSpeak').checked = config.autoSpeak;
        document.getElementById('readSources').checked = config.readSources;
        document.getElementById('persistence').checked = config.persistence;
        document.getElementById('showViewer').checked = config.showViewer;
        document.getElementById('parallax').checked = config.parallax;
        document.getElementById('debug').checked = config.debug;
        
        // Selects
        document.getElementById('modeSelect').value = config.mode;
        document.getElementById('apiUrl').value = config.apiUrl;
        
        // Sliders
        document.getElementById('historySize').value = config.historySize;
        document.getElementById('historySizeValue').textContent = config.historySize;
        document.getElementById('eyesDelay').value = config.eyesDelay;
        document.getElementById('eyesDelayValue').textContent = config.eyesDelay + 's';
    }

    function loadConversation() {
        if (!config.persistence) return;
        
        try {
            const savedHistory = localStorage.getItem('curiosity-history');
            const savedMessages = localStorage.getItem('curiosity-messages');
            
            if (savedHistory) {
                history = JSON.parse(savedHistory);
            }
            
            if (savedMessages) {
                document.getElementById('messages').innerHTML = savedMessages;
            }
        } catch (e) {
            if (config.debug) console.warn('Could not load conversation:', e);
        }
    }

    function saveConversation() {
        if (!config.persistence) return;
        
        try {
            localStorage.setItem('curiosity-history', JSON.stringify(history));
            localStorage.setItem('curiosity-messages', document.getElementById('messages').innerHTML);
        } catch (e) {
            if (config.debug) console.warn('Could not save conversation:', e);
        }
    }

    function clearConversation() {
        if (confirm('Effacer toute la conversation ?')) {
            history = [];
            localStorage.removeItem('curiosity-history');
            localStorage.removeItem('curiosity-messages');
            
            document.getElementById('messages').innerHTML = `
                <div class="message bot">
                    👋 <strong>Bonjour !</strong> Je suis Curiosity, votre oracle français. Météo, heure, Bitcoin, recettes, histoire... Posez-moi n'importe quelle question !
                </div>
            `;
        }
    }

    // ============================================
    // THEME
    // ============================================
    function detectTheme() {
        return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
    }

    function setTheme(mode) {
        config.theme = mode;
        document.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active'));
        document.getElementById(mode + 'ThemeBtn').classList.add('active');
        document.body.className = (mode === 'auto' ? detectTheme() : mode) + '-mode';
    }

    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
        if(config.theme === 'auto') setTheme('auto');
    });

    // ============================================
    // VOICE RECOGNITION
    // ============================================
    if('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
        const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
        recognition = new SR();
        recognition.lang = 'fr-FR';
        recognition.onresult = e => {
            document.getElementById('inputField').value = e.results[0][0].transcript;
            sendMessage();
        };
        recognition.onend = () => stopListening();
    }

    function toggleVoice() {
        if(!recognition) return alert('❌ Non supporté');
        isListening ? stopListening() : startListening();
    }

    function startListening() {
        isListening = true;
        document.getElementById('micBtn').classList.add('listening');
        recognition.start();
    }

    function stopListening() {
        isListening = false;
        document.getElementById('micBtn').classList.remove('listening');
        if(recognition) recognition.stop();
    }

    // ============================================
    // VOICE SYNTHESIS
    // ============================================
    function loadVoices() {
        voices = speechSynthesis.getVoices().filter(v => v.lang.startsWith('fr'));
        const sel = document.getElementById('voiceSelect');
        sel.innerHTML = '';
        voices.forEach((v, i) => {
            const opt = document.createElement('option');
            opt.value = i;
            opt.textContent = `${v.name} (${v.lang})`;
            sel.appendChild(opt);
        });
        const pref = voices.findIndex(v => v.name.includes('Thomas') || v.name.includes('French'));
        if(pref !== -1) {
            sel.value = pref;
            selectedVoice = voices[pref];
        }
    }

    speechSynthesis.onvoiceschanged = loadVoices;
    loadVoices();
    document.getElementById('voiceSelect').onchange = e => selectedVoice = voices[e.target.value];

    function cleanTextForSpeech(text) {
        // Supprimer les caractères markdown: ** // _ ~~
        return text
            .replace(/\*\*/g, '')
            .replace(/\*/g, '')
            .replace(/\_\_/g, '')
            .replace(/\_/g, '')
            .replace(/\~\~/g, '')
            .replace(/\`\`\`/g, '')
            .replace(/\`/g, '')
            .replace(/\#\#\#/g, '')
            .replace(/\#\#/g, '')
            .replace(/\#/g, '')
            .replace(/\//g, '')
            .trim();
    }

    function speak(text, includeSources = false) {
        if (!config.autoSpeak) return;
        if (!('speechSynthesis' in window)) return;

        const cleanText = cleanTextForSpeech(text);
        
        const utterance = new SpeechSynthesisUtterance(cleanText);
        utterance.lang = 'fr-FR';
        utterance.rate = config.speechRate;
        utterance.pitch = 1.0;
        if(selectedVoice) utterance.voice = selectedVoice;
        
        speechSynthesis.speak(utterance);
    }

    function speakSources(sources) {
        if (!config.readSources || !sources || !sources.length) return;
        
        const sourcesText = `Sources consultées: ${sources.map((s, i) => {
            const domain = s.match(/https?:\/\/([^\/]+)/)?.[1] || s;
            return `source ${i + 1}: ${domain}`;
        }).join(', ')}`;
        
        setTimeout(() => {
            speak(sourcesText);
        }, 1000);
    }

    // ============================================
    // VIEWER COMMUNICATION
    // ============================================
    function sendToViewer(action, data) {
        if (!config.showViewer) return;
        
        // Si viewer dans iframe
        const viewerFrame = parent.document.querySelector('#curiosity-viewer');
        if (viewerFrame && viewerFrame.contentWindow) {
            viewerFrame.contentWindow.postMessage({ action, data }, '*');
        }
        
        // Si viewer dans window séparée
        if (viewerWindow && !viewerWindow.closed) {
            viewerWindow.postMessage({ action, data }, '*');
        }
    }

    // ============================================
    // MESSAGES
    // ============================================
    function addMsg(text, type, sources) {
        const msgs = document.getElementById('messages');
        const div = document.createElement('div');
        div.className = `message ${type}`;
        let html = text;
        
        if(sources?.length) {
            const sourceLinks = sources.map((s, i) => {
                const domain = s.match(/https?:\/\/([^\/]+)/)?.[1] || `Source ${i + 1}`;
                return `<a href="${s}" class="source-link" target="_blank" rel="noopener">${domain}</a>`;
            }).join('');
            
            html += `<div class="message-sources">
                📚 ${sourceLinks}
                ${!config.readSources ? '<button class="read-sources-btn" onclick="speakSources(' + JSON.stringify(sources) + ')">🔊 Lire les sources</button>' : ''}
            </div>`;
        }
        
        div.innerHTML = html;
        msgs.appendChild(div);
        msgs.scrollTop = msgs.scrollHeight;
        
        saveConversation();
    }

    // ============================================
    // SEND MESSAGE
    // ============================================
    async function sendMessage() {
        const input = document.getElementById('inputField');
        const msg = input.value.trim();
        if(!msg) return;

        addMsg(msg, 'user');
        history.push({role: 'user', content: msg});
        if(history.length > config.historySize * 2) {
            history = history.slice(-config.historySize * 2);
        }
        input.value = '';

        // Viewer
        sendToViewer('setState', 'THINKING');
        sendToViewer('setStatus', 'thinking');
        sendToViewer('showCode', msg);

        try {
            const maxSentences = config.mode === 'concise' ? 2 : config.mode === 'detailed' ? 5 : 10;
            
            const res = await fetch(config.apiUrl, {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({
                    question: msg,
                    max_sentences: maxSentences,
                    history: history.slice(-config.historySize)
                })
            });

            if(!res.ok) throw new Error();

            const data = await res.json();
            history.push({role: 'assistant', content: data.answer});

            addMsg(data.answer, 'bot', data.sources);

            // Aperçu dans l'œil droit
            if (data.sources && data.sources.length > 0) {
                sendToViewer('showPreview', data.sources[0]);
                
                setTimeout(() => {
                    sendToViewer('clearEyes');
                }, config.eyesDelay * 1000);
            }

            // Lecture vocale
            speak(data.answer);
            if (config.readSources) {
                speakSources(data.sources);
            }

            // Viewer
            sendToViewer('setState', 'GOOD');
            sendToViewer('setStatus', '');

        } catch(err) {
            if (config.debug) console.error(err);
            addMsg('❌ Erreur. Réessayez.', 'bot');
            sendToViewer('setState', 'BAD');
            sendToViewer('setStatus', 'error');
            sendToViewer('clearEyes');
        }
    }

    // ============================================
    // SETTINGS
    // ============================================
    function openSettings() {
        document.getElementById('settingsPanel').classList.add('active');
        document.getElementById('settingsOverlay').classList.add('active');
    }

    function closeSettings() {
        document.getElementById('settingsPanel').classList.remove('active');
        document.getElementById('settingsOverlay').classList.remove('active');
    }

    function updateChatSize(value) {
        config.chatSize = parseFloat(value);
        document.getElementById('chatSizeValue').textContent = Math.round(value * 100) + '%';
        document.documentElement.style.setProperty('--chat-width', `${600 * value}px`);
    }

    function updateSpeechRate(value) {
        config.speechRate = parseFloat(value);
        document.getElementById('speechRateValue').textContent = value + 'x';
    }

    function updateHistorySize(value) {
        config.historySize = parseInt(value);
        document.getElementById('historySizeValue').textContent = value;
    }

    function updateEyesDelay(value) {
        config.eyesDelay = parseInt(value);
        document.getElementById('eyesDelayValue').textContent = value + 's';
    }

    function toggleViewerDisplay(wrapper) {
        config.showViewer = wrapper.querySelector('input').checked;
    }

    function toggleAutoSpeak(wrapper) {
        config.autoSpeak = wrapper.querySelector('input').checked;
    }

    function toggleReadSources(wrapper) {
        config.readSources = wrapper.querySelector('input').checked;
    }

    function togglePersistence(wrapper) {
        config.persistence = wrapper.querySelector('input').checked;
    }

    function toggleParallax(wrapper) {
        config.parallax = wrapper.querySelector('input').checked;
        sendToViewer('toggleParallax', config.parallax);
    }

    function toggleDebug(wrapper) {
        config.debug = wrapper.querySelector('input').checked;
    }

    // ============================================
    // INIT
    // ============================================
    loadSettings();
    loadConversation();
    setTheme(config.theme);

    // Initialiser le viewer
    sendToViewer('setState', 'OFF');
    setTimeout(() => {
        sendToViewer('setState', 'ON');
        sendToViewer('setStatus', '');
    }, 1000);
</script>
</body>
</html>				</div>
				</div>
					</div>
				</div>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/curiosity/">Curiosity</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>SPLATGATE⟶⦗⦘</title>
		<link>https://presentcomposedesign.fr/splatgate/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Sat, 15 Nov 2025 10:19:01 +0000</pubDate>
				<category><![CDATA[[ia]]]></category>
		<category><![CDATA[[VR]]]></category>
		<category><![CDATA[Design d'espace]]></category>
		<category><![CDATA[Design produit]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=34089</guid>

					<description><![CDATA[<p>Embarquez pour un voyage immersif inédit avec SPLAT‎GATE, le portail WebXR de Présent Composé Design. Alliant la puissance du Gaussian Splatting à une accessibilité totale sans application, ce hub redéfinit l'exploration spatiale directement depuis votre navigateur. Découvrez notre candidature officielle pour le prix international #SplatOfTheYear aux #ThePolys et soutenez l'innovation française dans le futur du web 3D.</p>
<p>Cet article <a href="https://presentcomposedesign.fr/splatgate/">SPLATGATE⟶⦗⦘</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="34089" class="elementor elementor-34089">
						<section class="elementor-section elementor-top-section elementor-element elementor-element-e54cb28 elementor-section-full_width elementor-section-height-min-height elementor-section-items-bottom elementor-section-content-middle elementor-section-stretched elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no" data-id="e54cb28" data-element_type="section" data-e-type="section" data-settings="{&quot;stretch_section&quot;:&quot;section-stretched&quot;,&quot;background_background&quot;:&quot;classic&quot;}">
						<div class="elementor-container elementor-column-gap-extended">
					<div class="elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-d602f2f" data-id="d602f2f" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<div class="elementor-element elementor-element-4fd7d3a elementor-widget__width-inherit elementor-widget elementor-widget-html" data-id="4fd7d3a" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<style>
    /* Section plein écran sans aucune bordure ou marge parasite */
    .splatgate-hero-video-only {
        position: relative;
        width: 100%;
        height: 70vh; /* Remplit 100% de la hauteur de la fenêtre */
        margin: 0;
        padding: 0;
        overflow: hidden;
        background-color: #000; /* Fond noir de secours */
    }

    /* La vidéo s'adapte parfaitement à tout l'espace disponible */
    .hero-video-full {
        width: 100%;
        height: 100%;
        object-fit: cover; /* Garantit que la vidéo couvre tout l'espace sans déformation */
        display: block;
    }

    /* Ajustement de la hauteur pour le mobile si besoin d'apercevoir le titre dessous */
    @media (max-width: 768px) {
        .splatgate-hero-video-only {
            height: 70vh; /* Permet de voir le début du titre suivant sur un petit écran */
        }
    }
</style>

<section class="splatgate-hero-video-only">
    <video 
        autoplay 
        muted 
        loop 
        playsinline 
        class="hero-video-full">
        <source src="https://presentcomposedesign.fr/wp-content/uploads/2025/11/video_presentation_SPLATGATE_PresentComposedesign-2025.mp4" type="video/mp4">
        Votre navigateur ne supporte pas la lecture de vidéos.
    </video>
</section>				</div>
				</div>
				<section class="elementor-section elementor-inner-section elementor-element elementor-element-14759a4 elementor-section-boxed elementor-section-height-default elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no" data-id="14759a4" data-element_type="section" data-e-type="section">
						<div class="elementor-container elementor-column-gap-default">
					<div class="elementor-column elementor-col-100 elementor-inner-column elementor-element elementor-element-43ed930" data-id="43ed930" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<div class="elementor-element elementor-element-8fda2db splatgate-title elementor-widget elementor-widget-heading" data-id="8fda2db" data-element_type="widget" data-e-type="widget" data-widget_type="heading.default">
				<div class="elementor-widget-container">
					<h1 class="elementor-heading-title elementor-size-default">SPLATGATE ⟶⦗⦘</h1>				</div>
				</div>
					</div>
		</div>
					</div>
		</section>
					</div>
		</div>
					</div>
		</section>
				<section class="elementor-section elementor-top-section elementor-element elementor-element-5705354 elementor-section-full_width elementor-section-height-min-height elementor-section-items-bottom elementor-section-content-middle elementor-section-stretched elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no elementor-invisible" data-id="5705354" data-element_type="section" data-e-type="section" data-settings="{&quot;stretch_section&quot;:&quot;section-stretched&quot;,&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;fadeInUp&quot;}">
						<div class="elementor-container elementor-column-gap-extended">
					<div class="elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-85a3763" data-id="85a3763" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<section class="elementor-section elementor-inner-section elementor-element elementor-element-2ea17bf elementor-section-boxed elementor-section-height-default elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no" data-id="2ea17bf" data-element_type="section" data-e-type="section">
						<div class="elementor-container elementor-column-gap-default">
					<div class="elementor-column elementor-col-50 elementor-inner-column elementor-element elementor-element-fa870ea" data-id="fa870ea" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<div class="elementor-element elementor-element-d894641 elementor-widget elementor-widget-image" data-id="d894641" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img fetchpriority="high" decoding="async" width="1000" height="1000" src="https://presentcomposedesign.fr/wp-content/uploads/2025/11/SplatGate_Present-Compose-design-UHD2025_square.jpg" class="attachment-full size-full wp-image-35342" alt="Visuel présentation Splatgate" srcset="https://presentcomposedesign.fr/wp-content/uploads/2025/11/SplatGate_Present-Compose-design-UHD2025_square.jpg 1000w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/SplatGate_Present-Compose-design-UHD2025_square-300x300.jpg 300w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/SplatGate_Present-Compose-design-UHD2025_square-150x150.jpg 150w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/SplatGate_Present-Compose-design-UHD2025_square-768x768.jpg 768w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/SplatGate_Present-Compose-design-UHD2025_square-600x600.jpg 600w" sizes="(max-width: 1000px) 100vw, 1000px" />															</div>
				</div>
					</div>
		</div>
				<div class="elementor-column elementor-col-50 elementor-inner-column elementor-element elementor-element-dd36fbf" data-id="dd36fbf" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<div class="elementor-element elementor-element-9570f92 elementor-widget__width-initial elementor-widget elementor-widget-text-editor" data-id="9570f92" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<p><style>
    .emoji-container {<br />        display: inline-block;<br />        width: 0.8em; /* Taille du conteneur ajustée pour mieux voir l'effet */<br />        height: 0.8em;<br />        margin-right: 0.2em;<br />        position: relative;<br />        background: linear-gradient(45deg, #ff00ff, #00ffff, #ff00ff); /* Dégradé à 45 degrés */<br />        border-radius: 50%;<br />        vertical-align: middle;<br />        animation: colorchange 6s infinite;<br />    }</p>
<p>    .inner-circle {<br />        width: 50%; /* Taille du cercle blanc par rapport au conteneur */<br />        height: 20%;<br />        border-radius: 50%;<br />        background-color: white;<br />        position: relative;<br />        top: 50%; /* Ajuste la position verticale pour centrer */<br />        left: 50%; /* Ajuste la position horizontale pour centrer */<br />        animation: rotate 20s linear infinite; /* Animation de rotation */<br />        transform-origin: center center; /* Point de rotation au centre */<br />    }</p>
<p>    @keyframes colorchange {<br />        0% { filter: hue-rotate(0deg); }<br />        100% { filter: hue-rotate(360deg); }<br />    }</p>
<p>    @keyframes rotate {<br />        0% { transform: translate(-50%, -50%) rotate(0deg); }<br />        100% { transform: translate(-50%, -50%) rotate(360deg); }<br />    }<br /></style></p><div class="inner-circle"> </div><h1><span style="color: #0010ef;">Votre passeport VR pour ailleurs</span><span style="color: #0010ef; font-size: 16px;"> </span></h1><div class="vce-row-container" data-vce-boxed-width="true"><div id="el-ba3f564d" class="vce-row vce-row--col-gap-30 vce-row-equal-height vce-row-content--top" data-vce-do-apply="all el-ba3f564d"><div class="vce-row-content" data-vce-element-content="true"><div id="el-11f4390c" class="vce-col vce-col--md-auto vce-col--xs-1 vce-col--xs-last vce-col--xs-first vce-col--sm-last vce-col--sm-first vce-col--md-last vce-col--lg-last vce-col--xl-last vce-col--md-first vce-col--lg-first vce-col--xl-first"><div class="vce-col-inner" data-vce-do-apply="border margin background el-11f4390c"><div class="vce-col-content" data-vce-element-content="true" data-vce-do-apply="padding el-11f4390c"><div class="vce-google-fonts-heading vce-google-fonts-heading--align-left vce-google-fonts-heading--color-b-0-16-255--45--5C00FF--FF7200 vce-google-fonts-heading--font-family-Roboto"><div id="el-6e30c05c" class="vce-google-fonts-heading-wrapper"><div><span style="color: #0010ef;"> </span></div><div><p><span style="color: #0010ef;">SPLATGATE ⟶⦗⦘ n’est pas un jeu : c’est un portail.</span></p></div><h2><span style="color: #000000;">Ce hub immersif vous permet d’explorer des paysages réalistes en réalité virtuelle depuis votre ordinateur, votre smartphone ou votre casque XR.<br data-start="350" data-end="353" /><br />Installez‑vous confortablement, mettez votre casque (ou utilisez l’interface en version bureau, tablette ou téléphone) et laissez‑vous transporter vers des destinations d’exception.</span></h2><p>« Voyagez » seul pour vous ressourcer ou partagez l’expérience entre amis ; l’accès est gratuit et illimité.</p></div></div><div class="vce-text-block"><div id="el-93e454cf" class="vce-text-block-wrapper vce" data-vce-do-apply="all el-93e454cf" data-vce-animate="vce-o-animate--fadeInDown"> </div></div></div></div></div></div></div></div><div class="vce-row-container" data-vce-boxed-width="true"><div id="el-d9d9a9fd" class="vce-row vce-row--col-gap-30 vce-row-equal-height vce-row-content--top" data-vce-do-apply="all el-d9d9a9fd"><div class="vce-row-content" data-vce-element-content="true"><div id="el-87440d41" class="vce-col vce-col--md-auto vce-col--xs-1 vce-col--xs-last vce-col--xs-first vce-col--sm-last vce-col--sm-first vce-col--md-last vce-col--lg-last vce-col--xl-last vce-col--md-first vce-col--lg-first vce-col--xl-first"><div class="vce-col-inner" data-vce-do-apply="border margin background el-87440d41"><div class="vce-col-content" data-vce-element-content="true" data-vce-do-apply="padding el-87440d41"><p><!-- /wp:vcwb/empty-comment-element-wrapper --></p></div></div></div><p><!-- /vcwb/dynamicElementComment:87440d41 --></p></div></div></div><p><!-- /vcwb/dynamicElementComment:d9d9a9fd --><!--vcv no format--></p>								</div>
				</div>
					</div>
		</div>
					</div>
		</section>
					</div>
		</div>
					</div>
		</section>
		<div class="wpr-jarallax elementor-element elementor-element-5fe6ffb e-con-full wpr-jarallax-yes e-flex wpr-particle-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent" speed-data="1.4" bg-image="https://presentcomposedesign.fr/wp-content/uploads/2025/11/SplatGate_Present-Compose-design-UHD2025-2_2553x1594.jpg" bg-image-mobile="" scroll-effect="scroll" data-id="5fe6ffb" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
				<div class="elementor-element elementor-element-8ce9abf elementor-widget elementor-widget-spacer" data-id="8ce9abf" data-element_type="widget" data-e-type="widget" data-widget_type="spacer.default">
				<div class="elementor-widget-container">
							<div class="elementor-spacer">
			<div class="elementor-spacer-inner"></div>
		</div>
						</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-c8759f0 e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no elementor-invisible e-con e-parent" data-id="c8759f0" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;slideInRight&quot;}">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-86a7112 elementor-arrows-position-outside elementor-pagination-position-outside elementor-widget elementor-widget-image-carousel" data-id="86a7112" data-element_type="widget" data-e-type="widget" data-settings="{&quot;slides_to_show&quot;:&quot;3&quot;,&quot;autoplay_speed&quot;:1800,&quot;speed&quot;:350,&quot;slides_to_scroll&quot;:&quot;1&quot;,&quot;image_spacing_custom&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;size&quot;:100,&quot;sizes&quot;:[]},&quot;navigation&quot;:&quot;both&quot;,&quot;autoplay&quot;:&quot;yes&quot;,&quot;pause_on_hover&quot;:&quot;yes&quot;,&quot;pause_on_interaction&quot;:&quot;yes&quot;,&quot;infinite&quot;:&quot;yes&quot;,&quot;image_spacing_custom_tablet&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;size&quot;:&quot;&quot;,&quot;sizes&quot;:[]},&quot;image_spacing_custom_mobile&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;size&quot;:&quot;&quot;,&quot;sizes&quot;:[]}}" data-widget_type="image-carousel.default">
				<div class="elementor-widget-container">
							<div class="elementor-image-carousel-wrapper swiper" role="region" aria-roledescription="carousel" aria-label="Carrousel d’images" dir="ltr">
			<div class="elementor-image-carousel swiper-wrapper" aria-live="off">
								<div class="swiper-slide" role="group" aria-roledescription="slide" aria-label="1 sur 5"><figure class="swiper-slide-inner"><img decoding="async" class="swiper-slide-image" src="https://presentcomposedesign.fr/wp-content/uploads/2025/11/Broceliande-Present-Compose-design-360-600x600.png" alt="Vue 360° forêt Brocéliande avec cercle de pierres, immersion en réalité virtuelle, Présent Composé design pour_SPLATGATE ⟶⦗⦘" /><figcaption class="elementor-image-carousel-caption">Broceliande ⟶⦗⦘</figcaption></figure></div><div class="swiper-slide" role="group" aria-roledescription="slide" aria-label="2 sur 5"><figure class="swiper-slide-inner"><img decoding="async" class="swiper-slide-image" src="https://presentcomposedesign.fr/wp-content/uploads/2025/11/GRAND-CANYON-Present-Compose-design-HD-360-600x600.png" alt="Vue 360° Grand Canyon réaliste en réalité virtuelle, lumière naturelle filtrée, Présent Composé design pour SPLATGATE ⟶⦗⦘" /><figcaption class="elementor-image-carousel-caption">GRAND-CANYON ⟶⦗⦘</figcaption></figure></div><div class="swiper-slide" role="group" aria-roledescription="slide" aria-label="3 sur 5"><figure class="swiper-slide-inner"><img decoding="async" class="swiper-slide-image" src="https://presentcomposedesign.fr/wp-content/uploads/2025/11/Rail-Road_Present-Compose-design-360-600x600.png" alt="Vue 360° de rails enneigés sous un ciel orageux, voyage virtuel SPLATGATE ⟶⦗⦘ Présent Composé design" /><figcaption class="elementor-image-carousel-caption">Rail-Road ⟶⦗⦘</figcaption></figure></div><div class="swiper-slide" role="group" aria-roledescription="slide" aria-label="4 sur 5"><figure class="swiper-slide-inner"><img decoding="async" class="swiper-slide-image" src="https://presentcomposedesign.fr/wp-content/uploads/2025/11/SALAR-DE-UYUNI-Present-Compose-design-360-600x600.png" alt="Vue 360° du Salar d’Uyuni, désert de sel sous ciel bleu, Présent Composé design pour SPLATGATE ⟶⦗⦘" /><figcaption class="elementor-image-carousel-caption">SALAR DE UYUNI ⟶⦗⦘</figcaption></figure></div><div class="swiper-slide" role="group" aria-roledescription="slide" aria-label="5 sur 5"><figure class="swiper-slide-inner"><img decoding="async" class="swiper-slide-image" src="https://presentcomposedesign.fr/wp-content/uploads/2025/11/Zhangjiajie-Present-Compose-design-360-600x600.png" alt="Panorama 360° du parc national de Zhangjiajie en Chine, imposants pitons rocheux inspirant Avatar" /><figcaption class="elementor-image-carousel-caption">Zhangjiajie⟶⦗⦘</figcaption></figure></div>			</div>
												<div class="elementor-swiper-button elementor-swiper-button-prev" role="button" tabindex="0">
						<svg aria-hidden="true" class="e-font-icon-svg e-eicon-chevron-left" viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg"><path d="M646 125C629 125 613 133 604 142L308 442C296 454 292 471 292 487 292 504 296 521 308 533L604 854C617 867 629 875 646 875 663 875 679 871 692 858 704 846 713 829 713 812 713 796 708 779 692 767L438 487 692 225C700 217 708 204 708 187 708 171 704 154 692 142 675 129 663 125 646 125Z"></path></svg>					</div>
					<div class="elementor-swiper-button elementor-swiper-button-next" role="button" tabindex="0">
						<svg aria-hidden="true" class="e-font-icon-svg e-eicon-chevron-right" viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg"><path d="M696 533C708 521 713 504 713 487 713 471 708 454 696 446L400 146C388 133 375 125 354 125 338 125 325 129 313 142 300 154 292 171 292 187 292 204 296 221 308 233L563 492 304 771C292 783 288 800 288 817 288 833 296 850 308 863 321 871 338 875 354 875 371 875 388 867 400 854L696 533Z"></path></svg>					</div>
				
									<div class="swiper-pagination"></div>
									</div>
						</div>
				</div>
				<div class="elementor-element elementor-element-63d407d elementor-widget elementor-widget-spacer" data-id="63d407d" data-element_type="widget" data-e-type="widget" data-widget_type="spacer.default">
				<div class="elementor-widget-container">
							<div class="elementor-spacer">
			<div class="elementor-spacer-inner"></div>
		</div>
						</div>
				</div>
					</div>
				</div>
				<header class="elementor-section elementor-top-section elementor-element elementor-element-4c148bf elementor-section-full_width elementor-section-height-default elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no" data-id="4c148bf" data-element_type="section" data-e-type="section">
						<div class="elementor-container elementor-column-gap-no">
					<header class="elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-3c33b84" data-id="3c33b84" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<div class="elementor-element elementor-element-778d960 elementor-widget__width-inherit elementor-widget elementor-widget-html" data-id="778d960" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<div class="splatgate-container">
    <!-- Iframe Arrival Space -->
    <iframe
        id="arrival-space-iframe"
        src="https://arrival.space/VR_Xprmnts?utm=embed_82998375"
        width="100%"
        height="860"
        allow="camera; microphone; vr; xr; fullscreen"
        allowfullscreen>
    </iframe>

    <!-- Conteneur pour les boutons -->
    <div class="buttons-container">
        <!-- Bloc de texte centré au-dessus du bouton VR -->
        <div class="vr-instruction-text">
            Click here 🡡 from your VR headset
        </div>

        <!-- Bouton VR (centré en haut) -->
        <a
            href="https://arrival.space/VR_Xprmnts"
            target="_blank"
            class="vr-direct-link vr-button"
        >
            SplatGate⟶⦗⦘ in VR ᯅ
        </a>

        <!-- Bouton Plein écran (bas à droite) -->
        <a
            id="fullscreen-link"
            class="vr-direct-link fullscreen-button"
        >
            Plein écran 🗖
        </a>
    </div>
</div>

<style>
    .splatgate-container {
        position: relative;
        width: 100vw;
        max-width: 100%;
        margin: 0;
        padding: 0;
        height: 860px;
        overflow: hidden;
    }

    .buttons-container {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        pointer-events: none;
    }

    .vr-instruction-text {
        position: absolute;
        top: 64px;
        left: 50%;
        transform: translateX(-50%);
        color: white;
        font-size: 14px;
        text-align: center;
        z-index: 1001;
        text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
        pointer-events: auto;
    }

    .vr-direct-link {
        position: absolute;
        padding: 8px 15px;
        background: #FF00FF;
        color: white;
        text-decoration: none;
        font-weight: bold;
        font-size: 14px;
        border-radius: 8px;
        border: 2px solid white;
        box-shadow: 0 0 8px rgba(255, 0, 255, 0.7);
        z-index: 1000;
        pointer-events: auto;
        cursor: pointer;
        transition: all 0.3s;
        font-family: sans-serif;
    }

    .vr-button {
        top: 16px;
        left: 50%;
        transform: translateX(-50%);
    }

    .fullscreen-button {
        bottom: 20px;
        right: 20px;
    }

    .vr-direct-link:hover {
        background: #FF69B4;
        box-shadow: 0 0 12px rgba(255, 0, 255, 0.9);
        z-index: 1001 !important;
    }

    #arrival-space-iframe {
        border: none;
        display: block;
        width: 100vw;
        margin: 0;
    }
</style>

<script>
    // Gestion du bouton Plein écran
    document.getElementById('fullscreen-link').addEventListener('click', function(e) {
        e.preventDefault();
        const iframe = document.getElementById('arrival-space-iframe');
        if (iframe.requestFullscreen) iframe.requestFullscreen();
        else if (iframe.webkitRequestFullscreen) iframe.webkitRequestFullscreen();
        else if (iframe.msRequestFullscreen) iframe.msRequestFullscreen();
    });
</script>
				</div>
				</div>
					</div>
		</header>
					</div>
		</header>
				<section class="elementor-section elementor-top-section elementor-element elementor-element-de33bb3 elementor-section-height-min-height elementor-section-items-bottom elementor-section-content-middle elementor-section-stretched elementor-section-boxed elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no" data-id="de33bb3" data-element_type="section" data-e-type="section" data-settings="{&quot;stretch_section&quot;:&quot;section-stretched&quot;,&quot;background_background&quot;:&quot;classic&quot;}">
						<div class="elementor-container elementor-column-gap-extended">
					<div class="elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-87ce8ad" data-id="87ce8ad" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<section class="elementor-section elementor-inner-section elementor-element elementor-element-0da3fca elementor-section-full_width elementor-section-height-default elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no" data-id="0da3fca" data-element_type="section" data-e-type="section">
						<div class="elementor-container elementor-column-gap-default">
					<div class="elementor-column elementor-col-50 elementor-inner-column elementor-element elementor-element-797121e" data-id="797121e" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<div class="elementor-element elementor-element-5ab8be5 elementor-widget__width-initial elementor-widget elementor-widget-text-editor" data-id="5ab8be5" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h1 style="color: #ffffff;">Des destinations à couper le souffle</h1><div data-vce-boxed-width="true"><div data-vce-do-apply="all el-ba3f564d"><div data-vce-element-content="true"><div data-vce-do-apply="border margin background el-11f4390c"><div data-vce-element-content="true" data-vce-do-apply="padding el-11f4390c"><h2 style="color: #ffffff;">Depuis le hall principal, 5 portes vous invitent à la découverte :</h2><h3 style="color: #ffffff;">Brocéliande : plongez au cœur de la mythique forêt bretonne. Ruisseaux chantants, arbres centenaires et clairières lumineuses vous attendent dans cette reconstitution 3D fidèle à l’esprit des légendes arthuriennes.</h3><h3 style="color: #ffffff;">Grand Canyon : admirez les falaises orangées, les gorges vertigineuses et les jeux de lumière du sud-ouest américain. La visite en VR vous permet d’appréhender l’échelle monumentale de ce site emblématique sans prendre l’avion.</h3><h3 style="color: #ffffff;">Salar d’Uyuni : marchez sur un miroir naturel géant en Bolivie. Ce désert de sel offre un horizon infini où ciel et terre se confondent, réinterprété ici en 360° pour vous procurer une sensation de liberté absolue.<br /><br />&#8230;</h3><h2 style="color: #ffffff;">Chaque univers a été sculpté dans un moteur graphique réaliste et optimisé pour la VR. Les textures, les sons d’ambiance et la profondeur des décors vous plongent dans une escapade dépaysante tout en restant chez vous. Revenez régulièrement : de nouvelles destinations sont en préparation.</h2><h6><a style="color: #ffffff; text-decoration: underline;" href="https://arrival.space/VR_Xprmnts">https://arrival.space/VR_Xprmnts</a></h6><h2 style="color: #ffffff; font-variant-ligatures: normal; font-variant-caps: normal; font-family: 'Inter Tight', sans-serif; font-style: normal; font-weight: 400;">Conception &amp; réalisation : <em style="font-size: 20px;">VR_Xprmnts, Présent Composé Design &#8211; 2025</em></h2></div></div></div></div></div>								</div>
				</div>
					</div>
		</div>
				<div class="elementor-column elementor-col-50 elementor-inner-column elementor-element elementor-element-1738006" data-id="1738006" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<div class="elementor-element elementor-element-87f2b39 elementor-invisible elementor-widget elementor-widget-image" data-id="87f2b39" data-element_type="widget" data-e-type="widget" data-settings="{&quot;_animation&quot;:&quot;zoomIn&quot;}" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img decoding="async" width="768" height="384" src="https://presentcomposedesign.fr/wp-content/uploads/2025/11/SALAR-DE-UYUNI-Present-Compose-design-360-768x384.png" class="attachment-medium_large size-medium_large wp-image-34236" alt="Vue 360° du Salar d’Uyuni, désert de sel sous ciel bleu, Présent Composé design pour SPLATGATE ⟶⦗⦘" srcset="https://presentcomposedesign.fr/wp-content/uploads/2025/11/SALAR-DE-UYUNI-Present-Compose-design-360-768x384.png 768w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/SALAR-DE-UYUNI-Present-Compose-design-360-300x150.png 300w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/SALAR-DE-UYUNI-Present-Compose-design-360-1920x960.png 1920w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/SALAR-DE-UYUNI-Present-Compose-design-360-1536x768.png 1536w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/SALAR-DE-UYUNI-Present-Compose-design-360-2048x1024.png 2048w" sizes="(max-width: 768px) 100vw, 768px" />															</div>
				</div>
				<div class="elementor-element elementor-element-e71b66e elementor-invisible elementor-widget elementor-widget-image" data-id="e71b66e" data-element_type="widget" data-e-type="widget" data-settings="{&quot;_animation&quot;:&quot;zoomIn&quot;}" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img decoding="async" width="600" height="600" src="https://presentcomposedesign.fr/wp-content/uploads/2025/11/Broceliande-Present-Compose-design-360-600x600.png" class="attachment-ocean-thumb-m size-ocean-thumb-m wp-image-34233" alt="Vue 360° forêt Brocéliande avec cercle de pierres, immersion en réalité virtuelle, Présent Composé design pour_SPLATGATE ⟶⦗⦘" srcset="https://presentcomposedesign.fr/wp-content/uploads/2025/11/Broceliande-Present-Compose-design-360-600x600.png 600w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/Broceliande-Present-Compose-design-360-150x150.png 150w" sizes="(max-width: 600px) 100vw, 600px" />															</div>
				</div>
				<div class="elementor-element elementor-element-4275f24 elementor-invisible elementor-widget elementor-widget-image" data-id="4275f24" data-element_type="widget" data-e-type="widget" data-settings="{&quot;_animation&quot;:&quot;zoomIn&quot;}" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="2048" height="1024" src="https://presentcomposedesign.fr/wp-content/uploads/2025/11/GRAND-CANYON-Present-Compose-design-HD-360-2048x1024.png" class="attachment-2048x2048 size-2048x2048 wp-image-34234" alt="Vue 360° Grand Canyon réaliste en réalité virtuelle, lumière naturelle filtrée, Présent Composé design pour SPLATGATE ⟶⦗⦘" srcset="https://presentcomposedesign.fr/wp-content/uploads/2025/11/GRAND-CANYON-Present-Compose-design-HD-360-2048x1024.png 2048w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/GRAND-CANYON-Present-Compose-design-HD-360-300x150.png 300w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/GRAND-CANYON-Present-Compose-design-HD-360-1920x960.png 1920w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/GRAND-CANYON-Present-Compose-design-HD-360-768x384.png 768w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/GRAND-CANYON-Present-Compose-design-HD-360-1536x768.png 1536w" sizes="(max-width: 2048px) 100vw, 2048px" />															</div>
				</div>
					</div>
		</div>
					</div>
		</section>
					</div>
		</div>
					</div>
		</section>
				<section class="elementor-section elementor-top-section elementor-element elementor-element-b6639fe elementor-section-full_width elementor-section-height-min-height elementor-section-items-bottom elementor-section-content-middle elementor-section-stretched elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no elementor-invisible" data-id="b6639fe" data-element_type="section" data-e-type="section" data-settings="{&quot;stretch_section&quot;:&quot;section-stretched&quot;,&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;fadeInUp&quot;}">
						<div class="elementor-container elementor-column-gap-extended">
					<div class="elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-c5a8616" data-id="c5a8616" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<section class="elementor-section elementor-inner-section elementor-element elementor-element-adee29b elementor-section-boxed elementor-section-height-default elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no" data-id="adee29b" data-element_type="section" data-e-type="section">
						<div class="elementor-container elementor-column-gap-default">
					<div class="elementor-column elementor-col-50 elementor-inner-column elementor-element elementor-element-ac60838" data-id="ac60838" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<div class="elementor-element elementor-element-343ca59 elementor-widget__width-initial elementor-widget elementor-widget-text-editor" data-id="343ca59" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h1><span style="color: #0010ef;">Une aventure collaborative</span><span style="color: #0010ef; font-size: 16px;"> </span></h1><div class="vce-row-container" data-vce-boxed-width="true"><div id="el-ba3f564d" class="vce-row vce-row--col-gap-30 vce-row-equal-height vce-row-content--top" data-vce-do-apply="all el-ba3f564d"><div class="vce-row-content" data-vce-element-content="true"><div id="el-11f4390c" class="vce-col vce-col--md-auto vce-col--xs-1 vce-col--xs-last vce-col--xs-first vce-col--sm-last vce-col--sm-first vce-col--md-last vce-col--lg-last vce-col--xl-last vce-col--md-first vce-col--lg-first vce-col--xl-first"><div class="vce-col-inner" data-vce-do-apply="border margin background el-11f4390c"><div class="vce-col-content" data-vce-element-content="true" data-vce-do-apply="padding el-11f4390c"><div class="vce-google-fonts-heading vce-google-fonts-heading--align-left vce-google-fonts-heading--color-b-0-16-255--45--5C00FF--FF7200 vce-google-fonts-heading--font-family-Roboto"><div id="el-6e30c05c" class="vce-google-fonts-heading-wrapper"><div><span style="color: #0010ef;"> </span></div><div><p><span style="color: #0010ef;">SPLATGATE ⟶⦗⦘ est une expérience participative &amp; évolutive.</span></p></div><h2><span style="color: #000000;">Dans le hall, <b>un écran interactif vous permet de voter pour les prochains lieux que vous aimeriez visiter. </b><br />Vos suggestions et vos retours guident le choix des futures scènes à recréer.</span></h2><p>Le projet est entièrement autofinancé. Si cette démarche vous plaît et que vous souhaitez soutenir le développement de nouvelles destinations et l’amélioration de l’expérience, vous pouvez contribuer de plusieurs façons:<br /><br data-start="350" data-end="353" /><b>Faites un don</b> ponctuel ou récurrent : <b>en un clic, offrez un « café »</b> via la plateforme Buy Me a Coffe intégrée à cette page.<br />Les dons servent à financer la modélisation 3D, l’hébergement des expériences.</p><p><b>Partagez </b><span style="color: #0010ef; font-family: 'Inter Tight', sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: bold;">SPLATGATE ⟶⦗⦘ </span> : parlez‑en autour de vous, sur les réseaux sociaux ou dans votre blog. Plus la communauté est grande, plus les votes et les idées seront riches.</p></div></div><div class="vce-text-block"> </div></div></div></div></div></div></div><div class="vce-row-container" data-vce-boxed-width="true"><div id="el-d9d9a9fd" class="vce-row vce-row--col-gap-30 vce-row-equal-height vce-row-content--top" data-vce-do-apply="all el-d9d9a9fd"><div class="vce-row-content" data-vce-element-content="true"><div id="el-87440d41" class="vce-col vce-col--md-auto vce-col--xs-1 vce-col--xs-last vce-col--xs-first vce-col--sm-last vce-col--sm-first vce-col--md-last vce-col--lg-last vce-col--xl-last vce-col--md-first vce-col--lg-first vce-col--xl-first"><div class="vce-col-inner" data-vce-do-apply="border margin background el-87440d41"><div class="vce-col-content" data-vce-element-content="true" data-vce-do-apply="padding el-87440d41"><p><!-- /wp:vcwb/empty-comment-element-wrapper --></p></div></div></div><p><!-- /vcwb/dynamicElementComment:87440d41 --></p></div></div></div><p><!-- /vcwb/dynamicElementComment:d9d9a9fd --><!--vcv no format--></p>								</div>
				</div>
					</div>
		</div>
				<div class="elementor-column elementor-col-50 elementor-inner-column elementor-element elementor-element-7bf6dea" data-id="7bf6dea" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<div class="elementor-element elementor-element-859c5d0 elementor-widget elementor-widget-image" data-id="859c5d0" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="1000" height="1000" src="https://presentcomposedesign.fr/wp-content/uploads/2025/11/SplatGate_Present-Compose-design-UHD2025_square4.jpg" class="attachment-full size-full wp-image-35360" alt="Visuel promotionnel de l&apos;expérience virtuelle SplatGate, un environnement immersif accessible via la plateforme Arrival.Space. Cette expérience permet aux utilisateurs de découvrir des paysages virtuels et des escapades gratuitement et sans limites. L&apos;image montre deux &quot;voyageurs&quot; en réalité virtuelle, explorant l&apos;univers SplatGate et les destinations proposées." srcset="https://presentcomposedesign.fr/wp-content/uploads/2025/11/SplatGate_Present-Compose-design-UHD2025_square4.jpg 1000w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/SplatGate_Present-Compose-design-UHD2025_square4-300x300.jpg 300w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/SplatGate_Present-Compose-design-UHD2025_square4-150x150.jpg 150w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/SplatGate_Present-Compose-design-UHD2025_square4-768x768.jpg 768w, https://presentcomposedesign.fr/wp-content/uploads/2025/11/SplatGate_Present-Compose-design-UHD2025_square4-600x600.jpg 600w" sizes="(max-width: 1000px) 100vw, 1000px" />															</div>
				</div>
				<div class="elementor-element elementor-element-d211448 elementor-widget elementor-widget-spacer" data-id="d211448" data-element_type="widget" data-e-type="widget" data-widget_type="spacer.default">
				<div class="elementor-widget-container">
							<div class="elementor-spacer">
			<div class="elementor-spacer-inner"></div>
		</div>
						</div>
				</div>
				<div class="elementor-element elementor-element-ea9b63d elementor-widget elementor-widget-html" data-id="ea9b63d" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<section class="pcd-hero-title-wrap">
    <div class="pcd-card" style="max-width: 600px; margin: 0 auto; text-align: center;">
        <h3 class="pcd-animate-text" style="font-size: 1.5rem !important; margin-bottom: 15px;">
            Soutenez SPLATGATE ⟶⦗⦘
        </h3>
        <p style="color: #1001ef; margin-bottom: 25px;">
            Ce hub est gratuit et sans publicité. 
            Votre contribution aide à financer les futures destinations et soutient l'innovation de manière générale</strong>.
        </p>
        <div class="pcd-btn">
            <a href="https://www.buymeacoffee.com/presentcomposedesign" target="_blank">
                ☕ Soutenir le projet
            </a>
        </div>
    </div>
</section>				</div>
				</div>
					</div>
		</div>
					</div>
		</section>
					</div>
		</div>
					</div>
		</section>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/splatgate/">SPLATGATE⟶⦗⦘</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		<enclosure url="https://presentcomposedesign.fr/wp-content/uploads/2025/11/video_presentation_SPLATGATE_PresentComposedesign-2025.mp4" length="45880030" type="video/mp4" />

			</item>
		<item>
		<title>Opti-môme</title>
		<link>https://presentcomposedesign.fr/opti-mome/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Mon, 29 Sep 2025 00:12:07 +0000</pubDate>
				<category><![CDATA[[ia]]]></category>
		<category><![CDATA[Design graphique]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=33661</guid>

					<description><![CDATA[<p>Opti-môme est un concept d'application conçue et développée dans le cadre du Hackathon ia for impact organisé par La mêlée numérique. Son objectif : l'optimisation des transports dédiés à l’Aide Sociale à l’Enfance du conseil départemental de Haute Garonne. Simplification de la planification des trajets, mutualisation les déplacements, réduction des coûts et de l’impact environnemental tout en renforçant la sécurité et la coordination.</p>
<p>Cet article <a href="https://presentcomposedesign.fr/opti-mome/">Opti-môme</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="33661" class="elementor elementor-33661">
				<div class="wpr-jarallax elementor-element elementor-element-8192ee7 wpr-jarallax-yes e-con-full e-flex wpr-particle-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent" speed-data="0.5" bg-image="https://presentcomposedesign.fr/wp-content/uploads/2025/10/prev_map_opti-mome.jpg" bg-image-mobile="" scroll-effect="scroll" data-id="8192ee7" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
				<div class="elementor-element elementor-element-2627d84 elementor-widget__width-inherit elementor-widget elementor-widget-image" data-id="2627d84" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/elementor/thumbs/logo_optimome_black-rd1rrp8gq9jp94h4vzy83bliln5ncrfal8y7ai3xt8.png" title="logo_opti-mome_app_black" alt="logo_opti-mome_app_black" loading="lazy" />															</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-e235fbc e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no elementor-invisible e-con e-parent" data-id="e235fbc" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;fadeInUp&quot;}">
					<div class="e-con-inner">
		<div class="elementor-element elementor-element-8d40ee4 e-con-full e-flex wpr-particle-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="8d40ee4" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
				<div class="elementor-element elementor-element-014482d elementor-widget elementor-widget-spacer" data-id="014482d" data-element_type="widget" data-e-type="widget" data-widget_type="spacer.default">
				<div class="elementor-widget-container">
							<div class="elementor-spacer">
			<div class="elementor-spacer-inner"></div>
		</div>
						</div>
				</div>
				<div class="elementor-element elementor-element-426906f elementor-widget elementor-widget-image" data-id="426906f" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="1024" height="1024" src="https://presentcomposedesign.fr/wp-content/uploads/2025/10/phone2mokup.png" class="attachment-large size-large wp-image-33664" alt="" srcset="https://presentcomposedesign.fr/wp-content/uploads/2025/10/phone2mokup.png 1024w, https://presentcomposedesign.fr/wp-content/uploads/2025/10/phone2mokup-300x300.png 300w, https://presentcomposedesign.fr/wp-content/uploads/2025/10/phone2mokup-150x150.png 150w, https://presentcomposedesign.fr/wp-content/uploads/2025/10/phone2mokup-768x768.png 768w, https://presentcomposedesign.fr/wp-content/uploads/2025/10/phone2mokup-600x600.png 600w" sizes="(max-width: 1024px) 100vw, 1024px" />															</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-1a31b73 e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="1a31b73" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-6df7a1f elementor-widget__width-initial elementor-widget elementor-widget-text-editor" data-id="6df7a1f" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h1><span style="color: #0381a2;">ux/ui Design</span></h1><h1><span style="color: #0381a2;">Concept d&rsquo;Application</span></h1><p><strong> </strong></p><p><strong>Opti-môme</strong> est un concept d&rsquo;application innovante de gestion et d’optimisation des transports dédiée à l’<strong>Aide Sociale à l’Enfance (ASE)</strong>. Développée dans le cadre du « <a href="https://www.meleenumerique.com/temps-fort-hackathon-ia-for-impact/#:~:text=La%20M%C3%AAl%C3%A9e%20organise%20un%20hackathon,propos%C3%A9s%20par%20des%20entreprises%20partenaires.">Hackathon ia for impact</a> » organisé par « <a href="https://lamelee.com/">La mêlée</a> » en partenariat avec les acteurs du département de la <strong>Haute-Garonne. <br /></strong><strong>Opti-môme </strong>répond à un double enjeu : <strong>améliorer la qualité de service</strong> offerte aux enfants confiés tout en <strong>réduisant l’impact environnemental</strong> et les coûts logistiques.</p><p>Grâce à l’intelligence artificielle, <strong>Opti-môme</strong> planifie, mutualise et suit en temps réel l’ensemble des trajets. L’application simplifie la communication entre <strong>chauffeurs</strong>, <strong>assistantes sociales</strong> et <strong>superviseurs</strong>, tout en garantissant la <strong>sécurité, la traçabilité et la fiabilité</strong> des transports. Son interface ergonomique, pensée pour le terrain, traduit une approche <strong>écologique, économique et humaine</strong> du service public.</p><p>Conçue comme une plateforme évolutive, <strong>Opti-môme</strong> s’inscrit dans une dynamique de <strong>transition numérique responsable</strong> et d’innovation sociale. Elle répond à un souhait marqué par le Département de moderniser la mobilité des enfants accompagnés, en conciliant <strong>efficacité, équité et durabilité</strong>.</p>								</div>
				</div>
				</div>
					</div>
				</div>
		<div class="elementor-element elementor-element-5787b5c e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent" data-id="5787b5c" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
				<div class="elementor-element elementor-element-10cd316 elementor-widget elementor-widget-html" data-id="10cd316" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<div class="viewer-container" id="viewer-container-10">
  <canvas class="viewer-canvas" id="viewer-canvas-10"></canvas>
</div>

<style>
  .viewer-container {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    height: 60vh; /* Ajustez en fonction de vos besoins */
    width: 100%;
    margin: 0;
    padding: 0;
  }

  .viewer-canvas {
    width: 100%;
    height: 100%;
  }

  /* Éviter les conflits avec les autres éléments du site */
  body {
    margin: 0;
  }
</style>

<script type="module">
  import { Application } from "https://esm.sh/@splinetool/runtime";

  const initializeViewer = (canvasId, fileName) => {
    const canvas = document.getElementById(canvasId);
    const app = new Application(canvas);
    app.load(`https://prod.spline.design/${fileName}/scene.splinecode`);
  };

  // Initialize first viewer
  initializeViewer('viewer-canvas-10', '2A8Sr5uxpOZ31tIN');

  // Add more viewers as needed
</script>
				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-fe1fb28 e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no elementor-invisible e-con e-parent" data-id="fe1fb28" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;fadeInUp&quot;}">
					<div class="e-con-inner">
		<div class="elementor-element elementor-element-dac6e50 e-con-full e-flex wpr-particle-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="dac6e50" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
				<div class="elementor-element elementor-element-fbd207b elementor-widget elementor-widget-image" data-id="fbd207b" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="600" height="600" src="https://presentcomposedesign.fr/wp-content/uploads/2025/10/chauffeur-600x600.png" class="attachment-ocean-thumb-m size-ocean-thumb-m wp-image-33663" alt="Conducteur ASE consultant ses trajets du jour sur l’application Opti-môme depuis le tableau de bord de son véhicule." srcset="https://presentcomposedesign.fr/wp-content/uploads/2025/10/chauffeur-600x600.png 600w, https://presentcomposedesign.fr/wp-content/uploads/2025/10/chauffeur-150x150.png 150w" sizes="(max-width: 600px) 100vw, 600px" />															</div>
				</div>
				<div class="elementor-element elementor-element-c0bf39c elementor-widget elementor-widget-spacer" data-id="c0bf39c" data-element_type="widget" data-e-type="widget" data-widget_type="spacer.default">
				<div class="elementor-widget-container">
							<div class="elementor-spacer">
			<div class="elementor-spacer-inner"></div>
		</div>
						</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-dffc864 e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="dffc864" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-26e901e elementor-widget__width-initial elementor-widget elementor-widget-text-editor" data-id="26e901e" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2><span style="color: #8a3589;"><strong>⦿ Conducteurs affiliés</strong></span></h2><h2>Fonctionnalités proposées par l&rsquo;application:</h2><ul><li><h6><span style="color: #0381a2;"><strong>Page d’accueil :</strong></span> affichage du trajet du jour avec carte interactive, étapes, liste des arrêts et actions rapides (démarrer, signaler un retard, contacter un coordinateur).</h6></li><li><h6><span style="color: #0381a2;"><strong>Navigation et prise en charge :</strong></span> intégration GPS avec itinéraire préchargé, check-in/check-out digital via QR code ou signature électronique, accès hors ligne aux fiches des jeunes et signalement d’incident.</h6></li><li><h6><span style="color: #0381a2;"><strong>Tableau de bord personnel :</strong></span> suivi des kilomètres, du temps de trajet, de la consommation et des économies de CO₂, avec historique exportable.</h6></li><li><h6><span style="color: #0381a2;"><strong>Paramètres et personnalisation :</strong></span> mode clair/sombre, notifications, gestion du profil et verrouillage automatique pour la sécurité.</h6></li></ul>								</div>
				</div>
				<div class="elementor-element elementor-element-e5ce5c1 wpr-feature-list-left wpr-feature-list-square wpr-feature-list-line-yes elementor-widget elementor-widget-wpr-feature-list" data-id="e5ce5c1" data-element_type="widget" data-e-type="widget" data-widget_type="wpr-feature-list.default">
				<div class="elementor-widget-container">
					<div class="wpr-feature-list-wrap"><ul class="wpr-feature-list"><li class="wpr-feature-list-item elementor-repeater-item-5e38f5c"><div class="wpr-feature-list-icon-wrap"><span class="wpr-feature-list-line"></span><div class="wpr-feature-list-icon-inner-wrap"><svg aria-hidden="true" class="e-font-icon-svg e-far-check-circle" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 48c110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200-110.532 0-200-89.451-200-200 0-110.532 89.451-200 200-200m140.204 130.267l-22.536-22.718c-4.667-4.705-12.265-4.736-16.97-.068L215.346 303.697l-59.792-60.277c-4.667-4.705-12.265-4.736-16.97-.069l-22.719 22.536c-4.705 4.667-4.736 12.265-.068 16.971l90.781 91.516c4.667 4.705 12.265 4.736 16.97.068l172.589-171.204c4.704-4.668 4.734-12.266.067-16.971z"></path></svg></div></div><div class="wpr-feature-list-content-wrap"><h5 class="wpr-feature-list-title"><a class="wpr-feature-list-url" target="_blank" rel="nofollow">Cahier des charge initial</a></h5><p class="wpr-feature-list-description"></p></div></li></ul></div>				</div>
				</div>
				</div>
					</div>
				</div>
		<div class="elementor-element elementor-element-8732660 e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no elementor-invisible e-con e-parent" data-id="8732660" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;fadeInUp&quot;}">
					<div class="e-con-inner">
		<div class="elementor-element elementor-element-1169535 e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="1169535" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-99977ce elementor-widget__width-initial elementor-widget elementor-widget-text-editor" data-id="99977ce" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2><span style="color: #8a3589;"><strong>⦿ Assistantes sociales / Maisons de solidarités</strong></span></h2><ul><li><h6><span style="color: #333333;"><span style="color: #0381a2;"><strong>Tableau de bord des demandes :</strong> </span>liste des ordres de mission en attente, validés ou refusés, vue calendrier des trajets programmés et filtres par date ou jeune.</span></h6></li><li><h6><span style="color: #333333;"><span style="color: #0381a2;"><strong>Création et validation de demande :</strong></span> formulaire de réservation de transport avec suggestion de créneaux et de chauffeurs, validation par signature électronique et notification des parties.</span></h6></li><li><h6><span style="color: #333333;"><span style="color: #0381a2;"><strong>Gestion des dossiers :</strong></span> consultation et mise à jour des fiches des jeunes (coordonnées, allergies, autorisations), transmission sécurisée au chauffeur assigné.</span></h6></li><li><h6><span style="color: #333333;"><span style="color: #0381a2;"><strong>Préfacturation et suivi budgétaire :</strong></span> module de préfacturation pour suivre les kilomètres et les coûts, export des données pour audit ou reporting.</span></h6></li></ul><h6> </h6>								</div>
				</div>
				<div class="elementor-element elementor-element-4673650 wpr-feature-list-left wpr-feature-list-square wpr-feature-list-line-yes elementor-widget elementor-widget-wpr-feature-list" data-id="4673650" data-element_type="widget" data-e-type="widget" data-widget_type="wpr-feature-list.default">
				<div class="elementor-widget-container">
					<div class="wpr-feature-list-wrap"><ul class="wpr-feature-list"><li class="wpr-feature-list-item elementor-repeater-item-5e38f5c"><div class="wpr-feature-list-icon-wrap"><span class="wpr-feature-list-line"></span><div class="wpr-feature-list-icon-inner-wrap"><svg aria-hidden="true" class="e-font-icon-svg e-fas-brain" viewBox="0 0 576 512" xmlns="http://www.w3.org/2000/svg"><path d="M208 0c-29.9 0-54.7 20.5-61.8 48.2-.8 0-1.4-.2-2.2-.2-35.3 0-64 28.7-64 64 0 4.8.6 9.5 1.7 14C52.5 138 32 166.6 32 200c0 12.6 3.2 24.3 8.3 34.9C16.3 248.7 0 274.3 0 304c0 33.3 20.4 61.9 49.4 73.9-.9 4.6-1.4 9.3-1.4 14.1 0 39.8 32.2 72 72 72 4.1 0 8.1-.5 12-1.2 9.6 28.5 36.2 49.2 68 49.2 39.8 0 72-32.2 72-72V64c0-35.3-28.7-64-64-64zm368 304c0-29.7-16.3-55.3-40.3-69.1 5.2-10.6 8.3-22.3 8.3-34.9 0-33.4-20.5-62-49.7-74 1-4.5 1.7-9.2 1.7-14 0-35.3-28.7-64-64-64-.8 0-1.5.2-2.2.2C422.7 20.5 397.9 0 368 0c-35.3 0-64 28.6-64 64v376c0 39.8 32.2 72 72 72 31.8 0 58.4-20.7 68-49.2 3.9.7 7.9 1.2 12 1.2 39.8 0 72-32.2 72-72 0-4.8-.5-9.5-1.4-14.1 29-12 49.4-40.6 49.4-73.9z"></path></svg></div></div><div class="wpr-feature-list-content-wrap"><h5 class="wpr-feature-list-title"><a class="wpr-feature-list-url" target="_blank" rel="nofollow">Fonctionnalités supplémentaires proposées</a></h5><p class="wpr-feature-list-description"></p></div></li></ul></div>				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-7fc3c12 e-con-full e-flex wpr-particle-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="7fc3c12" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
				<div class="elementor-element elementor-element-56ce7b5 elementor-widget elementor-widget-image" data-id="56ce7b5" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="600" height="600" src="https://presentcomposedesign.fr/wp-content/uploads/2025/10/preview-app-4-600x600.jpg" class="attachment-ocean-thumb-m size-ocean-thumb-m wp-image-33773" alt="Assistante sociale utilisant l’application Opti-môme pour commander un trajet sécurisé pour un enfant accompagné." srcset="https://presentcomposedesign.fr/wp-content/uploads/2025/10/preview-app-4-600x600.jpg 600w, https://presentcomposedesign.fr/wp-content/uploads/2025/10/preview-app-4-150x150.jpg 150w" sizes="(max-width: 600px) 100vw, 600px" />															</div>
				</div>
				</div>
					</div>
				</div>
		<div class="elementor-element elementor-element-740e89b e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no elementor-invisible e-con e-parent" data-id="740e89b" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;fadeInUp&quot;}">
					<div class="e-con-inner">
		<div class="elementor-element elementor-element-32cd6a1 e-con-full e-flex wpr-particle-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="32cd6a1" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
				<div class="elementor-element elementor-element-3467bee elementor-widget elementor-widget-image" data-id="3467bee" data-element_type="widget" data-e-type="widget" data-widget_type="image.default">
				<div class="elementor-widget-container">
															<img loading="lazy" decoding="async" width="600" height="600" src="https://presentcomposedesign.fr/wp-content/uploads/2025/10/preview-app-supervisor-600x600.jpg" class="attachment-ocean-thumb-m size-ocean-thumb-m wp-image-33808" alt="" srcset="https://presentcomposedesign.fr/wp-content/uploads/2025/10/preview-app-supervisor-600x600.jpg 600w, https://presentcomposedesign.fr/wp-content/uploads/2025/10/preview-app-supervisor-150x150.jpg 150w" sizes="(max-width: 600px) 100vw, 600px" />															</div>
				</div>
				<div class="elementor-element elementor-element-b22a7b6 elementor-widget elementor-widget-spacer" data-id="b22a7b6" data-element_type="widget" data-e-type="widget" data-widget_type="spacer.default">
				<div class="elementor-widget-container">
							<div class="elementor-spacer">
			<div class="elementor-spacer-inner"></div>
		</div>
						</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-c4a53a8 e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="c4a53a8" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-bb466b9 elementor-widget__width-initial elementor-widget elementor-widget-text-editor" data-id="bb466b9" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h2><span style="color: #8a3589;"><strong>⦿ Superviseurs / Responsables logistiques</strong></span></h2><ul><li><h6><strong><span style="color: #0381a2;">Supervision en temps réel</span> <span style="color: #0381a2;">:</span></strong> carte interactive des trajets en cours, liste dynamique, alertes instantanées et actions rapides (contacter un chauffeur, replanifier un trajet).</h6></li><li><h6><span style="color: #0381a2;"><strong>Bilan écologique :</strong></span> graphiques de suivi des émissions de CO₂, kilomètres évités, taux de mutualisation et recommandations automatiques pour optimiser les tournées.</h6></li><li><h6><span style="color: #0381a2;"><strong>Analyse économique :</strong></span> visualisation des coûts par kilomètre, répartition des dépenses (carburant, maintenance, taxis) et simulateur d’économies selon différents scénarios.</h6></li><li><h6><span style="color: #0381a2;"><strong>Gestion des ressources :</strong></span> tableau de bord des chauffeurs (ponctualité, kilomètres, économies réalisées), suivi des véhicules (maintenance, carburant) et gestion des incidents et de la conformité.</h6></li></ul><h6> </h6>								</div>
				</div>
				<div class="elementor-element elementor-element-04f6e76 wpr-feature-list-left wpr-feature-list-square wpr-feature-list-line-yes elementor-widget elementor-widget-wpr-feature-list" data-id="04f6e76" data-element_type="widget" data-e-type="widget" data-widget_type="wpr-feature-list.default">
				<div class="elementor-widget-container">
					<div class="wpr-feature-list-wrap"><ul class="wpr-feature-list"><li class="wpr-feature-list-item elementor-repeater-item-5e38f5c"><div class="wpr-feature-list-icon-wrap"><span class="wpr-feature-list-line"></span><div class="wpr-feature-list-icon-inner-wrap"><svg aria-hidden="true" class="e-font-icon-svg e-fas-brain" viewBox="0 0 576 512" xmlns="http://www.w3.org/2000/svg"><path d="M208 0c-29.9 0-54.7 20.5-61.8 48.2-.8 0-1.4-.2-2.2-.2-35.3 0-64 28.7-64 64 0 4.8.6 9.5 1.7 14C52.5 138 32 166.6 32 200c0 12.6 3.2 24.3 8.3 34.9C16.3 248.7 0 274.3 0 304c0 33.3 20.4 61.9 49.4 73.9-.9 4.6-1.4 9.3-1.4 14.1 0 39.8 32.2 72 72 72 4.1 0 8.1-.5 12-1.2 9.6 28.5 36.2 49.2 68 49.2 39.8 0 72-32.2 72-72V64c0-35.3-28.7-64-64-64zm368 304c0-29.7-16.3-55.3-40.3-69.1 5.2-10.6 8.3-22.3 8.3-34.9 0-33.4-20.5-62-49.7-74 1-4.5 1.7-9.2 1.7-14 0-35.3-28.7-64-64-64-.8 0-1.5.2-2.2.2C422.7 20.5 397.9 0 368 0c-35.3 0-64 28.6-64 64v376c0 39.8 32.2 72 72 72 31.8 0 58.4-20.7 68-49.2 3.9.7 7.9 1.2 12 1.2 39.8 0 72-32.2 72-72 0-4.8-.5-9.5-1.4-14.1 29-12 49.4-40.6 49.4-73.9z"></path></svg></div></div><div class="wpr-feature-list-content-wrap"><h5 class="wpr-feature-list-title"><a class="wpr-feature-list-url" target="_blank" rel="nofollow">Fonctionnalités supplémentaires proposées</a></h5><p class="wpr-feature-list-description"></p></div></li></ul></div>				</div>
				</div>
				</div>
					</div>
				</div>
		<div class="elementor-element elementor-element-3c0d6dd e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent" data-id="3c0d6dd" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-cfc4ced wpr-fancy-text-clip wpr-advanced-text-style-animated wpr-animated-text-infinite-yes elementor-widget elementor-widget-wpr-advanced-text" data-id="cfc4ced" data-element_type="widget" data-e-type="widget" data-settings="{&quot;anim_loop&quot;:&quot;yes&quot;}" data-widget_type="wpr-advanced-text.default">
				<div class="elementor-widget-container">
					
		<h3 class="wpr-advanced-text">

					
			
		<span class="wpr-anim-text wpr-anim-text-type-clip" data-anim-duration="1000,2000" data-anim-loop="yes">
			<span class="wpr-anim-text-inner">
									<b>Un projet ?</b>
									<b>Un besoin spécifique ? </b>
							</span>
			<span class="wpr-anim-text-cursor">|</span>		</span>

				
		</h3>
		
						</div>
				</div>
					</div>
				</div>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/opti-mome/">Opti-môme</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>VR_Xprmnts</title>
		<link>https://presentcomposedesign.fr/vr_xprmnts/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Sat, 20 Sep 2025 22:09:13 +0000</pubDate>
				<category><![CDATA[[ia]]]></category>
		<category><![CDATA[[VR]]]></category>
		<category><![CDATA[Design d'espace]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=33443</guid>

					<description><![CDATA[<p>La VR nécessite JavaScript. Ouvrir le monde : lien direct. broceliande ᯅ broceliande3 ᯅ broceliande3 ᯅ broceliande3 ᯅ broceliande3 ᯅ ᯅ ✕ broceliande ᯅ broceliande3 ᯅ ᯅ ✕ broceliande ᯅ broceliande3 ᯅ ᯅ ✕</p>
<p>Cet article <a href="https://presentcomposedesign.fr/vr_xprmnts/">VR_Xprmnts</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="33443" class="elementor elementor-33443">
						<header class="elementor-section elementor-top-section elementor-element elementor-element-eb3024d elementor-section-full_width elementor-section-stretched elementor-section-height-default elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no" data-id="eb3024d" data-element_type="section" data-e-type="section" data-settings="{&quot;stretch_section&quot;:&quot;section-stretched&quot;}">
						<div class="elementor-container elementor-column-gap-no">
					<header class="elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-3fa2d06" data-id="3fa2d06" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<div class="elementor-element elementor-element-9f43ee9 elementor-widget elementor-widget-html" data-id="9f43ee9" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!-- VR EMBED – Centrage visuel du bouton "enter VR" natif -->
<style>
  html, body { height:100%; margin:0; padding:0; }
  .pcd-vr-wrap{
    position:relative;
    width:100vw;
    height:100svh;
    height:100dvh;
    min-height:100vh;
    overflow:hidden;
    background:#000;
    /* valeur par défaut si JS bloqué */
    --pcd-shift: 0vh; /* translation verticale appliquée à l'iframe */
  }
  .elementor .pcd-vr-wrap,
  .elementor-section .pcd-vr-wrap,
  .elementor-widget-container .pcd-vr-wrap{ max-height:none !important; }

  /* Canvas qui contient l'iframe, on garde overflow hidden */
  .pcd-vr-canvas{
    position:absolute; inset:0;
    overflow:hidden;
    background:#000;
  }

  /* L'iframe est agrandie en hauteur pour permettre la translation sans bords vides */
  .pcd-vr-iframe{
    position:absolute; left:0; width:100%;
    /* on ajoute 30vh au-dessus et 30vh en-dessous comme "marges" de sécurité */
    top:-30vh;
    height:calc(100% + 60vh);
    border:0; display:block;
    background:#000;
    /* translation pour "remonter" le contenu -> le bouton natif (en bas) vient au centre */
    transform:translateY(var(--pcd-shift));
    will-change:transform;
  }

  /* Icône fullscreen discrète (même style que précédemment) */
  .pcd-vr-fs{
    position:absolute; right:14px; bottom:14px; z-index:10;
    width:44px; height:44px; border-radius:999px; border:0;
    display:grid; place-items:center;
    background:rgba(255,255,255,.10);
    backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
    cursor:pointer; color:#fff; opacity:.85;
    transition:background .2s, opacity .2s, transform .08s;
  }
  .pcd-vr-fs:hover{ background:rgba(255,255,255,.18); opacity:1; }
  .pcd-vr-fs:active{ transform:translateY(1px) scale(.98); }
  .pcd-vr-fs svg{ width:22px; height:22px; display:block; }

  /* Barre admin WP */
  body.admin-bar .pcd-vr-wrap{ height:calc(100dvh - 32px); }
  @media (max-width:782px){
    body.admin-bar .pcd-vr-wrap{ height:calc(100dvh - 46px); }
  }
</style>

<div id="pcd-vr" class="pcd-vr-wrap"
     data-shift-desktop="0vh"
     data-shift-mobile="-18vh"
     data-shift-quest="-30vh">
  <div class="pcd-vr-canvas">
    <iframe
      id="pcd-vr-iframe"
      class="pcd-vr-iframe"
      src="https://marble.worldlabs.ai/worldvr/a9c1b8ad-9e88-431d-abc6-1199b6340a98"
      title="WorldLabs VR – Embedded"
      loading="eager"
      allow="accelerometer; magnetometer; gyroscope; gamepad; vr; xr-spatial-tracking; microphone; camera; autoplay; clipboard-write; fullscreen"
      allowfullscreen
      referrerpolicy="no-referrer-when-downgrade">
    </iframe>
  </div>

  <!-- Icône plein écran (style discret) -->
  <button class="pcd-vr-fs" type="button" id="pcd-vr-fs" aria-label="Plein écran">
    <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
      <path d="M4 9V5a1 1 0 0 1 1-1h4M20 9V5a1 1 0 0 0-1-1h-4M4 15v4a1 1 0 0 0 1 1h4M20 15v4a1 1 0 0 1-1 1h-4"
            stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
    </svg>
  </button>

  <noscript>
    <p style="position:absolute;left:12px;bottom:12px;color:#fff">
      La VR nécessite JavaScript. Ouvrir le monde :
      <a href="https://marble.worldlabs.ai/worldvr/b1f77449-6ddf-4aaf-bcf9-7c45d6949931" target="_blank" rel="noopener" style="color:#9cf">lien direct</a>.
    </p>
  </noscript>
</div>

<script>
  (function(){
    const wrap   = document.getElementById('pcd-vr');
    const iframe = document.getElementById('pcd-vr-iframe');
    const btnFS  = document.getElementById('pcd-vr-fs');

    // Hauteur robuste = hauteur viewport réelle (évite les barres d’adresse)
    function fit(){ wrap.style.height = (window.innerHeight || document.documentElement.clientHeight) + 'px'; }
    fit();
    window.addEventListener('resize', fit, {passive:true});
    window.addEventListener('orientationchange', fit, {passive:true});

    // Détection simple des devices pour centrer le bouton natif via translation du contenu
    const ua = navigator.userAgent || '';
    const isQuest  = /OculusBrowser|Meta\s?Quest|Quest/i.test(ua);
    const isMobile = /Android|iPhone|iPad|Mobile/i.test(ua);

    const shiftDesktop = wrap.dataset.shiftDesktop || '0vh';
    const shiftMobile  = wrap.dataset.shiftMobile  || '-18vh';
    const shiftQuest   = wrap.dataset.shiftQuest   || '-30vh';

    const shift = isQuest ? shiftQuest : (isMobile ? shiftMobile : shiftDesktop);
    wrap.style.setProperty('--pcd-shift', shift);

    // Plein écran natif
    function goFullscreen(el){
      const rfs = el.requestFullscreen || el.webkitRequestFullscreen ||
                  el.msRequestFullscreen || el.mozRequestFullScreen;
      if (rfs) rfs.call(el);
    }
    btnFS.addEventListener('click', ()=>{ goFullscreen(iframe) || goFullscreen(wrap); });

    // Option d’ajustement fin via touches (désactive si inutile)
    // Haut/Bas : affiner la translation par pas de 2vh
    let currentVal = parseFloat(String(shift).replace('vh','')) || 0;
    window.addEventListener('keydown', (e)=>{
      if(!e.altKey) return;    // Alt+↑ / Alt+↓ pour régler
      if(e.key === 'ArrowUp'){ currentVal -= 2; }
      if(e.key === 'ArrowDown'){ currentVal += 2; }
      wrap.style.setProperty('--pcd-shift', currentVal + 'vh');
    });
  })();
</script>
				</div>
				</div>
					</div>
		</header>
					</div>
		</header>
		<div class="elementor-element elementor-element-4921e6d e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent" data-id="4921e6d" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
					</div>
				</div>
		<div class="elementor-element elementor-element-2f6a63c e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent" data-id="2f6a63c" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
		<div class="elementor-element elementor-element-89c8d5a e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="89c8d5a" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-678d08f elementor-widget elementor-widget-html" data-id="678d08f" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@600;700&display=swap" rel="stylesheet" />

<style>
  /* ===== Carte (style d’origine) + bordure légère & ombre douce ===== */
  .spline-card{
    position:relative; border-radius:16px; overflow:hidden;
    border:1px solid rgba(255,255,255,.14);
    box-shadow:
      0 12px 12px rgba(0,0,0,.24),
      0 12px 6px  rgba(0,0,0,.24);
    transform:translateZ(0); backface-visibility:hidden;
    transition:box-shadow .25s ease, filter .25s ease, transform .22s ease;
    margin:0 0 24px;
    background:#000; /* évite toute marge transparente parasite */
  }
  .spline-card:hover{
    transform:scale(1.022);
    box-shadow:
      0 20px 48px rgba(0,0,0,.28),
      0 4px 10px  rgba(0,0,0,.16);
  }

  /* ===== Glass effect (toggle) =====
     Active-le par carte avec data-glass="yes" */
  .spline-card[data-glass="yes"]{
    background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03));
    border-color: rgba(255,255,255,.22);
    backdrop-filter: blur(10px) saturate(120%);
    -webkit-backdrop-filter: blur(10px) saturate(120%);
    /* léger liseré intérieur pour un rendu “verre” sans masquer le contenu */
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,.14),
      0 12px 12px rgba(0,0,0,.24),
      0 12px 6px  rgba(0,0,0,.24);
  }

  .spline-live{
    position:relative; width:100%; aspect-ratio:16/9; background:#000; overflow:hidden;
    /* Anti-bande noire horizontale */
    --overscan: 12px;
    /* Mini overscan vertical (corrige le fin liseré en bas/haut) */
    --overscanY: 2px; /* ajuste 0–4px si besoin */
  }
  .spline-canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; border:0; }

  /* Iframe Marble : overscan centré + no-scrollbar */
  .spline-iframe{
    position:absolute;
    top:    calc(0px - var(--overscanY));
    bottom: calc(0px - var(--overscanY));
    left:   calc(10px - var(--overscan));
    right:  calc(10px - var(--overscan));
    width:  calc(100% + (var(--overscan) * 2));
    height: calc(100% + (var(--overscanY) * 2));
    display:block; border:0; background:#000;
    transform:translateZ(0); backface-visibility:hidden;
    -ms-overflow-style:none; scrollbar-width:none;
  }
  .spline-iframe::-webkit-scrollbar{ display:none; }

  /* Laisse le scroll à la page */
  .spline-hit{ position:absolute; inset:0; pointer-events:none; }

  /* Infobar (bas de carte) */
  .spline-infobar{
    position:absolute; left:0; right:0; bottom:0; z-index:3;
    display:flex; align-items:center; justify-content:space-between;
    padding:10px 12px; color:#fff;
    background:linear-gradient(180deg, rgba(0,0,0,0) 45%, rgba(0,0,0,.58) 100%);
    user-select:none;
  }
  .spline-title{
    margin:0; color:#fff;
    font:600 14px/1.2 "Inter Tight", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
    text-shadow:0 1px 6px rgba(0,0,0,.45);
  }

  /* Icônes pures */
  .icon-reset{ all: unset; display:inline-block; color:#fff; line-height:1; cursor:pointer; vertical-align:middle; }
  .icon-row{ display:flex; gap:10px; align-items:center; }
  .icon-reset svg{ width:20px; height:20px; display:block; }
  .icon-reset.text{ font-family:"Inter Tight", system-ui; font-weight:700; font-size:18px; }

  /* ===== Modal partagé (plein écran) ===== */
  .spline-modal{ position:fixed; inset:0; z-index:9999; display:none; }
  .spline-modal[aria-hidden="false"]{ display:block; }
  .spline-backdrop{ position:absolute; inset:0; background:rgba(0,0,0,.78); }
  .spline-stage{ position:absolute; inset:0; display:flex; flex-direction:column; background:#000; overflow:hidden; }
  .spline-bar{
    display:flex; justify-content:flex-end; gap:12px; padding:8px 12px; color:#fff;
    background:rgba(0,0,0,.45); border-bottom:1px solid rgba(255,255,255,.1);
    backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
  }
  .spline-fs, .spline-close{ all: unset; color:#fff; cursor:pointer; font:700 18px/1 "Inter Tight", system-ui; }

  .spline-canvas-full{
    position:relative; flex:1 1 auto; overflow:hidden; background:#000;
    --overscan: 12px;  /* horizontal */
    --overscanY: 2px;  /* vertical */
  }
  .spline-canvas-full canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; border:0; }

  .spline-iframe-full{
    position:absolute;
    top:    calc(0px - var(--overscanY));
    bottom: calc(0px - var(--overscanY));
    left:   calc(12px - var(--overscan));
    right:  calc(12px - var(--overscan));
    width:  calc(100% + (var(--overscan) * 2));
    height: calc(100% + (var(--overscanY) * 2));
    display:block; border:0; background:#000;
    transform:translateZ(0); backface-visibility:hidden;
    -ms-overflow-style:none; scrollbar-width:none;
  }
  .spline-iframe-full::-webkit-scrollbar{ display:none; }
</style>
</head>
<body>

<!-- ======= Carte MARBLE #1 (glass ON) ======= -->
<div class="spline-card"
     data-src="https://marble.worldlabs.ai/worldvr/ab31b740-b8a8-4fc0-91a1-1d6c1e53c422"
     data-title="Impression soleil levant"
     data-display="yes"
     data-glass="yes">
  <div class="spline-live">
    <canvas class="spline-canvas"></canvas>
    <iframe class="spline-iframe"
            allow="xr-spatial-tracking; gyroscope; accelerometer; magnetometer; fullscreen; autoplay"
            allowfullscreen
            loading="lazy" referrerpolicy="no-referrer-when-downgrade" scrolling="no"></iframe>
    <div class="spline-hit" aria-hidden="true"></div>

    <div class="spline-infobar">
      <span class="spline-title">broceliande</span>
      <div class="icon-row">
        <span class="icon-reset text" data-action="open" aria-label="Ouvrir en plein écran">ᯅ</span>
        <span class="icon-reset" data-action="open" aria-label="Plein écran">
          <svg viewBox="0 0 24 24" aria-hidden="true">
            <path d="M7 3h4a1 1 0 1 1 0 2H8v3a1 1 0 1 1-2 0V4c0-.55.45-1 1-1Zm10 0c.55 0 1 .45 1 1v4a1 1 0 1 1-2 0V5h-3a1 1 0 1 1 0-2h4Zm1 16v-4a1 1 0 1 1 2 0v4c0 .55-.45 1-1 1h-4a1 1 0 1 1 0-2h3Zm-9 1H5c-.55 0-1-.45-1-1v-4a1 1 0 1 1 2 0v3h3a1 1 0 1 1 0 2Z" fill="currentColor"/>
          </svg>
        </span>
      </div>
    </div>
  </div>
</div>
<!-- ======= Carte MARBLE #2 (glass ON) ======= -->
<div class="spline-card"
     data-src="https://marble.worldlabs.ai/worldvr/5fcb836f-8d04-4357-ad72-79bba45313b0"
     data-title="Nuit étoilée"
     data-display="yes"
     data-glass="yes">
  <div class="spline-live">
    <canvas class="spline-canvas"></canvas>
    <iframe class="spline-iframe"
            allow="xr-spatial-tracking; gyroscope; accelerometer; magnetometer; fullscreen; autoplay"
            allowfullscreen
            loading="lazy" referrerpolicy="no-referrer-when-downgrade" scrolling="no"></iframe>
    <div class="spline-hit" aria-hidden="true"></div>

    <div class="spline-infobar">
      <span class="spline-title">broceliande3</span>
      <div class="icon-row">
        <span class="icon-reset text" data-action="open" aria-label="Ouvrir en plein écran">ᯅ</span>
        <span class="icon-reset" data-action="open" aria-label="Plein écran">
          <svg viewBox="0 0 24 24" aria-hidden="true">
            <path d="M7 3h4a1 1 0 1 1 0 2H8v3a1 1 0 1 1-2 0V4c0-.55.45-1 1-1Zm10 0c.55 0 1 .45 1 1v4a1 1 0 1 1-2 0V5h-3a1 1 0 1 1 0-2h4Zm1 16v-4a1 1 0 1 1 2 0v4c0 .55-.45 1-1 1h-4a1 1 0 1 1 0-2h3Zm-9 1H5c-.55 0-1-.45-1-1v-4a1 1 0 1 1 2 0v3h3a1 1 0 1 1 0 2Z" fill="currentColor"/>
          </svg>
        </span>
      </div>
    </div>
  </div>
</div>
<!-- ======= Carte MARBLE #3 (glass ON) ======= -->
<div class="spline-card"
     data-src="https://marble.worldlabs.ai/worldvr/130d798e-728f-492b-a5c7-04bfd76ce654"
     data-title="Nuit étoilée"
     data-display="yes"
     data-glass="yes">
  <div class="spline-live">
    <canvas class="spline-canvas"></canvas>
    <iframe class="spline-iframe"
            allow="xr-spatial-tracking; gyroscope; accelerometer; magnetometer; fullscreen; autoplay"
            allowfullscreen
            loading="lazy" referrerpolicy="no-referrer-when-downgrade" scrolling="no"></iframe>
    <div class="spline-hit" aria-hidden="true"></div>

    <div class="spline-infobar">
      <span class="spline-title">broceliande3</span>
      <div class="icon-row">
        <span class="icon-reset text" data-action="open" aria-label="Ouvrir en plein écran">ᯅ</span>
        <span class="icon-reset" data-action="open" aria-label="Plein écran">
          <svg viewBox="0 0 24 24" aria-hidden="true">
            <path d="M7 3h4a1 1 0 1 1 0 2H8v3a1 1 0 1 1-2 0V4c0-.55.45-1 1-1Zm10 0c.55 0 1 .45 1 1v4a1 1 0 1 1-2 0V5h-3a1 1 0 1 1 0-2h4Zm1 16v-4a1 1 0 1 1 2 0v4c0 .55-.45 1-1 1h-4a1 1 0 1 1 0-2h3Zm-9 1H5c-.55 0-1-.45-1-1v-4a1 1 0 1 1 2 0v3h3a1 1 0 1 1 0 2Z" fill="currentColor"/>
          </svg>
        </span>
      </div>
    </div>
  </div>
</div>
<!-- ======= Carte MARBLE #4 (glass ON) ======= -->
<div class="spline-card"
     data-src="https://marble.worldlabs.ai/worldvr/f52fe890-82ed-442b-8bc1-6653160fb3f5"
     data-title="Nuit étoilée"
     data-display="yes"
     data-glass="yes">
  <div class="spline-live">
    <canvas class="spline-canvas"></canvas>
    <iframe class="spline-iframe"
            allow="xr-spatial-tracking; gyroscope; accelerometer; magnetometer; fullscreen; autoplay"
            allowfullscreen
            loading="lazy" referrerpolicy="no-referrer-when-downgrade" scrolling="no"></iframe>
    <div class="spline-hit" aria-hidden="true"></div>

    <div class="spline-infobar">
      <span class="spline-title">broceliande3</span>
      <div class="icon-row">
        <span class="icon-reset text" data-action="open" aria-label="Ouvrir en plein écran">ᯅ</span>
        <span class="icon-reset" data-action="open" aria-label="Plein écran">
          <svg viewBox="0 0 24 24" aria-hidden="true">
            <path d="M7 3h4a1 1 0 1 1 0 2H8v3a1 1 0 1 1-2 0V4c0-.55.45-1 1-1Zm10 0c.55 0 1 .45 1 1v4a1 1 0 1 1-2 0V5h-3a1 1 0 1 1 0-2h4Zm1 16v-4a1 1 0 1 1 2 0v4c0 .55-.45 1-1 1h-4a1 1 0 1 1 0-2h3Zm-9 1H5c-.55 0-1-.45-1-1v-4a1 1 0 1 1 2 0v3h3a1 1 0 1 1 0 2Z" fill="currentColor"/>
          </svg>
        </span>
      </div>
    </div>
  </div>
</div>
<!-- ======= Carte MARBLE #4 (glass ON) ======= -->
<div class="spline-card"
     data-src="https://marble.worldlabs.ai/worldvr/b8389b1f-fdfb-4e8d-b33a-06a06c86905d"
     data-title="Nuit étoilée"
     data-display="yes"
     data-glass="yes">
  <div class="spline-live">
    <canvas class="spline-canvas"></canvas>
    <iframe class="spline-iframe"
            allow="xr-spatial-tracking; gyroscope; accelerometer; magnetometer; fullscreen; autoplay"
            allowfullscreen
            loading="lazy" referrerpolicy="no-referrer-when-downgrade" scrolling="no"></iframe>
    <div class="spline-hit" aria-hidden="true"></div>

    <div class="spline-infobar">
      <span class="spline-title">broceliande3</span>
      <div class="icon-row">
        <span class="icon-reset text" data-action="open" aria-label="Ouvrir en plein écran">ᯅ</span>
        <span class="icon-reset" data-action="open" aria-label="Plein écran">
          <svg viewBox="0 0 24 24" aria-hidden="true">
            <path d="M7 3h4a1 1 0 1 1 0 2H8v3a1 1 0 1 1-2 0V4c0-.55.45-1 1-1Zm10 0c.55 0 1 .45 1 1v4a1 1 0 1 1-2 0V5h-3a1 1 0 1 1 0-2h4Zm1 16v-4a1 1 0 1 1 2 0v4c0 .55-.45 1-1 1h-4a1 1 0 1 1 0-2h3Zm-9 1H5c-.55 0-1-.45-1-1v-4a1 1 0 1 1 2 0v3h3a1 1 0 1 1 0 2Z" fill="currentColor"/>
          </svg>
        </span>
      </div>
    </div>
  </div>
</div>
<!-- ===== Modal partagé ===== -->
<div class="spline-modal" id="splineModal" aria-hidden="true" aria-label="Visionneuse 3D">
  <div class="spline-backdrop" data-close="1"></div>
  <div class="spline-stage">
    <div class="spline-bar">
      <button class="spline-fs" type="button" id="splineFs" aria-label="Plein écran">ᯅ</button>
      <button class="spline-close" type="button" id="splineClose" aria-label="Fermer">✕</button>
    </div>
    <div class="spline-canvas-full">
      <canvas id="splineFullCanvas"></canvas>
      <!-- L’iframe plein écran est injectée ici -->
    </div>
  </div>
</div>

<script>
  // Plein écran utilitaire
  function goFullscreen(el){
    const rfs = el.requestFullscreen || el.webkitRequestFullscreen || el.msRequestFullscreen || el.mozRequestFullScreen;
    if (rfs) rfs.call(el);
  }

  const modal    = document.getElementById('splineModal');
  const fullCv   = document.getElementById('splineFullCanvas');
  const btnFs    = document.getElementById('splineFs');
  const btnClose = document.getElementById('splineClose');

  let fullIframe = null;

  // Init chaque carte Marble
  document.querySelectorAll('.spline-card').forEach((card)=>{
    // Commande d’affichage
    const display = (card.getAttribute('data-display') || 'yes').toLowerCase().trim();
    if(display === 'no'){
      card.style.display = 'none';
      return; // on n'initialise pas cette carte
    }

    const url    = card.getAttribute('data-src');
    const iframe = card.querySelector('.spline-iframe');

    iframe.src = url;
    iframe.setAttribute('allow','xr-spatial-tracking; gyroscope; accelerometer; magnetometer; fullscreen; autoplay');
    iframe.setAttribute('allowfullscreen','');
    iframe.setAttribute('scrolling','no');

    card.addEventListener('click',(e)=>{
      const icon = e.target.closest('[data-action="open"]');
      if(!icon) return;
      openModal(url);
    });
  });

  function openModal(url){
    if(fullIframe){ fullIframe.remove(); fullIframe=null; }

    fullIframe = document.createElement('iframe');
    fullIframe.className = 'spline-iframe-full';
    fullIframe.src = url;
    fullIframe.allow = "xr-spatial-tracking; gyroscope; accelerometer; magnetometer; fullscreen; autoplay";
    fullIframe.allowFullscreen = true;
    fullIframe.setAttribute('scrolling','no');
    fullCv.insertAdjacentElement('afterend', fullIframe);

    modal.setAttribute('aria-hidden','false');
    document.documentElement.style.overflow='hidden';
    document.body.style.overflow='hidden';
  }

  function closeModal(){
    modal.setAttribute('aria-hidden','true');
    document.documentElement.style.overflow='';
    document.body.style.overflow='';
    if(fullIframe){ fullIframe.remove(); fullIframe=null; }
  }

  btnClose.addEventListener('click', closeModal);
  btnFs.addEventListener('click', ()=> goFullscreen(fullIframe || fullCv) );
  modal.addEventListener('click',(e)=>{ if(e.target.dataset.close) closeModal(); });
  window.addEventListener('keydown',(e)=>{ if(e.key==='Escape' && modal.getAttribute('aria-hidden')==='false') closeModal(); });
</script>

</body>
</html>
				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-6153d7e e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="6153d7e" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-95b21cc elementor-widget elementor-widget-html" data-id="95b21cc" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@600;700&display=swap" rel="stylesheet" />

<style>
  /* ===== Carte (style d’origine) + bordure légère & ombre douce ===== */
  .spline-card{
    position:relative; border-radius:16px; overflow:hidden;
    border:1px solid rgba(255,255,255,.14);
    box-shadow:
      0 12px 12px rgba(0,0,0,.24),
      0 12px 6px  rgba(0,0,0,.24);
    transform:translateZ(0); backface-visibility:hidden;
    transition:box-shadow .25s ease, filter .25s ease, transform .22s ease;
    margin:0 0 24px;
    background:#000; /* évite toute marge transparente parasite */
  }
  .spline-card:hover{
    transform:scale(1.022);
    box-shadow:
      0 20px 48px rgba(0,0,0,.28),
      0 4px 10px  rgba(0,0,0,.16);
  }

  /* ===== Glass effect (toggle) =====
     Active-le par carte avec data-glass="yes" */
  .spline-card[data-glass="yes"]{
    background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03));
    border-color: rgba(255,255,255,.22);
    backdrop-filter: blur(10px) saturate(120%);
    -webkit-backdrop-filter: blur(10px) saturate(120%);
    /* léger liseré intérieur pour un rendu “verre” sans masquer le contenu */
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,.14),
      0 12px 12px rgba(0,0,0,.24),
      0 12px 6px  rgba(0,0,0,.24);
  }

  .spline-live{
    position:relative; width:100%; aspect-ratio:16/9; background:#000; overflow:hidden;
    /* Anti-bande noire horizontale */
    --overscan: 12px;
    /* Mini overscan vertical (corrige le fin liseré en bas/haut) */
    --overscanY: 2px; /* ajuste 0–4px si besoin */
  }
  .spline-canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; border:0; }

  /* Iframe Marble : overscan centré + no-scrollbar */
  .spline-iframe{
    position:absolute;
    top:    calc(0px - var(--overscanY));
    bottom: calc(0px - var(--overscanY));
    left:   calc(10px - var(--overscan));
    right:  calc(10px - var(--overscan));
    width:  calc(100% + (var(--overscan) * 2));
    height: calc(100% + (var(--overscanY) * 2));
    display:block; border:0; background:#000;
    transform:translateZ(0); backface-visibility:hidden;
    -ms-overflow-style:none; scrollbar-width:none;
  }
  .spline-iframe::-webkit-scrollbar{ display:none; }

  /* Laisse le scroll à la page */
  .spline-hit{ position:absolute; inset:0; pointer-events:none; }

  /* Infobar (bas de carte) */
  .spline-infobar{
    position:absolute; left:0; right:0; bottom:0; z-index:3;
    display:flex; align-items:center; justify-content:space-between;
    padding:10px 12px; color:#fff;
    background:linear-gradient(180deg, rgba(0,0,0,0) 45%, rgba(0,0,0,.58) 100%);
    user-select:none;
  }
  .spline-title{
    margin:0; color:#fff;
    font:600 14px/1.2 "Inter Tight", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
    text-shadow:0 1px 6px rgba(0,0,0,.45);
  }

  /* Icônes pures */
  .icon-reset{ all: unset; display:inline-block; color:#fff; line-height:1; cursor:pointer; vertical-align:middle; }
  .icon-row{ display:flex; gap:10px; align-items:center; }
  .icon-reset svg{ width:20px; height:20px; display:block; }
  .icon-reset.text{ font-family:"Inter Tight", system-ui; font-weight:700; font-size:18px; }

  /* ===== Modal partagé (plein écran) ===== */
  .spline-modal{ position:fixed; inset:0; z-index:9999; display:none; }
  .spline-modal[aria-hidden="false"]{ display:block; }
  .spline-backdrop{ position:absolute; inset:0; background:rgba(0,0,0,.78); }
  .spline-stage{ position:absolute; inset:0; display:flex; flex-direction:column; background:#000; overflow:hidden; }
  .spline-bar{
    display:flex; justify-content:flex-end; gap:12px; padding:8px 12px; color:#fff;
    background:rgba(0,0,0,.45); border-bottom:1px solid rgba(255,255,255,.1);
    backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
  }
  .spline-fs, .spline-close{ all: unset; color:#fff; cursor:pointer; font:700 18px/1 "Inter Tight", system-ui; }

  .spline-canvas-full{
    position:relative; flex:1 1 auto; overflow:hidden; background:#000;
    --overscan: 12px;  /* horizontal */
    --overscanY: 2px;  /* vertical */
  }
  .spline-canvas-full canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; border:0; }

  .spline-iframe-full{
    position:absolute;
    top:    calc(0px - var(--overscanY));
    bottom: calc(0px - var(--overscanY));
    left:   calc(12px - var(--overscan));
    right:  calc(12px - var(--overscan));
    width:  calc(100% + (var(--overscan) * 2));
    height: calc(100% + (var(--overscanY) * 2));
    display:block; border:0; background:#000;
    transform:translateZ(0); backface-visibility:hidden;
    -ms-overflow-style:none; scrollbar-width:none;
  }
  .spline-iframe-full::-webkit-scrollbar{ display:none; }
</style>
</head>
<body>

<!-- ======= Carte MARBLE #1 (glass ON) ======= -->
<div class="spline-card"
     data-src=https://marble.worldlabs.ai/worldvr/130d798e-728f-492b-a5c7-04bfd76ce654"
     data-title="Tsunami"
     data-display="yes"
     data-glass="yes">
  <div class="spline-live">
    <canvas class="spline-canvas"></canvas>
    <iframe class="spline-iframe"
            allow="xr-spatial-tracking; gyroscope; accelerometer; magnetometer; fullscreen; autoplay"
            allowfullscreen
            loading="lazy" referrerpolicy="no-referrer-when-downgrade" scrolling="no"></iframe>
    <div class="spline-hit" aria-hidden="true"></div>

    <div class="spline-infobar">
      <span class="spline-title">broceliande</span>
      <div class="icon-row">
        <span class="icon-reset text" data-action="open" aria-label="Ouvrir en plein écran">ᯅ</span>
        <span class="icon-reset" data-action="open" aria-label="Plein écran">
          <svg viewBox="0 0 24 24" aria-hidden="true">
            <path d="M7 3h4a1 1 0 1 1 0 2H8v3a1 1 0 1 1-2 0V4c0-.55.45-1 1-1Zm10 0c.55 0 1 .45 1 1v4a1 1 0 1 1-2 0V5h-3a1 1 0 1 1 0-2h4Zm1 16v-4a1 1 0 1 1 2 0v4c0 .55-.45 1-1 1h-4a1 1 0 1 1 0-2h3Zm-9 1H5c-.55 0-1-.45-1-1v-4a1 1 0 1 1 2 0v3h3a1 1 0 1 1 0 2Z" fill="currentColor"/>
          </svg>
        </span>
      </div>
    </div>
  </div>
</div>
<!-- ======= Carte MARBLE #2 (glass ON) ======= -->
<div class="spline-card"
     data-src="https://marble.worldlabs.ai/worldvr/b339a26a-74da-4a01-bad8-9269f35e4024"
     data-title="broceliande3"
     data-display="yes"
     data-glass="yes">
  <div class="spline-live">
    <canvas class="spline-canvas"></canvas>
    <iframe class="spline-iframe"
            allow="xr-spatial-tracking; gyroscope; accelerometer; magnetometer; fullscreen; autoplay"
            allowfullscreen
            loading="lazy" referrerpolicy="no-referrer-when-downgrade" scrolling="no"></iframe>
    <div class="spline-hit" aria-hidden="true"></div>

    <div class="spline-infobar">
      <span class="spline-title">broceliande3</span>
      <div class="icon-row">
        <span class="icon-reset text" data-action="open" aria-label="Ouvrir en plein écran">ᯅ</span>
        <span class="icon-reset" data-action="open" aria-label="Plein écran">
          <svg viewBox="0 0 24 24" aria-hidden="true">
            <path d="M7 3h4a1 1 0 1 1 0 2H8v3a1 1 0 1 1-2 0V4c0-.55.45-1 1-1Zm10 0c.55 0 1 .45 1 1v4a1 1 0 1 1-2 0V5h-3a1 1 0 1 1 0-2h4Zm1 16v-4a1 1 0 1 1 2 0v4c0 .55-.45 1-1 1h-4a1 1 0 1 1 0-2h3Zm-9 1H5c-.55 0-1-.45-1-1v-4a1 1 0 1 1 2 0v3h3a1 1 0 1 1 0 2Z" fill="currentColor"/>
          </svg>
        </span>
      </div>
    </div>
  </div>
</div>
<!-- ===== Modal partagé ===== -->
<div class="spline-modal" id="splineModal" aria-hidden="true" aria-label="Visionneuse 3D">
  <div class="spline-backdrop" data-close="1"></div>
  <div class="spline-stage">
    <div class="spline-bar">
      <button class="spline-fs" type="button" id="splineFs" aria-label="Plein écran">ᯅ</button>
      <button class="spline-close" type="button" id="splineClose" aria-label="Fermer">✕</button>
    </div>
    <div class="spline-canvas-full">
      <canvas id="splineFullCanvas"></canvas>
      <!-- L’iframe plein écran est injectée ici -->
    </div>
  </div>
</div>

<script>
  // Plein écran utilitaire
  function goFullscreen(el){
    const rfs = el.requestFullscreen || el.webkitRequestFullscreen || el.msRequestFullscreen || el.mozRequestFullScreen;
    if (rfs) rfs.call(el);
  }

  const modal    = document.getElementById('splineModal');
  const fullCv   = document.getElementById('splineFullCanvas');
  const btnFs    = document.getElementById('splineFs');
  const btnClose = document.getElementById('splineClose');

  let fullIframe = null;

  // Init chaque carte Marble
  document.querySelectorAll('.spline-card').forEach((card)=>{
    // Commande d’affichage
    const display = (card.getAttribute('data-display') || 'yes').toLowerCase().trim();
    if(display === 'no'){
      card.style.display = 'none';
      return; // on n'initialise pas cette carte
    }

    const url    = card.getAttribute('data-src');
    const iframe = card.querySelector('.spline-iframe');

    iframe.src = url;
    iframe.setAttribute('allow','xr-spatial-tracking; gyroscope; accelerometer; magnetometer; fullscreen; autoplay');
    iframe.setAttribute('allowfullscreen','');
    iframe.setAttribute('scrolling','no');

    card.addEventListener('click',(e)=>{
      const icon = e.target.closest('[data-action="open"]');
      if(!icon) return;
      openModal(url);
    });
  });

  function openModal(url){
    if(fullIframe){ fullIframe.remove(); fullIframe=null; }

    fullIframe = document.createElement('iframe');
    fullIframe.className = 'spline-iframe-full';
    fullIframe.src = url;
    fullIframe.allow = "xr-spatial-tracking; gyroscope; accelerometer; magnetometer; fullscreen; autoplay";
    fullIframe.allowFullscreen = true;
    fullIframe.setAttribute('scrolling','no');
    fullCv.insertAdjacentElement('afterend', fullIframe);

    modal.setAttribute('aria-hidden','false');
    document.documentElement.style.overflow='hidden';
    document.body.style.overflow='hidden';
  }

  function closeModal(){
    modal.setAttribute('aria-hidden','true');
    document.documentElement.style.overflow='';
    document.body.style.overflow='';
    if(fullIframe){ fullIframe.remove(); fullIframe=null; }
  }

  btnClose.addEventListener('click', closeModal);
  btnFs.addEventListener('click', ()=> goFullscreen(fullIframe || fullCv) );
  modal.addEventListener('click',(e)=>{ if(e.target.dataset.close) closeModal(); });
  window.addEventListener('keydown',(e)=>{ if(e.key==='Escape' && modal.getAttribute('aria-hidden')==='false') closeModal(); });
</script>

</body>
</html>
				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-d2f86d3 e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="d2f86d3" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-a8a19f9 elementor-widget elementor-widget-html" data-id="a8a19f9" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@600;700&display=swap" rel="stylesheet" />

<style>
  /* ===== Carte (style d’origine) + bordure légère & ombre douce ===== */
  .spline-card{
    position:relative; border-radius:16px; overflow:hidden;
    border:1px solid rgba(255,255,255,.14);
    box-shadow:
      0 12px 12px rgba(0,0,0,.24),
      0 12px 6px  rgba(0,0,0,.24);
    transform:translateZ(0); backface-visibility:hidden;
    transition:box-shadow .25s ease, filter .25s ease, transform .22s ease;
    margin:0 0 24px;
    background:#000; /* évite toute marge transparente parasite */
  }
  .spline-card:hover{
    transform:scale(1.022);
    box-shadow:
      0 20px 48px rgba(0,0,0,.28),
      0 4px 10px  rgba(0,0,0,.16);
  }

  /* ===== Glass effect (toggle) =====
     Active-le par carte avec data-glass="yes" */
  .spline-card[data-glass="yes"]{
    background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03));
    border-color: rgba(255,255,255,.22);
    backdrop-filter: blur(10px) saturate(120%);
    -webkit-backdrop-filter: blur(10px) saturate(120%);
    /* léger liseré intérieur pour un rendu “verre” sans masquer le contenu */
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,.14),
      0 12px 12px rgba(0,0,0,.24),
      0 12px 6px  rgba(0,0,0,.24);
  }

  .spline-live{
    position:relative; width:100%; aspect-ratio:16/9; background:#000; overflow:hidden;
    /* Anti-bande noire horizontale */
    --overscan: 12px;
    /* Mini overscan vertical (corrige le fin liseré en bas/haut) */
    --overscanY: 2px; /* ajuste 0–4px si besoin */
  }
  .spline-canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; border:0; }

  /* Iframe Marble : overscan centré + no-scrollbar */
  .spline-iframe{
    position:absolute;
    top:    calc(0px - var(--overscanY));
    bottom: calc(0px - var(--overscanY));
    left:   calc(10px - var(--overscan));
    right:  calc(10px - var(--overscan));
    width:  calc(100% + (var(--overscan) * 2));
    height: calc(100% + (var(--overscanY) * 2));
    display:block; border:0; background:#000;
    transform:translateZ(0); backface-visibility:hidden;
    -ms-overflow-style:none; scrollbar-width:none;
  }
  .spline-iframe::-webkit-scrollbar{ display:none; }

  /* Laisse le scroll à la page */
  .spline-hit{ position:absolute; inset:0; pointer-events:none; }

  /* Infobar (bas de carte) */
  .spline-infobar{
    position:absolute; left:0; right:0; bottom:0; z-index:3;
    display:flex; align-items:center; justify-content:space-between;
    padding:10px 12px; color:#fff;
    background:linear-gradient(180deg, rgba(0,0,0,0) 45%, rgba(0,0,0,.58) 100%);
    user-select:none;
  }
  .spline-title{
    margin:0; color:#fff;
    font:600 14px/1.2 "Inter Tight", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
    text-shadow:0 1px 6px rgba(0,0,0,.45);
  }

  /* Icônes pures */
  .icon-reset{ all: unset; display:inline-block; color:#fff; line-height:1; cursor:pointer; vertical-align:middle; }
  .icon-row{ display:flex; gap:10px; align-items:center; }
  .icon-reset svg{ width:20px; height:20px; display:block; }
  .icon-reset.text{ font-family:"Inter Tight", system-ui; font-weight:700; font-size:18px; }

  /* ===== Modal partagé (plein écran) ===== */
  .spline-modal{ position:fixed; inset:0; z-index:9999; display:none; }
  .spline-modal[aria-hidden="false"]{ display:block; }
  .spline-backdrop{ position:absolute; inset:0; background:rgba(0,0,0,.78); }
  .spline-stage{ position:absolute; inset:0; display:flex; flex-direction:column; background:#000; overflow:hidden; }
  .spline-bar{
    display:flex; justify-content:flex-end; gap:12px; padding:8px 12px; color:#fff;
    background:rgba(0,0,0,.45); border-bottom:1px solid rgba(255,255,255,.1);
    backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
  }
  .spline-fs, .spline-close{ all: unset; color:#fff; cursor:pointer; font:700 18px/1 "Inter Tight", system-ui; }

  .spline-canvas-full{
    position:relative; flex:1 1 auto; overflow:hidden; background:#000;
    --overscan: 12px;  /* horizontal */
    --overscanY: 2px;  /* vertical */
  }
  .spline-canvas-full canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; border:0; }

  .spline-iframe-full{
    position:absolute;
    top:    calc(0px - var(--overscanY));
    bottom: calc(0px - var(--overscanY));
    left:   calc(12px - var(--overscan));
    right:  calc(12px - var(--overscan));
    width:  calc(100% + (var(--overscan) * 2));
    height: calc(100% + (var(--overscanY) * 2));
    display:block; border:0; background:#000;
    transform:translateZ(0); backface-visibility:hidden;
    -ms-overflow-style:none; scrollbar-width:none;
  }
  .spline-iframe-full::-webkit-scrollbar{ display:none; }
</style>
</head>
<body>

<!-- ======= Carte MARBLE #1 (glass ON) ======= -->
<div class="spline-card"
     data-src="https://marble.worldlabs.ai/worldvr/cf74c541-11cb-412f-b658-b3d87e7604d0"
     data-title="broceliande"
     data-display="yes"
     data-glass="yes">
  <div class="spline-live">
    <canvas class="spline-canvas"></canvas>
    <iframe class="spline-iframe"
            allow="xr-spatial-tracking; gyroscope; accelerometer; magnetometer; fullscreen; autoplay"
            allowfullscreen
            loading="lazy" referrerpolicy="no-referrer-when-downgrade" scrolling="no"></iframe>
    <div class="spline-hit" aria-hidden="true"></div>

    <div class="spline-infobar">
      <span class="spline-title">broceliande</span>
      <div class="icon-row">
        <span class="icon-reset text" data-action="open" aria-label="Ouvrir en plein écran">ᯅ</span>
        <span class="icon-reset" data-action="open" aria-label="Plein écran">
          <svg viewBox="0 0 24 24" aria-hidden="true">
            <path d="M7 3h4a1 1 0 1 1 0 2H8v3a1 1 0 1 1-2 0V4c0-.55.45-1 1-1Zm10 0c.55 0 1 .45 1 1v4a1 1 0 1 1-2 0V5h-3a1 1 0 1 1 0-2h4Zm1 16v-4a1 1 0 1 1 2 0v4c0 .55-.45 1-1 1h-4a1 1 0 1 1 0-2h3Zm-9 1H5c-.55 0-1-.45-1-1v-4a1 1 0 1 1 2 0v3h3a1 1 0 1 1 0 2Z" fill="currentColor"/>
          </svg>
        </span>
      </div>
    </div>
  </div>
</div>
<!-- ======= Carte MARBLE #2 (glass ON) ======= -->
<div class="spline-card"
     data-src="https://marble.worldlabs.ai/worldvr/b8389b1f-fdfb-4e8d-b33a-06a06c86905d"
     data-title="Turner"
     data-display="yes"
     data-glass="yes">
  <div class="spline-live">
    <canvas class="spline-canvas"></canvas>
    <iframe class="spline-iframe"
            allow="xr-spatial-tracking; gyroscope; accelerometer; magnetometer; fullscreen; autoplay"
            allowfullscreen
            loading="lazy" referrerpolicy="no-referrer-when-downgrade" scrolling="no"></iframe>
    <div class="spline-hit" aria-hidden="true"></div>

    <div class="spline-infobar">
      <span class="spline-title">broceliande3</span>
      <div class="icon-row">
        <span class="icon-reset text" data-action="open" aria-label="Ouvrir en plein écran">ᯅ</span>
        <span class="icon-reset" data-action="open" aria-label="Plein écran">
          <svg viewBox="0 0 24 24" aria-hidden="true">
            <path d="M7 3h4a1 1 0 1 1 0 2H8v3a1 1 0 1 1-2 0V4c0-.55.45-1 1-1Zm10 0c.55 0 1 .45 1 1v4a1 1 0 1 1-2 0V5h-3a1 1 0 1 1 0-2h4Zm1 16v-4a1 1 0 1 1 2 0v4c0 .55-.45 1-1 1h-4a1 1 0 1 1 0-2h3Zm-9 1H5c-.55 0-1-.45-1-1v-4a1 1 0 1 1 2 0v3h3a1 1 0 1 1 0 2Z" fill="currentColor"/>
          </svg>
        </span>
      </div>
    </div>
  </div>
</div>
<!-- ===== Modal partagé ===== -->
<div class="spline-modal" id="splineModal" aria-hidden="true" aria-label="Visionneuse 3D">
  <div class="spline-backdrop" data-close="1"></div>
  <div class="spline-stage">
    <div class="spline-bar">
      <button class="spline-fs" type="button" id="splineFs" aria-label="Plein écran">ᯅ</button>
      <button class="spline-close" type="button" id="splineClose" aria-label="Fermer">✕</button>
    </div>
    <div class="spline-canvas-full">
      <canvas id="splineFullCanvas"></canvas>
      <!-- L’iframe plein écran est injectée ici -->
    </div>
  </div>
</div>

<script>
  // Plein écran utilitaire
  function goFullscreen(el){
    const rfs = el.requestFullscreen || el.webkitRequestFullscreen || el.msRequestFullscreen || el.mozRequestFullScreen;
    if (rfs) rfs.call(el);
  }

  const modal    = document.getElementById('splineModal');
  const fullCv   = document.getElementById('splineFullCanvas');
  const btnFs    = document.getElementById('splineFs');
  const btnClose = document.getElementById('splineClose');

  let fullIframe = null;

  // Init chaque carte Marble
  document.querySelectorAll('.spline-card').forEach((card)=>{
    // Commande d’affichage
    const display = (card.getAttribute('data-display') || 'yes').toLowerCase().trim();
    if(display === 'no'){
      card.style.display = 'none';
      return; // on n'initialise pas cette carte
    }

    const url    = card.getAttribute('data-src');
    const iframe = card.querySelector('.spline-iframe');

    iframe.src = url;
    iframe.setAttribute('allow','xr-spatial-tracking; gyroscope; accelerometer; magnetometer; fullscreen; autoplay');
    iframe.setAttribute('allowfullscreen','');
    iframe.setAttribute('scrolling','no');

    card.addEventListener('click',(e)=>{
      const icon = e.target.closest('[data-action="open"]');
      if(!icon) return;
      openModal(url);
    });
  });

  function openModal(url){
    if(fullIframe){ fullIframe.remove(); fullIframe=null; }

    fullIframe = document.createElement('iframe');
    fullIframe.className = 'spline-iframe-full';
    fullIframe.src = url;
    fullIframe.allow = "xr-spatial-tracking; gyroscope; accelerometer; magnetometer; fullscreen; autoplay";
    fullIframe.allowFullscreen = true;
    fullIframe.setAttribute('scrolling','no');
    fullCv.insertAdjacentElement('afterend', fullIframe);

    modal.setAttribute('aria-hidden','false');
    document.documentElement.style.overflow='hidden';
    document.body.style.overflow='hidden';
  }

  function closeModal(){
    modal.setAttribute('aria-hidden','true');
    document.documentElement.style.overflow='';
    document.body.style.overflow='';
    if(fullIframe){ fullIframe.remove(); fullIframe=null; }
  }

  btnClose.addEventListener('click', closeModal);
  btnFs.addEventListener('click', ()=> goFullscreen(fullIframe || fullCv) );
  modal.addEventListener('click',(e)=>{ if(e.target.dataset.close) closeModal(); });
  window.addEventListener('keydown',(e)=>{ if(e.key==='Escape' && modal.getAttribute('aria-hidden')==='false') closeModal(); });
</script>

</body>
</html>
				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-ef1f3b3 e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="ef1f3b3" data-element_type="container" data-e-type="container">
				</div>
					</div>
				</div>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/vr_xprmnts/">VR_Xprmnts</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Fluffybrick AR</title>
		<link>https://presentcomposedesign.fr/fluffybrick-ar/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Tue, 17 Dec 2024 14:02:23 +0000</pubDate>
				<category><![CDATA[[ia]]]></category>
		<category><![CDATA[Design graphique]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=30699</guid>

					<description><![CDATA[<p>Fluffybrick [AR] est un remake saturé de l’œuvre de 2022. Trois risographies uniques et 50 stickers imprimés, chacun accompagné de son jumeau numérique NFT. L’œuvre combine risographie, IA générative et réalité augmentée pour questionner la saturation visuelle et sonore à l’ère des outils créatifs démocratisés.</p>
<p>Cet article <a href="https://presentcomposedesign.fr/fluffybrick-ar/">Fluffybrick AR</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="30699" class="elementor elementor-30699">
				<div class="elementor-element elementor-element-7d7d18b e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent" data-id="7d7d18b" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-36b5610 elementor-widget__width-inherit elementor-absolute elementor-widget elementor-widget-html" data-id="36b5610" data-element_type="widget" data-e-type="widget" data-settings="{&quot;_position&quot;:&quot;absolute&quot;}" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<div id="container">
  <canvas id="canvas3d"></canvas>
</div>

<style>
  body {
    background-color: #FFFFFF00;
    margin: 0;
    overflow: hidden;
  }

  #container {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    height: 100% !important;
  }

  #canvas3d {
    width: 100% !important;
    height: 100% !important;
  }
</style>

<script type="module">
  const fileName = "aEutQhNMO3QPL-pO";
  import { Application } from "https://esm.sh/@splinetool/runtime";
  const canvas = document.getElementById('canvas3d');
  const app = new Application(canvas);
  const urlParams = new URLSearchParams(window.location.search);
  const colorValue = urlParams.size > 1 ? urlParams.get('destinationValue') : 3.2;

  app.load(`https://prod.spline.design/${fileName}/scene.splinecode`);
</script>
				</div>
				</div>
					</div>
				</div>
				<section class="elementor-section elementor-top-section elementor-element elementor-element-cd23dd4 elementor-section-full_width elementor-section-height-min-height elementor-section-items-bottom elementor-section-content-middle elementor-section-stretched elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no" data-id="cd23dd4" data-element_type="section" data-e-type="section" data-settings="{&quot;stretch_section&quot;:&quot;section-stretched&quot;,&quot;background_background&quot;:&quot;classic&quot;}">
						<div class="elementor-container elementor-column-gap-extended">
					<div class="elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-4c4f73e" data-id="4c4f73e" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<section class="elementor-section elementor-inner-section elementor-element elementor-element-33dfe8e elementor-section-boxed elementor-section-height-default elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no" data-id="33dfe8e" data-element_type="section" data-e-type="section">
						<div class="elementor-container elementor-column-gap-default">
					<div class="elementor-column elementor-col-100 elementor-inner-column elementor-element elementor-element-30362e9" data-id="30362e9" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<div class="elementor-element elementor-element-e27097c elementor-widget elementor-widget-heading" data-id="e27097c" data-element_type="widget" data-e-type="widget" data-widget_type="heading.default">
				<div class="elementor-widget-container">
					<h1 class="elementor-heading-title elementor-size-default">Fluffybrick <span style=", sans-serif;font-size: 8vw;font-style: normal;font-weight: 700;background-color: var( --e-global-color-a81baed )">[AR]</span><span style=", sans-serif;font-size: 8vw;font-style: normal;font-weight: 700;background-color: var( --e-global-color-a81baed )"> </span><span style=", sans-serif;font-size: 8vw;font-style: normal;font-weight: 700;background-color: var( --e-global-color-a81baed )"></span><br></h1>				</div>
				</div>
					</div>
		</div>
					</div>
		</section>
		<div class="elementor-element elementor-element-b30017a e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no elementor-invisible e-con e-parent" data-id="b30017a" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;fadeInUp&quot;}">
					<div class="e-con-inner">
		<div class="elementor-element elementor-element-c2492ce e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="c2492ce" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-b6c88d8 elementor-widget__width-initial elementor-widget elementor-widget-text-editor" data-id="b6c88d8" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h1>Fluffybrick [AR]</h1>
<div data-vce-boxed-width="true">
<div data-vce-do-apply="all el-ba3f564d">
<div data-vce-element-content="true">
<div data-vce-do-apply="border margin background el-11f4390c">
<div data-vce-element-content="true" data-vce-do-apply="padding el-11f4390c">
<div><span style="color: #0010ef;">&nbsp;</span></div>
<h2>On peut considérer cette production comme un remake saturé version 2024 de  » l&rsquo;œuvre » initiale « Fluffybrick » produite en 2022 et pour laquelle les outils d&rsquo;intelligence artificielles génératifs utilisés n&rsquo;en étaient alors qu&rsquo;à leurs balbutiements.<p></p>
<p>Fluffybrick [AR] est une production intéractive, multimédia existant physiquement sous la forme de trois reproductions uniques pour chacune d&rsquo;entres elles puisqu&rsquo; imprimées en risographie.<br>Il est volontaire qu&rsquo;au premier abord l&rsquo;aspect graphique et visuel de la production paraisse relativement cloisonné et peu esthétique. En effet à l&rsquo;inverse de la version Fluffybrick de 2022, elle aussi imprimée en 10 exemplaires en risographie mais en bichromie, une troisième couche d&rsquo;encre noire mettant en avant les éléments de codes nécessaires à la génération de telles images (d&rsquo;ordinaire cachés) ont été volontairement superposés sur l&rsquo;image elle-même.</p>
<p>En y regardant de plus près on comprend alors qu&rsquo;une seconde lecture de l&rsquo;œuvre devient possible en utilisant son outil de prédilection du moment, à savoir, son smartphone.</p>
<p>En scannant le QR code présent sur la production risographique, et de ce fait grâce à votre « amulette numérique » fluffybrick se libère et apparaît sous une nouvelle forme qui relève cette fois-ci de la vidéo en réalité augmentée où là aussi, l&rsquo;intégralité des contenus qu&rsquo;ils soient visuels ou audios, ont volontairement été générés via ia.</p>
<p>Au delà du caractère absurde de son sujet Fluffybrick [AR] vise à provoquer  une forme de saturation à la fois visuelle et sonore qui questionne sur le rôle des créateurs, artistes, rêveurs, désormais à une époque où l&rsquo;accès et l&rsquo;utilisation « d&rsquo;outils ? »  sont devenus on ne peut plus massifs et démocratisés.&nbsp;</p>
<p><strong><span style="color: #0010ef;">Créations&nbsp; artistiques</span></strong> personnelles <strong><span style="color: #0010ef;">obtenues en utilisant des outils d&rsquo;intelligence artificielle générative</span></strong> entre autres logiciels, retouches &amp; modifications <strong><span style="color: #0010ef;">dans leur processus de réalisation</span></strong>.<br>PrésentComposédesign-2024</p>
</h2>
<h2 style="text-align: center;"><span style="color: #0010ef;"><strong><i class="fa-solid fa-chevron-down"></i>&nbsp;</strong></span></h2>
<h2 style="text-align: center;"><span style="color: #0010ef;"><strong>Vous êtes intéressé par l&rsquo;acquisition d&rsquo;un de ces 3 exemplaires ?&nbsp;</strong></span></h2>
<div><span style="color: #0010ef;"><strong><br></strong></span></div>
</div>
</div>
</div>
</div>
</div>
<div data-vce-boxed-width="true">
<div data-vce-do-apply="all el-d9d9a9fd">
<div data-vce-element-content="true">
<div>
<div data-vce-do-apply="border margin background el-87440d41">
<div data-vce-element-content="true" data-vce-do-apply="padding el-87440d41">
<p><!-- /wp:vcwb/empty-comment-element-wrapper --></p>
</div>
</div>
</div>
<p><!-- /vcwb/dynamicElementComment:87440d41 --></p>
</div>
</div>
</div>
<p><!-- /vcwb/dynamicElementComment:d9d9a9fd --><!--vcv no format--></p>								</div>
				</div>
				<div class="elementor-element elementor-element-ec35dd3 elementor-align-center elementor-widget elementor-widget-button" data-id="ec35dd3" data-element_type="widget" data-e-type="widget" data-widget_type="button.default">
				<div class="elementor-widget-container">
									<div class="elementor-button-wrapper">
					<a class="elementor-button elementor-button-link elementor-size-sm" href="https://presentcomposedesign.fr/contact/">
						<span class="elementor-button-content-wrapper">
									<span class="elementor-button-text">Cliquez ici</span>
					</span>
					</a>
				</div>
								</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-385e30c e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="385e30c" data-element_type="container" data-e-type="container">
		<div class="elementor-element elementor-element-47b7392 e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="47b7392" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-f41da31 elementor-widget elementor-widget-video" data-id="f41da31" data-element_type="widget" data-e-type="widget" data-settings="{&quot;video_type&quot;:&quot;hosted&quot;,&quot;autoplay&quot;:&quot;yes&quot;,&quot;play_on_mobile&quot;:&quot;yes&quot;,&quot;loop&quot;:&quot;yes&quot;,&quot;controls&quot;:&quot;yes&quot;}" data-widget_type="video.default">
				<div class="elementor-widget-container">
							<div class="e-hosted-video elementor-wrapper elementor-open-inline">
					<video class="elementor-video" src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/Sequence-01.mp4" autoplay="" loop="" controls="" playsinline="" controlsList="nodownload"></video>
				</div>
						</div>
				</div>
					</div>
				</div>
		<div class="elementor-element elementor-element-017f9e7 e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="017f9e7" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-2d7cb4d wpr-grid-columns-1 wpr-grid-columns--tablet3 wpr-grid-columns--mobile1 wpr-grid-pagination-center elementor-widget elementor-widget-wpr-media-grid" data-id="2d7cb4d" data-element_type="widget" data-e-type="widget" data-widget_type="wpr-media-grid.default">
				<div class="elementor-widget-container">
					<section class="wpr-grid wpr-media-grid elementor-clearfix" data-settings="{&quot;layout&quot;:&quot;masonry&quot;,&quot;columns_desktop&quot;:&quot;1&quot;,&quot;gutter_hr&quot;:1,&quot;gutter_hr_mobile&quot;:1,&quot;gutter_hr_mobile_extra&quot;:1,&quot;gutter_hr_tablet&quot;:1,&quot;gutter_hr_tablet_extra&quot;:1,&quot;gutter_hr_laptop&quot;:1,&quot;gutter_hr_widescreen&quot;:1,&quot;gutter_vr&quot;:1,&quot;gutter_vr_mobile&quot;:1,&quot;gutter_vr_mobile_extra&quot;:1,&quot;gutter_vr_tablet&quot;:1,&quot;gutter_vr_tablet_extra&quot;:1,&quot;gutter_vr_laptop&quot;:1,&quot;gutter_vr_widescreen&quot;:1,&quot;animation&quot;:&quot;default&quot;,&quot;animation_duration&quot;:0.3,&quot;animation_delay&quot;:0.1,&quot;deeplinking&quot;:&quot;&quot;,&quot;filters_default_filter&quot;:&quot;&quot;,&quot;filters_linkable&quot;:&quot;no&quot;,&quot;filters_count&quot;:&quot;&quot;,&quot;filters_hide_empty&quot;:&quot;no&quot;,&quot;filters_animation&quot;:&quot;default&quot;,&quot;filters_animation_duration&quot;:0.3,&quot;filters_animation_delay&quot;:0.1,&quot;pagination_type&quot;:&quot;load-more&quot;,&quot;pagination_max_pages&quot;:1,&quot;lightbox&quot;:{&quot;selector&quot;:&quot;.wpr-grid-image-wrap&quot;,&quot;iframeMaxWidth&quot;:&quot;60%&quot;,&quot;hash&quot;:false,&quot;autoplay&quot;:&quot;true&quot;,&quot;pause&quot;:5000,&quot;progressBar&quot;:&quot;true&quot;,&quot;counter&quot;:&quot;true&quot;,&quot;controls&quot;:&quot;true&quot;,&quot;getCaptionFromTitleOrAlt&quot;:&quot;true&quot;,&quot;thumbnail&quot;:&quot;&quot;,&quot;showThumbByDefault&quot;:&quot;&quot;,&quot;share&quot;:&quot;&quot;,&quot;zoom&quot;:&quot;true&quot;,&quot;fullScreen&quot;:&quot;true&quot;,&quot;download&quot;:&quot;true&quot;}}"><article class="wpr-grid-item elementor-clearfix post-30705 attachment type-attachment status-inherit hentry entry"><div class="wpr-grid-item-inner"><div class="wpr-grid-media-wrap wpr-effect-size-small "><div class="wpr-grid-image-wrap" data-src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_1.jpg"><img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_1.jpg" alt="FLUFFYBRICK_1" class="wpr-anim-timing-ease-default"></div><div class="wpr-grid-media-hover wpr-animation-wrap"><div class="wpr-grid-media-hover-bg  wpr-overlay-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency" data-url="https://presentcomposedesign.fr/fluffybrick_1/"></div><div class="wpr-cv-container"><div class="wpr-cv-outer"><div class="wpr-cv-inner"><div class="wpr-grid-media-hover-middle elementor-clearfix"><div class="wpr-grid-item-lightbox elementor-repeater-item-4d1548a wpr-grid-item-display-block wpr-grid-item-align-center wpr-element-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency"><div class="inner-block"><span data-src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_1.jpg"><i class="fas fa-search"></i></span><div class="wpr-grid-lightbox-overlay"></div></div></div></div></div></div></div></div></div></div></article><article class="wpr-grid-item elementor-clearfix post-30706 attachment type-attachment status-inherit hentry entry"><div class="wpr-grid-item-inner"><div class="wpr-grid-media-wrap wpr-effect-size-small "><div class="wpr-grid-image-wrap" data-src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_2.jpg"><img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_2.jpg" alt="FLUFFYBRICK_2" class="wpr-anim-timing-ease-default"></div><div class="wpr-grid-media-hover wpr-animation-wrap"><div class="wpr-grid-media-hover-bg  wpr-overlay-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency" data-url="https://presentcomposedesign.fr/fluffybrick_2/"></div><div class="wpr-cv-container"><div class="wpr-cv-outer"><div class="wpr-cv-inner"><div class="wpr-grid-media-hover-middle elementor-clearfix"><div class="wpr-grid-item-lightbox elementor-repeater-item-4d1548a wpr-grid-item-display-block wpr-grid-item-align-center wpr-element-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency"><div class="inner-block"><span data-src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_2.jpg"><i class="fas fa-search"></i></span><div class="wpr-grid-lightbox-overlay"></div></div></div></div></div></div></div></div></div></div></article><article class="wpr-grid-item elementor-clearfix post-30713 attachment type-attachment status-inherit hentry entry"><div class="wpr-grid-item-inner"><div class="wpr-grid-media-wrap wpr-effect-size-small "><div class="wpr-grid-image-wrap" data-src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_9-scaled.jpg"><img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_9-scaled.jpg" alt="FLUFFYBRICK_9" class="wpr-anim-timing-ease-default"></div><div class="wpr-grid-media-hover wpr-animation-wrap"><div class="wpr-grid-media-hover-bg  wpr-overlay-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency" data-url="https://presentcomposedesign.fr/fluffybrick-ar/fluffybrick_9/"></div><div class="wpr-cv-container"><div class="wpr-cv-outer"><div class="wpr-cv-inner"><div class="wpr-grid-media-hover-middle elementor-clearfix"><div class="wpr-grid-item-lightbox elementor-repeater-item-4d1548a wpr-grid-item-display-block wpr-grid-item-align-center wpr-element-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency"><div class="inner-block"><span data-src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_9-scaled.jpg"><i class="fas fa-search"></i></span><div class="wpr-grid-lightbox-overlay"></div></div></div></div></div></div></div></div></div></div></article><article class="wpr-grid-item elementor-clearfix post-30710 attachment type-attachment status-inherit hentry entry"><div class="wpr-grid-item-inner"><div class="wpr-grid-media-wrap wpr-effect-size-small "><div class="wpr-grid-image-wrap" data-src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_6.jpg"><img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_6.jpg" alt="FLUFFYBRICK_6" class="wpr-anim-timing-ease-default"></div><div class="wpr-grid-media-hover wpr-animation-wrap"><div class="wpr-grid-media-hover-bg  wpr-overlay-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency" data-url="https://presentcomposedesign.fr/fluffybrick_6/"></div><div class="wpr-cv-container"><div class="wpr-cv-outer"><div class="wpr-cv-inner"><div class="wpr-grid-media-hover-middle elementor-clearfix"><div class="wpr-grid-item-lightbox elementor-repeater-item-4d1548a wpr-grid-item-display-block wpr-grid-item-align-center wpr-element-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency"><div class="inner-block"><span data-src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_6.jpg"><i class="fas fa-search"></i></span><div class="wpr-grid-lightbox-overlay"></div></div></div></div></div></div></div></div></div></div></article><article class="wpr-grid-item elementor-clearfix post-30709 attachment type-attachment status-inherit hentry entry"><div class="wpr-grid-item-inner"><div class="wpr-grid-media-wrap wpr-effect-size-small "><div class="wpr-grid-image-wrap" data-src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_5.jpg"><img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_5.jpg" alt="FLUFFYBRICK_5" class="wpr-anim-timing-ease-default"></div><div class="wpr-grid-media-hover wpr-animation-wrap"><div class="wpr-grid-media-hover-bg  wpr-overlay-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency" data-url="https://presentcomposedesign.fr/fluffybrick_5/"></div><div class="wpr-cv-container"><div class="wpr-cv-outer"><div class="wpr-cv-inner"><div class="wpr-grid-media-hover-middle elementor-clearfix"><div class="wpr-grid-item-lightbox elementor-repeater-item-4d1548a wpr-grid-item-display-block wpr-grid-item-align-center wpr-element-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency"><div class="inner-block"><span data-src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_5.jpg"><i class="fas fa-search"></i></span><div class="wpr-grid-lightbox-overlay"></div></div></div></div></div></div></div></div></div></div></article><article class="wpr-grid-item elementor-clearfix post-30708 attachment type-attachment status-inherit hentry entry"><div class="wpr-grid-item-inner"><div class="wpr-grid-media-wrap wpr-effect-size-small "><div class="wpr-grid-image-wrap" data-src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_4.jpg"><img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_4.jpg" alt="FLUFFYBRICK_4" class="wpr-anim-timing-ease-default"></div><div class="wpr-grid-media-hover wpr-animation-wrap"><div class="wpr-grid-media-hover-bg  wpr-overlay-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency" data-url="https://presentcomposedesign.fr/fluffybrick_4/"></div><div class="wpr-cv-container"><div class="wpr-cv-outer"><div class="wpr-cv-inner"><div class="wpr-grid-media-hover-middle elementor-clearfix"><div class="wpr-grid-item-lightbox elementor-repeater-item-4d1548a wpr-grid-item-display-block wpr-grid-item-align-center wpr-element-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency"><div class="inner-block"><span data-src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_4.jpg"><i class="fas fa-search"></i></span><div class="wpr-grid-lightbox-overlay"></div></div></div></div></div></div></div></div></div></div></article><article class="wpr-grid-item elementor-clearfix post-30707 attachment type-attachment status-inherit hentry entry"><div class="wpr-grid-item-inner"><div class="wpr-grid-media-wrap wpr-effect-size-small "><div class="wpr-grid-image-wrap" data-src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_3.jpg"><img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_3.jpg" alt="FLUFFYBRICK_3" class="wpr-anim-timing-ease-default"></div><div class="wpr-grid-media-hover wpr-animation-wrap"><div class="wpr-grid-media-hover-bg  wpr-overlay-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency" data-url="https://presentcomposedesign.fr/fluffybrick_3/"></div><div class="wpr-cv-container"><div class="wpr-cv-outer"><div class="wpr-cv-inner"><div class="wpr-grid-media-hover-middle elementor-clearfix"><div class="wpr-grid-item-lightbox elementor-repeater-item-4d1548a wpr-grid-item-display-block wpr-grid-item-align-center wpr-element-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency"><div class="inner-block"><span data-src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/FLUFFYBRICK_3.jpg"><i class="fas fa-search"></i></span><div class="wpr-grid-lightbox-overlay"></div></div></div></div></div></div></div></div></div></div></article><article class="wpr-grid-item elementor-clearfix post-30717 attachment type-attachment status-inherit hentry entry"><div class="wpr-grid-item-inner"><div class="wpr-grid-media-wrap wpr-effect-size-small "><div class="wpr-grid-image-wrap" data-src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/Fluffybrick_AR_Riso-1.jpg"><img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/Fluffybrick_AR_Riso-1.jpg" alt="Fluffybrick_AR_Riso" class="wpr-anim-timing-ease-default"></div><div class="wpr-grid-media-hover wpr-animation-wrap"><div class="wpr-grid-media-hover-bg  wpr-overlay-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency" data-url="https://presentcomposedesign.fr/fluffybrick-ar/fluffybrick_ar_riso-2/"></div><div class="wpr-cv-container"><div class="wpr-cv-outer"><div class="wpr-cv-inner"><div class="wpr-grid-media-hover-middle elementor-clearfix"><div class="wpr-grid-item-lightbox elementor-repeater-item-4d1548a wpr-grid-item-display-block wpr-grid-item-align-center wpr-element-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency"><div class="inner-block"><span data-src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/Fluffybrick_AR_Riso-1.jpg"><i class="fas fa-search"></i></span><div class="wpr-grid-lightbox-overlay"></div></div></div></div></div></div></div></div></div></div></article></section>				</div>
				</div>
					</div>
				</div>
				</div>
					</div>
				</div>
					</div>
		</div>
					</div>
		</section>
		<div class="elementor-element elementor-element-9c20fa5 e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-parent" data-id="9c20fa5" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
		<div class="elementor-element elementor-element-1489b53 e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="1489b53" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-9b37b02 elementor-widget elementor-widget-video" data-id="9b37b02" data-element_type="widget" data-e-type="widget" data-settings="{&quot;video_type&quot;:&quot;hosted&quot;,&quot;autoplay&quot;:&quot;yes&quot;,&quot;play_on_mobile&quot;:&quot;yes&quot;,&quot;loop&quot;:&quot;yes&quot;,&quot;controls&quot;:&quot;yes&quot;}" data-widget_type="video.default">
				<div class="elementor-widget-container">
							<div class="e-hosted-video elementor-wrapper elementor-open-inline">
					<video class="elementor-video" src="https://presentcomposedesign.fr/wp-content/uploads/2024/12/Sequence-01.mp4" autoplay="" loop="" controls="" playsinline="" controlsList="nodownload"></video>
				</div>
						</div>
				</div>
				</div>
				</div>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/fluffybrick-ar/">Fluffybrick AR</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		<enclosure url="https://presentcomposedesign.fr/wp-content/uploads/2024/12/Sequence-01.mp4" length="730046" type="video/mp4" />

			</item>
		<item>
		<title>Spirit E-Boost</title>
		<link>https://presentcomposedesign.fr/spirit-e-boost/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Mon, 04 Mar 2024 13:05:59 +0000</pubDate>
				<category><![CDATA[[ia]]]></category>
		<category><![CDATA[Design produit]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=30015</guid>

					<description><![CDATA[<p>Spirit E-Boost<br />
Design prospectif d'un scooter électrique ultra-léger dans le cadre et la structure sont entièrement imprimées en 3D en métal selon une optimisation topologique permettant de n'utiliser que la matière nécessaire dans cet objectif.<br />
Projet en cours de développement... PrésentComposédesign-2024</p>
<p>Cet article <a href="https://presentcomposedesign.fr/spirit-e-boost/">Spirit E-Boost</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="30015" class="elementor elementor-30015">
						<section class="elementor-section elementor-top-section elementor-element elementor-element-ec38232 elementor-section-full_width elementor-section-height-min-height elementor-section-items-bottom elementor-section-content-middle elementor-section-stretched elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no" data-id="ec38232" data-element_type="section" data-e-type="section" data-settings="{&quot;stretch_section&quot;:&quot;section-stretched&quot;,&quot;background_background&quot;:&quot;classic&quot;}">
						<div class="elementor-container elementor-column-gap-extended">
					<div class="elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-f411c8d" data-id="f411c8d" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<section class="elementor-section elementor-inner-section elementor-element elementor-element-7765d2d elementor-section-boxed elementor-section-height-default elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no" data-id="7765d2d" data-element_type="section" data-e-type="section">
						<div class="elementor-container elementor-column-gap-default">
					<div class="elementor-column elementor-col-100 elementor-inner-column elementor-element elementor-element-9503862" data-id="9503862" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<div class="elementor-element elementor-element-b25684a elementor-widget elementor-widget-heading" data-id="b25684a" data-element_type="widget" data-e-type="widget" data-widget_type="heading.default">
				<div class="elementor-widget-container">
					<h1 class="elementor-heading-title elementor-size-default">Spirit e-BOOST</h1>				</div>
				</div>
					</div>
		</div>
					</div>
		</section>
					</div>
		</div>
					</div>
		</section>
		<div class="elementor-element elementor-element-e4df8a6 e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no elementor-invisible e-con e-parent" data-id="e4df8a6" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;fadeInUp&quot;}">
		<div class="elementor-element elementor-element-a1ee7e0 e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="a1ee7e0" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-0f810c9 elementor-widget__width-initial elementor-widget elementor-widget-text-editor" data-id="0f810c9" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h1>Design Génératif [ia]</h1><div data-vce-boxed-width="true"><div data-vce-do-apply="all el-ba3f564d"><div data-vce-element-content="true"><div data-vce-do-apply="border margin background el-11f4390c"><div data-vce-element-content="true" data-vce-do-apply="padding el-11f4390c"><div><span style="color: #0010ef;"> </span></div><h2>Spirit E-Boost</h2><h2><br />Design prospectif d&rsquo;un <span style="color: #0010ef;"><strong>scooter électrique ultra-léger</strong></span> dont le cadre et la <strong><span style="color: #0010ef;">structure</span></strong> sont entièrement <strong><span style="color: #0010ef;">imprimées en 3D en métal</span></strong> selon une <strong><span style="color: #0010ef;">optimisation topologique</span></strong> permettant de <strong><span style="color: #0010ef;">n&rsquo;utiliser que la matière nécessaire</span></strong> dans cet objectif.</h2><h2><br />Projet en cours de développement&#8230; PrésentComposédesign-2024</h2></div></div></div></div></div><div data-vce-boxed-width="true"><div data-vce-do-apply="all el-d9d9a9fd"><div data-vce-element-content="true"><div><div data-vce-do-apply="border margin background el-87440d41"><div data-vce-element-content="true" data-vce-do-apply="padding el-87440d41"><p><!-- /wp:vcwb/empty-comment-element-wrapper --></p></div></div></div><p><!-- /vcwb/dynamicElementComment:87440d41 --></p></div></div></div><p><!-- /vcwb/dynamicElementComment:d9d9a9fd --><!--vcv no format--></p>								</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-97fb5da e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="97fb5da" data-element_type="container" data-e-type="container">
		<div class="wpr-jarallax elementor-element elementor-element-52163d1 e-con-full wpr-jarallax-yes e-flex wpr-particle-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no elementor-invisible e-con e-child" speed-data="1.4" bg-image="https://presentcomposedesign.fr/wp-content/uploads/2024/03/ELECTRIC-Scooterv3_PresentComposedesign_2024_GIF2048.gif" bg-image-mobile="" scroll-effect="scale" data-id="52163d1" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;fadeInUp&quot;}">
				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-c783795 e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no elementor-invisible e-con e-parent" data-id="c783795" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;slideInRight&quot;}">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-27c1527 elementor-view-default elementor-widget elementor-widget-icon" data-id="27c1527" data-element_type="widget" data-e-type="widget" data-widget_type="icon.default">
				<div class="elementor-widget-container">
							<div class="elementor-icon-wrapper">
			<div class="elementor-icon">
			<svg xmlns="http://www.w3.org/2000/svg" id="Calque_1" data-name="Calque 1" viewBox="0 0 230 95"><defs><style>      .cls-1 {        fill: #fc6a00;      }      .cls-1, .cls-2, .cls-3 {        stroke-width: 0px;      }      .cls-2 {        fill: #0010ef;      }      .cls-3 {        fill: #fff;        opacity: .8;      }    </style></defs><rect class="cls-3" x="13.9" y="4" width="212.9" height="57.7" rx="4" ry="4"></rect><path class="cls-2" d="M222.8,62.2H17.9c-2.5,0-4.5-2-4.5-4.5V8c0-2.5,2-4.5,4.5-4.5h204.9c2.5,0,4.5,2,4.5,4.5v49.7c0,2.5-2,4.5-4.5,4.5ZM17.9,4.5c-1.9,0-3.5,1.6-3.5,3.5v49.7c0,1.9,1.6,3.5,3.5,3.5h204.9c1.9,0,3.5-1.6,3.5-3.5V8c0-1.9-1.6-3.5-3.5-3.5H17.9Z"></path><g><path class="cls-2" d="M43.8,17.6c-.6-.3-1.2-.5-2.1-.5h-4v10.6h1.9v-3.6h2c.8,0,1.5-.2,2.1-.5.6-.3,1-.7,1.3-1.2.3-.5.4-1.1.4-1.8s-.1-1.3-.4-1.8c-.3-.5-.7-1-1.3-1.3ZM43.3,21.7c-.1.3-.4.5-.7.7-.3.2-.7.2-1.2.2h-1.8v-3.8h1.8c.5,0,.9,0,1.2.2.3.2.5.4.7.7.1.3.2.6.2,1s0,.7-.2,1Z"></path><path class="cls-2" d="M51.9,20.2c-.6-.3-1.2-.5-2-.5s-1.5.2-2,.5c-.6.3-1,.8-1.3,1.4-.3.6-.5,1.3-.5,2.2s.2,1.5.5,2.2c.3.6.8,1.1,1.3,1.4.6.3,1.2.5,2,.5s1.5-.2,2-.5c.6-.3,1-.8,1.3-1.4.3-.6.5-1.3.5-2.2s-.2-1.5-.5-2.2c-.3-.6-.8-1.1-1.3-1.4ZM51.5,25.2c-.1.4-.4.7-.6.9s-.6.3-1.1.3-.8-.1-1.1-.3-.5-.5-.6-.9-.2-.8-.2-1.3,0-.9.2-1.3.4-.7.6-.9.6-.3,1.1-.3.8.1,1.1.3.5.5.6.9.2.8.2,1.3,0,.9-.2,1.3Z"></path><path class="cls-2" d="M57.3,27.9c.6,0,1.1-.1,1.5-.4.4-.3.7-.7.9-1.1h0v1.4h1.8v-8h-1.9v4.6c0,.4,0,.7-.2,1-.2.3-.4.5-.6.6-.3.1-.5.2-.8.2-.5,0-.8-.1-1.1-.4-.3-.3-.4-.7-.4-1.2v-4.8h-1.9v5.1c0,.6.1,1.2.3,1.6.2.4.5.8.9,1,.4.2.9.3,1.4.3Z"></path><path class="cls-2" d="M64.9,23.1c0-.3,0-.6.2-.9s.4-.5.6-.6c.3-.1.6-.2.9-.2s.3,0,.5,0c.2,0,.3,0,.4,0v-1.7c0,0-.2,0-.3,0-.1,0-.2,0-.3,0-.5,0-.9.1-1.2.4-.4.2-.6.6-.7,1.1h0v-1.3h-1.8v8h1.9v-4.7Z"></path><path class="cls-2" d="M72.5,27.3c.4.4,1,.6,1.7.6s.4,0,.6,0,.4,0,.6-.1l-.3-1.5c0,0-.2,0-.3,0,0,0-.2,0-.3,0-.3,0-.5,0-.7-.2-.1-.1-.2-.4-.2-.7v-4.1h1.6v-1.5h-1.6v-1.9h-1.9v1.9h-1.1v1.5h1.1v4.4c0,.7.2,1.2.6,1.6Z"></path><path class="cls-2" d="M77.7,27.4c.6.3,1.2.5,2,.5s1.5-.2,2-.5c.6-.3,1-.8,1.3-1.4.3-.6.5-1.3.5-2.2s-.2-1.5-.5-2.2c-.3-.6-.8-1.1-1.3-1.4-.6-.3-1.2-.5-2-.5s-1.5.2-2,.5c-.6.3-1,.8-1.3,1.4-.3.6-.5,1.3-.5,2.2s.2,1.5.5,2.2c.3.6.8,1.1,1.3,1.4ZM78,22.5c.1-.4.4-.7.6-.9s.6-.3,1.1-.3.8.1,1.1.3.5.5.6.9.2.8.2,1.3,0,.9-.2,1.3-.4.7-.6.9-.6.3-1.1.3-.8-.1-1.1-.3-.5-.5-.6-.9-.2-.8-.2-1.3,0-.9.2-1.3Z"></path><path class="cls-2" d="M91.6,27.8v-8h-1.9v4.6c0,.4,0,.7-.2,1-.2.3-.4.5-.6.6-.3.1-.5.2-.8.2-.5,0-.8-.1-1.1-.4-.3-.3-.4-.7-.4-1.2v-4.8h-1.9v5.1c0,.6.1,1.2.3,1.6.2.4.5.8.9,1,.4.2.9.3,1.4.3.6,0,1.1-.1,1.5-.4.4-.3.7-.7.9-1.1h0v1.4h1.8Z"></path><path class="cls-2" d="M93.5,25.7c0,.7.2,1.2.6,1.6.4.4,1,.6,1.7.6s.4,0,.6,0,.4,0,.6-.1l-.3-1.5c0,0-.2,0-.3,0,0,0-.2,0-.3,0-.3,0-.5,0-.7-.2-.1-.1-.2-.4-.2-.7v-4.1h1.6v-1.5h-1.6v-1.9h-1.9v1.9h-1.1v1.5h1.1v4.4Z"></path><path class="cls-2" d="M103,27.7c.5-.2.9-.5,1.2-.8s.5-.7.6-1.2l-1.8-.2c0,.2-.2.4-.4.6-.2.2-.3.3-.6.3-.2,0-.5.1-.7.1-.4,0-.8,0-1.1-.3-.3-.2-.5-.4-.7-.8-.2-.3-.2-.7-.2-1.1h5.5v-.6c0-.7,0-1.3-.3-1.8-.2-.5-.5-.9-.8-1.3-.3-.3-.7-.6-1.2-.7-.4-.2-.9-.2-1.4-.2-.8,0-1.4.2-2,.5-.6.3-1,.8-1.3,1.5-.3.6-.5,1.3-.5,2.2s.2,1.6.5,2.2c.3.6.8,1.1,1.3,1.4.6.3,1.3.5,2.1.5s1.2,0,1.6-.3ZM100.2,21.5c.3-.2.6-.3,1-.3s.7,0,1,.3c.3.2.5.5.6.8,0,.2.1.5.2.8h-3.7c0-.3,0-.6.2-.8.1-.3.4-.6.6-.8Z"></path><path class="cls-2" d="M112,26.7c.3-.4.4-.8.4-1.3s-.2-1-.6-1.4c-.4-.4-.9-.6-1.7-.8l-1.4-.3c-.4,0-.7-.2-.9-.4s-.3-.3-.3-.6.1-.5.4-.7c.3-.2.6-.3,1-.3s.8.1,1,.3.4.4.5.7l1.7-.2c-.1-.7-.5-1.2-1-1.6-.5-.4-1.3-.6-2.2-.6s-1.2,0-1.7.3c-.5.2-.9.5-1.1.8s-.4.8-.4,1.3.2,1.1.5,1.4c.4.4.9.6,1.7.8l1.4.3c.7.2,1.1.5,1.1.9s-.1.5-.4.7-.6.3-1.1.3-.8,0-1.1-.3c-.3-.2-.5-.5-.5-.8l-1.8.2c.1.7.5,1.3,1.1,1.7.6.4,1.4.6,2.3.6s1.3-.1,1.8-.3.9-.5,1.2-.9Z"></path><path class="cls-2" d="M117.7,27.5c.5.3,1.1.5,1.7.5s.8,0,1.1-.2c.3-.2.5-.3.7-.6.2-.2.3-.4.4-.6h.1v1.3h1.8v-10.6h-1.9v4h0c0-.2-.2-.4-.4-.6-.2-.2-.4-.4-.7-.6-.3-.2-.7-.2-1.2-.2s-1.2.2-1.7.5c-.5.3-.9.8-1.2,1.4-.3.6-.4,1.4-.4,2.2s.1,1.6.4,2.2.7,1.1,1.2,1.4ZM118.3,22.5c.1-.4.4-.7.6-.9s.6-.3,1-.3.7.1,1,.3.5.5.6.9c.1.4.2.8.2,1.4s0,1-.2,1.4c-.1.4-.4.7-.6.9-.3.2-.6.3-1,.3s-.8-.1-1-.3c-.3-.2-.5-.5-.6-.9s-.2-.8-.2-1.3,0-.9.2-1.3Z"></path><path class="cls-2" d="M130.2,27.7c.5-.2.9-.5,1.2-.8s.5-.7.6-1.2l-1.8-.2c0,.2-.2.4-.4.6-.2.2-.3.3-.6.3-.2,0-.5.1-.7.1-.4,0-.8,0-1.1-.3s-.5-.4-.7-.8c-.2-.3-.2-.7-.2-1.1h5.5v-.6c0-.7,0-1.3-.3-1.8-.2-.5-.5-.9-.8-1.3-.3-.3-.7-.6-1.2-.7-.4-.2-.9-.2-1.4-.2-.8,0-1.4.2-2,.5s-1,.8-1.3,1.5c-.3.6-.5,1.3-.5,2.2s.2,1.6.5,2.2.8,1.1,1.3,1.4c.6.3,1.3.5,2.1.5s1.2,0,1.6-.3ZM127.4,21.5c.3-.2.6-.3,1-.3s.7,0,1,.3c.3.2.5.5.6.8,0,.2.1.5.2.8h-3.7c0-.3,0-.6.2-.8.1-.3.4-.6.6-.8Z"></path><path class="cls-2" d="M142,21.7c.3.2.4.6.4,1.1v5.1h1.9v-5.3c0-.9-.2-1.6-.7-2-.5-.5-1.1-.7-1.8-.7s-1.1.1-1.5.4c-.4.3-.7.6-.9,1.1h0c-.1-.5-.4-.8-.8-1.1-.4-.3-.8-.4-1.4-.4s-1,.1-1.4.4-.7.6-.8,1.1h0v-1.4h-1.8v8h1.9v-4.8c0-.3,0-.6.2-.9.1-.2.3-.4.5-.6.2-.1.5-.2.7-.2.4,0,.7.1,1,.4.2.2.4.6.4,1v5.1h1.9v-5c0-.4.1-.8.4-1.1.3-.3.6-.4,1.1-.4s.7.1,1,.3Z"></path><path class="cls-2" d="M149.8,27.3c.2-.2.4-.4.5-.6h0v1.1h1.8v-5.3c0-.5,0-1-.3-1.3-.2-.4-.4-.6-.8-.8-.3-.2-.7-.4-1-.4-.4,0-.8-.1-1.1-.1-.6,0-1.1,0-1.5.2-.5.2-.8.4-1.2.7s-.5.7-.7,1.2l1.7.2c0-.3.3-.5.5-.7.3-.2.6-.3,1.1-.3s.8.1,1,.3c.2.2.3.5.3.9h0c0,.2,0,.3-.2.4-.1,0-.3.1-.6.2s-.7,0-1.1.1c-.4,0-.8.1-1.1.2s-.7.2-1,.4c-.3.2-.5.4-.7.7-.2.3-.2.7-.2,1.1s.1,1,.3,1.3c.2.4.6.6,1,.8.4.2.9.3,1.4.3s.8,0,1.1-.2c.3-.1.6-.3.8-.5ZM147.5,26.3c-.3-.2-.4-.4-.4-.8s0-.4.2-.6c.1-.2.3-.3.5-.4.2,0,.5-.2.8-.2.1,0,.3,0,.4,0,.2,0,.3,0,.5,0s.3,0,.5-.1c.1,0,.2,0,.3-.1v.9c0,.3,0,.6-.2.8-.2.2-.4.4-.6.6-.3.1-.6.2-1,.2s-.7,0-1-.3Z"></path><path class="cls-2" d="M159.3,20.1c-.4-.2-.9-.3-1.4-.3s-1.1.1-1.5.4c-.4.3-.7.6-.9,1.1h0v-1.4h-1.8v8h1.9v-4.7c0-.4,0-.7.2-1,.1-.3.3-.5.6-.6.3-.1.5-.2.9-.2.5,0,.9.2,1.1.5.3.3.4.7.4,1.3v4.8h1.9v-5.1c0-.6-.1-1.2-.3-1.6-.2-.4-.5-.8-1-1Z"></path><path class="cls-2" d="M166,27.7c.3-.2.5-.3.7-.6.2-.2.3-.4.4-.6h.1v1.3h1.8v-10.6h-1.9v4h0c0-.2-.2-.4-.4-.6-.2-.2-.4-.4-.7-.6-.3-.2-.7-.2-1.2-.2s-1.2.2-1.7.5c-.5.3-.9.8-1.2,1.4-.3.6-.4,1.4-.4,2.2s.1,1.6.4,2.2.7,1.1,1.2,1.4c.5.3,1.1.5,1.7.5s.8,0,1.1-.2ZM163.7,25.2c-.1-.4-.2-.8-.2-1.3s0-.9.2-1.3c.1-.4.4-.7.6-.9.3-.2.6-.3,1-.3s.7.1,1,.3c.3.2.5.5.6.9.1.4.2.8.2,1.4s0,1-.2,1.4c-.1.4-.4.7-.6.9-.3.2-.6.3-1,.3s-.8-.1-1-.3c-.3-.2-.5-.5-.6-.9Z"></path><path class="cls-2" d="M175.7,25.5c0,.2-.2.4-.4.6-.2.2-.3.3-.6.3-.2,0-.5.1-.7.1-.4,0-.8,0-1.1-.3s-.5-.4-.7-.8c-.2-.3-.2-.7-.2-1.1h5.5v-.6c0-.7,0-1.3-.3-1.8-.2-.5-.5-.9-.8-1.3-.3-.3-.7-.6-1.2-.7-.4-.2-.9-.2-1.4-.2-.8,0-1.4.2-2,.5s-1,.8-1.3,1.5c-.3.6-.5,1.3-.5,2.2s.2,1.6.5,2.2c.3.6.8,1.1,1.3,1.4.6.3,1.3.5,2.1.5s1.2,0,1.6-.3.9-.5,1.2-.8.5-.7.6-1.2l-1.8-.2ZM172.9,21.5c.3-.2.6-.3,1-.3s.7,0,1,.3c.3.2.5.5.6.8,0,.2.1.5.2.8h-3.7c0-.3,0-.6.2-.8.1-.3.4-.6.6-.8Z"></path><path class="cls-2" d="M182.8,23.3l-1.4-.3c-.4,0-.7-.2-.9-.4-.2-.1-.3-.3-.3-.6s.1-.5.4-.7c.3-.2.6-.3,1-.3s.8.1,1,.3.4.4.5.7l1.7-.2c-.1-.7-.5-1.2-1-1.6-.5-.4-1.3-.6-2.2-.6s-1.2,0-1.7.3c-.5.2-.9.5-1.1.8-.3.4-.4.8-.4,1.3s.2,1.1.5,1.4c.4.4.9.6,1.7.8l1.4.3c.7.2,1.1.5,1.1.9s-.1.5-.4.7c-.3.2-.6.3-1.1.3s-.8,0-1.1-.3-.5-.5-.5-.8l-1.8.2c.1.7.5,1.3,1.1,1.7s1.4.6,2.3.6,1.3-.1,1.8-.3.9-.5,1.2-.9c.3-.4.4-.8.4-1.3s-.2-1-.6-1.4c-.4-.4-.9-.6-1.7-.8Z"></path><path class="cls-2" d="M194.6,20.2c-.6-.3-1.2-.5-2-.5s-1.5.2-2,.5c-.6.3-1,.8-1.3,1.4-.3.6-.5,1.3-.5,2.2s.2,1.5.5,2.2c.3.6.8,1.1,1.3,1.4s1.2.5,2,.5,1.5-.2,2-.5,1-.8,1.3-1.4c.3-.6.5-1.3.5-2.2s-.2-1.5-.5-2.2c-.3-.6-.8-1.1-1.3-1.4ZM194.3,25.2c-.1.4-.4.7-.6.9-.3.2-.6.3-1.1.3s-.8-.1-1.1-.3-.5-.5-.6-.9c-.1-.4-.2-.8-.2-1.3s0-.9.2-1.3c.1-.4.4-.7.6-.9s.6-.3,1.1-.3.8.1,1.1.3c.3.2.5.5.6.9.1.4.2.8.2,1.3s0,.9-.2,1.3Z"></path><path class="cls-2" d="M198.7,27.6c.4.2.9.3,1.4.3.6,0,1.1-.1,1.5-.4.4-.3.7-.7.9-1.1h0v1.4h1.8v-8h-1.9v4.6c0,.4,0,.7-.2,1-.2.3-.4.5-.6.6-.3.1-.5.2-.8.2-.5,0-.8-.1-1.1-.4-.3-.3-.4-.7-.4-1.2v-4.8h-1.9v5.1c0,.6.1,1.2.3,1.6.2.4.5.8.9,1Z"></path><path class="cls-2" d="M61.4,37.7c-.5-.3-1-.5-1.7-.5s-.9,0-1.2.2c-.3.2-.5.3-.7.6-.2.2-.3.4-.4.6h-.1v-1.3h-1.8v10.9h1.9v-4.2h0c0,.2.2.4.4.6.2.2.4.4.7.6s.7.2,1.1.2,1.2-.2,1.7-.5c.5-.3.9-.8,1.2-1.4s.4-1.4.4-2.2-.1-1.6-.4-2.2c-.3-.6-.7-1.1-1.2-1.4ZM60.9,42.7c-.1.4-.4.7-.6.9-.3.2-.6.3-1,.3s-.7-.1-1-.3c-.3-.2-.5-.5-.6-.9-.1-.4-.2-.8-.2-1.4s0-1,.2-1.4c.1-.4.4-.7.6-.9s.6-.3,1-.3.8.1,1,.3c.3.2.5.5.6.9s.2.8.2,1.3,0,.9-.2,1.3Z"></path><path class="cls-2" d="M67.9,37.2c-.5,0-.9.1-1.2.4-.4.2-.6.6-.7,1.1h0v-1.3h-1.8v8h1.9v-4.7c0-.3,0-.6.2-.9s.4-.5.6-.6c.3-.1.6-.2.9-.2s.3,0,.5,0c.2,0,.3,0,.4,0v-1.7c0,0-.2,0-.3,0-.1,0-.2,0-.3,0Z"></path><path class="cls-2" d="M70.2,34.2c-.3,0-.6,0-.8.3-.2.2-.3.4-.3.7s.1.5.3.7.5.3.8.3.5,0,.8-.3.3-.4.3-.7-.1-.5-.3-.7c-.2-.2-.5-.3-.8-.3Z"></path><rect class="cls-2" x="69.3" y="37.4" width="1.9" height="8"></rect><path class="cls-2" d="M76.8,40.8l-1.4-.3c-.4,0-.7-.2-.9-.4s-.3-.3-.3-.6.1-.5.4-.7c.3-.2.6-.3,1-.3s.8.1,1,.3.4.4.5.7l1.7-.2c-.1-.7-.5-1.2-1-1.6-.5-.4-1.3-.6-2.2-.6s-1.2,0-1.7.3c-.5.2-.9.5-1.1.8s-.4.8-.4,1.3.2,1.1.5,1.4c.4.4.9.6,1.7.8l1.4.3c.7.2,1.1.5,1.1.9s-.1.5-.4.7-.6.3-1.1.3-.8,0-1.1-.3c-.3-.2-.5-.5-.5-.8l-1.8.2c.1.7.5,1.3,1.1,1.7.6.4,1.4.6,2.3.6s1.3-.1,1.8-.3.9-.5,1.2-.9c.3-.4.4-.8.4-1.3s-.2-1-.6-1.4c-.4-.4-.9-.6-1.7-.8Z"></path><path class="cls-2" d="M86,38.2c-.3-.3-.7-.6-1.2-.7-.4-.2-.9-.2-1.4-.2-.8,0-1.4.2-2,.5-.6.3-1,.8-1.3,1.5-.3.6-.5,1.3-.5,2.2s.2,1.6.5,2.2c.3.6.8,1.1,1.3,1.4.6.3,1.3.5,2.1.5s1.2,0,1.6-.3.9-.5,1.2-.8.5-.7.6-1.2l-1.8-.2c0,.2-.2.4-.4.6-.2.2-.3.3-.6.3-.2,0-.5.1-.7.1-.4,0-.8,0-1.1-.3-.3-.2-.5-.4-.7-.8-.2-.3-.2-.7-.2-1.1h5.5v-.6c0-.7,0-1.3-.3-1.8-.2-.5-.5-.9-.8-1.3ZM82.4,39c.3-.2.6-.3,1-.3s.7,0,1,.3c.3.2.5.5.6.8,0,.2.1.5.2.8h-3.7c0-.3,0-.6.2-.8.1-.3.4-.6.6-.8Z"></path><path class="cls-2" d="M96.5,38.7h0c0-.2-.2-.4-.4-.6-.2-.2-.4-.4-.7-.6-.3-.2-.7-.2-1.2-.2s-1.2.2-1.7.5-.9.8-1.2,1.4c-.3.6-.4,1.4-.4,2.2s.1,1.6.4,2.2.7,1.1,1.2,1.4c.5.3,1.1.5,1.7.5s.8,0,1.1-.2c.3-.2.5-.3.7-.6.2-.2.3-.4.4-.6h.1v1.3h1.8v-10.6h-1.9v4ZM96.3,42.7c-.1.4-.4.7-.6.9-.3.2-.6.3-1,.3s-.8-.1-1-.3c-.3-.2-.5-.5-.6-.9s-.2-.8-.2-1.3,0-.9.2-1.3.4-.7.6-.9.6-.3,1-.3.7.1,1,.3c.3.2.5.5.6.9s.2.8.2,1.4,0,1-.2,1.4Z"></path><path class="cls-2" d="M105.7,38.2c-.3-.3-.7-.6-1.2-.7-.4-.2-.9-.2-1.4-.2-.8,0-1.4.2-2,.5-.6.3-1,.8-1.3,1.5-.3.6-.5,1.3-.5,2.2s.2,1.6.5,2.2c.3.6.8,1.1,1.3,1.4.6.3,1.3.5,2.1.5s1.2,0,1.6-.3.9-.5,1.2-.8.5-.7.6-1.2l-1.8-.2c0,.2-.2.4-.4.6-.2.2-.3.3-.6.3-.2,0-.5.1-.7.1-.4,0-.8,0-1.1-.3-.3-.2-.5-.4-.7-.8-.2-.3-.2-.7-.2-1.1h5.5v-.6c0-.7,0-1.3-.3-1.8-.2-.5-.5-.9-.8-1.3ZM102.1,39c.3-.2.6-.3,1-.3s.7,0,1,.3c.3.2.5.5.6.8,0,.2.1.5.2.8h-3.7c0-.3,0-.6.2-.8.1-.3.4-.6.6-.8Z"></path><path class="cls-2" d="M113.4,39.1c.3-.2.6-.3,1-.3s.6,0,.8.2c.2.1.4.3.5.5.1.2.2.4.3.7h1.8c0-.6-.2-1.1-.5-1.5-.3-.4-.7-.8-1.2-1-.5-.2-1.1-.4-1.7-.4s-1.4.2-2,.5c-.6.3-1,.8-1.3,1.5-.3.6-.5,1.3-.5,2.1s.2,1.5.5,2.1c.3.6.7,1.1,1.3,1.5s1.3.5,2,.5,1.2-.1,1.7-.4c.5-.2.9-.6,1.2-1,.3-.4.5-.9.5-1.5h-1.8c0,.4-.3.8-.6,1s-.6.4-1.1.4-.7-.1-1-.3c-.3-.2-.5-.5-.7-.9-.2-.4-.2-.9-.2-1.4s0-1,.2-1.4.4-.7.7-.9Z"></path><path class="cls-2" d="M124.3,37.8c-.6-.3-1.2-.5-2-.5s-1.5.2-2,.5c-.6.3-1,.8-1.3,1.4-.3.6-.5,1.3-.5,2.2s.2,1.5.5,2.2c.3.6.8,1.1,1.3,1.4.6.3,1.2.5,2,.5s1.5-.2,2-.5c.6-.3,1-.8,1.3-1.4.3-.6.5-1.3.5-2.2s-.2-1.5-.5-2.2c-.3-.6-.8-1.1-1.3-1.4ZM124,42.7c-.1.4-.4.7-.6.9s-.6.3-1.1.3-.8-.1-1.1-.3-.5-.5-.6-.9c-.1-.4-.2-.8-.2-1.3s0-.9.2-1.3c.1-.4.4-.7.6-.9s.6-.3,1.1-.3.8.1,1.1.3.5.5.6.9c.1.4.2.8.2,1.3s0,.9-.2,1.3Z"></path><path class="cls-2" d="M132.8,37.6c-.4-.2-.9-.3-1.4-.3s-1.1.1-1.5.4c-.4.3-.7.6-.9,1.1h0v-1.4h-1.8v8h1.9v-4.7c0-.4,0-.7.2-1,.1-.3.3-.5.6-.6.3-.1.5-.2.9-.2.5,0,.9.2,1.1.5.3.3.4.7.4,1.3v4.8h1.9v-5.1c0-.6-.1-1.2-.3-1.6-.2-.4-.5-.8-1-1Z"></path><path class="cls-2" d="M139,43.8c0,0-.2,0-.3,0-.3,0-.5,0-.7-.2s-.2-.4-.2-.7v-4.1h1.6v-1.5h-1.6v-1.9h-1.9v1.9h-1.1v1.5h1.1v4.4c0,.7.2,1.2.6,1.6.4.4,1,.6,1.7.6s.4,0,.6,0c.2,0,.4,0,.6-.1l-.3-1.5c0,0-.2,0-.3,0Z"></path><path class="cls-2" d="M145.9,37.8c-.3-.2-.7-.4-1-.4-.4,0-.8-.1-1.1-.1-.6,0-1.1,0-1.5.2-.5.2-.8.4-1.2.7s-.5.7-.7,1.2l1.7.2c0-.3.3-.5.5-.7.3-.2.6-.3,1.1-.3s.8.1,1,.3c.2.2.3.5.3.9h0c0,.2,0,.3-.2.4-.1,0-.3.1-.6.2s-.7,0-1.1.1c-.4,0-.8.1-1.1.2s-.7.2-1,.4c-.3.2-.5.4-.7.7-.2.3-.2.7-.2,1.1s.1,1,.3,1.3c.2.4.6.6,1,.8.4.2.9.3,1.4.3s.8,0,1.1-.2c.3-.1.6-.3.8-.5s.4-.4.5-.6h0v1.1h1.8v-5.3c0-.5,0-1-.3-1.3-.2-.4-.4-.6-.8-.8ZM145.1,42.5c0,.3,0,.6-.2.8-.2.2-.4.4-.6.6-.3.1-.6.2-1,.2s-.7,0-1-.3-.4-.4-.4-.8,0-.4.2-.6c.1-.2.3-.3.5-.4.2,0,.5-.2.8-.2.1,0,.3,0,.4,0,.2,0,.3,0,.5,0s.3,0,.5-.1c.1,0,.2,0,.3-.1v.9Z"></path><path class="cls-2" d="M150.9,39.1c.3-.2.6-.3,1-.3s.6,0,.8.2c.2.1.4.3.5.5.1.2.2.4.3.7h1.8c0-.6-.2-1.1-.5-1.5-.3-.4-.7-.8-1.2-1-.5-.2-1.1-.4-1.7-.4s-1.4.2-2,.5c-.6.3-1,.8-1.3,1.5-.3.6-.5,1.3-.5,2.1s.2,1.5.5,2.1c.3.6.7,1.1,1.3,1.5s1.3.5,2,.5,1.2-.1,1.7-.4c.5-.2.9-.6,1.2-1,.3-.4.5-.9.5-1.5h-1.8c0,.4-.3.8-.6,1s-.6.4-1.1.4-.7-.1-1-.3c-.3-.2-.5-.5-.7-.9-.2-.4-.2-.9-.2-1.4s0-1,.2-1.4c.2-.4.4-.7.7-.9Z"></path><path class="cls-2" d="M159.8,43.8c0,0-.2,0-.3,0-.3,0-.5,0-.7-.2s-.2-.4-.2-.7v-4.1h1.6v-1.5h-1.6v-1.9h-1.9v1.9h-1.1v1.5h1.1v4.4c0,.7.2,1.2.6,1.6.4.4,1,.6,1.7.6s.4,0,.6,0c.2,0,.4,0,.6-.1l-.3-1.5c0,0-.2,0-.3,0Z"></path><path class="cls-2" d="M168.2,37.2c-.5,0-.9.1-1.2.4-.4.2-.6.6-.7,1.1h0v-1.3h-1.8v8h1.9v-4.7c0-.3,0-.6.2-.9s.4-.5.6-.6c.3-.1.6-.2.9-.2s.3,0,.5,0c.2,0,.3,0,.4,0v-1.7c0,0-.2,0-.3,0-.1,0-.2,0-.3,0Z"></path><path class="cls-2" d="M174.9,37.8c-.3-.2-.7-.4-1-.4-.4,0-.8-.1-1.1-.1-.6,0-1.1,0-1.5.2-.5.2-.8.4-1.2.7s-.5.7-.7,1.2l1.7.2c0-.3.3-.5.5-.7.3-.2.6-.3,1.1-.3s.8.1,1,.3c.2.2.3.5.3.9h0c0,.2,0,.3-.2.4-.1,0-.3.1-.6.2s-.7,0-1.1.1c-.4,0-.8.1-1.1.2s-.7.2-1,.4c-.3.2-.5.4-.7.7-.2.3-.2.7-.2,1.1s.1,1,.3,1.3c.2.4.6.6,1,.8.4.2.9.3,1.4.3s.8,0,1.1-.2c.3-.1.6-.3.8-.5s.4-.4.5-.6h0v1.1h1.8v-5.3c0-.5,0-1-.3-1.3-.2-.4-.4-.6-.8-.8ZM174.1,42.5c0,.3,0,.6-.2.8-.2.2-.4.4-.6.6-.3.1-.6.2-1,.2s-.7,0-1-.3-.4-.4-.4-.8,0-.4.2-.6c.1-.2.3-.3.5-.4.2,0,.5-.2.8-.2.1,0,.3,0,.4,0,.2,0,.3,0,.5,0s.3,0,.5-.1c.1,0,.2,0,.3-.1v.9Z"></path><path class="cls-2" d="M183.2,37.7c-.5-.3-1-.5-1.7-.5s-.9,0-1.2.2c-.3.2-.5.3-.7.6-.2.2-.3.4-.4.6h-.1v-1.3h-1.8v10.9h1.9v-4.2h0c0,.2.2.4.4.6.2.2.4.4.7.6.3.2.7.2,1.1.2s1.2-.2,1.7-.5c.5-.3.9-.8,1.2-1.4.3-.6.4-1.4.4-2.2s-.1-1.6-.4-2.2c-.3-.6-.7-1.1-1.2-1.4ZM182.7,42.7c-.1.4-.4.7-.6.9-.3.2-.6.3-1,.3s-.7-.1-1-.3c-.3-.2-.5-.5-.6-.9-.1-.4-.2-.8-.2-1.4s0-1,.2-1.4c.1-.4.4-.7.6-.9.3-.2.6-.3,1-.3s.8.1,1,.3c.3.2.5.5.6.9s.2.8.2,1.3,0,.9-.2,1.3Z"></path><path class="cls-2" d="M186.8,34.2c-.3,0-.6,0-.8.3-.2.2-.3.4-.3.7s.1.5.3.7.5.3.8.3.5,0,.8-.3.3-.4.3-.7-.1-.5-.3-.7c-.2-.2-.5-.3-.8-.3Z"></path><rect class="cls-2" x="185.9" y="37.4" width="1.9" height="8"></rect><path class="cls-2" d="M194.4,38.7h0c0-.2-.2-.4-.4-.6-.2-.2-.4-.4-.7-.6-.3-.2-.7-.2-1.2-.2s-1.2.2-1.7.5c-.5.3-.9.8-1.2,1.4-.3.6-.4,1.4-.4,2.2s.1,1.6.4,2.2.7,1.1,1.2,1.4c.5.3,1.1.5,1.7.5s.8,0,1.1-.2c.3-.2.5-.3.7-.6.2-.2.3-.4.4-.6h.1v1.3h1.8v-10.6h-1.9v4ZM194.3,42.7c-.1.4-.4.7-.6.9-.3.2-.6.3-1,.3s-.8-.1-1-.3c-.3-.2-.5-.5-.6-.9s-.2-.8-.2-1.3,0-.9.2-1.3c.1-.4.4-.7.6-.9.3-.2.6-.3,1-.3s.7.1,1,.3c.3.2.5.5.6.9.1.4.2.8.2,1.4s0,1-.2,1.4Z"></path><path class="cls-2" d="M204.8,41.3c0-.7,0-1.3-.3-1.8-.2-.5-.5-.9-.8-1.3-.3-.3-.7-.6-1.2-.7-.4-.2-.9-.2-1.4-.2-.8,0-1.4.2-2,.5s-1,.8-1.3,1.5c-.3.6-.5,1.3-.5,2.2s.2,1.6.5,2.2c.3.6.8,1.1,1.3,1.4.6.3,1.3.5,2.1.5s1.2,0,1.6-.3.9-.5,1.2-.8.5-.7.6-1.2l-1.8-.2c0,.2-.2.4-.4.6-.2.2-.3.3-.6.3-.2,0-.5.1-.7.1-.4,0-.8,0-1.1-.3s-.5-.4-.7-.8c-.2-.3-.2-.7-.2-1.1h5.5v-.6ZM200.1,39c.3-.2.6-.3,1-.3s.7,0,1,.3c.3.2.5.5.6.8,0,.2.1.5.2.8h-3.7c0-.3,0-.6.2-.8.1-.3.4-.6.6-.8Z"></path></g><path class="cls-1" d="M41.4,62.3S-10.4,45.2,28.6,1.1c0,0-64.5,42.1,10.1,74.6l-8.9,14.8,42.1-14.9-32.5-30.1,2,16.7Z"></path><path class="cls-2" d="M72.4,75.2c-3.6-3.4-7.3-6.7-10.9-10.1-5.9-5.4-11.7-10.8-17.6-16.2-1.3-1.2-2.7-2.5-4-3.7-.4-.4-1.3,0-1.3.6.6,5.2,1.3,10.4,1.9,15.6-4.9-1.8-9.6-4.5-13.6-7.7-3.8-3-7.1-6.6-9.3-10.9-2.6-5-3.3-10.7-2.3-16.3,1.7-9.5,7.7-17.9,14-25,.6-.7-.7-.9-1.1-.6-2.1,1.4-4.1,2.9-6,4.6-4.3,3.6-8.3,7.5-11.7,12-3.8,5-7,10.7-8.1,17-1.2,7,.6,13.9,4.6,19.6,6.3,9.1,16.4,15.2,26.2,19.9,1.5.7,3.1,1.5,4.7,2.2-2.9,4.8-5.8,9.6-8.7,14.4-.3.5.5.7.8.6,4.7-1.7,9.5-3.4,14.2-5l22.6-8c1.7-.6,3.5-1.2,5.2-1.9.4-.1.8-.6.4-1ZM57.5,80.1l-22.6,8c-1.2.4-2.5.9-3.7,1.3,2.7-4.6,5.5-9.1,8.2-13.7.2-.2,0-.5-.3-.6C23.6,68.4,1.4,56.1,3.6,36c1.3-11.8,9.9-21.7,18.6-29.1.9-.8,1.9-1.6,2.8-2.3-8.2,10.3-15.1,23.6-9.8,36.6,3.6,8.7,11.4,14.8,19.6,19,1.9,1,3.9,1.9,6,2.6.3.3,1.1,0,1.3-.4,0,0,0-.2,0-.3-.6-4.9-1.2-9.8-1.8-14.7,3.1,2.9,6.3,5.8,9.4,8.7,5.9,5.4,11.7,10.8,17.6,16.2,1.1,1,2.3,2.1,3.4,3.1-4.4,1.6-8.8,3.1-13.2,4.7Z"></path></svg>			</div>
		</div>
						</div>
				</div>
					</div>
				</div>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/spirit-e-boost/">Spirit E-Boost</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>[ia]RTWORKS</title>
		<link>https://presentcomposedesign.fr/iartworks/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Fri, 09 Dec 2022 11:27:00 +0000</pubDate>
				<category><![CDATA[[ia]]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=29948</guid>

					<description><![CDATA[<p>iaRTWORKS  est la page sur laquelle j'ai choisi de regrouper différentes créations personnelles obtenues en utilisant des outils d'intelligence artificielle générative entre autres logiciels, retouches &#038; modifications dans leur processus de réalisation.<br />
PrésentComposédesign-2022 </p>
<p>Cet article <a href="https://presentcomposedesign.fr/iartworks/">[ia]RTWORKS</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></description>
										<content:encoded><![CDATA[		<div data-elementor-type="wp-post" data-elementor-id="29948" class="elementor elementor-29948">
						<section class="elementor-section elementor-top-section elementor-element elementor-element-0f5079d elementor-section-full_width elementor-section-height-min-height elementor-section-items-bottom elementor-section-content-middle elementor-section-stretched elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no" data-id="0f5079d" data-element_type="section" data-e-type="section" data-settings="{&quot;stretch_section&quot;:&quot;section-stretched&quot;,&quot;background_background&quot;:&quot;classic&quot;}">
						<div class="elementor-container elementor-column-gap-extended">
					<div class="elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-53020c6" data-id="53020c6" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<section class="elementor-section elementor-inner-section elementor-element elementor-element-5247421 elementor-section-boxed elementor-section-height-default elementor-section-height-default wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no" data-id="5247421" data-element_type="section" data-e-type="section">
						<div class="elementor-container elementor-column-gap-default">
					<div class="elementor-column elementor-col-100 elementor-inner-column elementor-element elementor-element-df621bb" data-id="df621bb" data-element_type="column" data-e-type="column">
			<div class="elementor-widget-wrap elementor-element-populated">
						<div class="elementor-element elementor-element-62da91d elementor-widget elementor-widget-heading" data-id="62da91d" data-element_type="widget" data-e-type="widget" data-widget_type="heading.default">
				<div class="elementor-widget-container">
					<h1 class="elementor-heading-title elementor-size-default">[ia]RTWORKS</h1>				</div>
				</div>
					</div>
		</div>
					</div>
		</section>
					</div>
		</div>
					</div>
		</section>
		<div class="elementor-element elementor-element-15577e8 e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no elementor-invisible e-con e-parent" data-id="15577e8" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;fadeInUp&quot;}">
					<div class="e-con-inner">
		<div class="elementor-element elementor-element-dd28605 e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="dd28605" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-2147181 elementor-widget__width-initial elementor-widget elementor-widget-text-editor" data-id="2147181" data-element_type="widget" data-e-type="widget" data-widget_type="text-editor.default">
				<div class="elementor-widget-container">
									<h1>Design Génératif [ia]</h1><div data-vce-boxed-width="true"><div data-vce-do-apply="all el-ba3f564d"><div data-vce-element-content="true"><div data-vce-do-apply="border margin background el-11f4390c"><div data-vce-element-content="true" data-vce-do-apply="padding el-11f4390c"><div><span style="color: #0010ef;"> </span></div><h2>iaRTWORKS est la page sur laquelle j&rsquo;ai choisi de regrouper différentes <strong><span style="color: #0010ef;">créations  artistiques</span></strong> personnelles <strong><span style="color: #0010ef;">obtenues en utilisant des outils d&rsquo;intelligence artificielle générative</span></strong> entre autres logiciels, retouches &amp; modifications <strong><span style="color: #0010ef;">dans leur processus de réalisation</span></strong>. <br />PrésentComposédesign-2022</h2><h2 style="text-align: center;"><span style="color: #0010ef;"><strong><i class="fa-solid fa-chevron-down"></i> </strong></span></h2><h2 style="text-align: center;"><span style="color: #0010ef;"><strong>Vous êtes intéressé par l&rsquo;acquisition d&rsquo;une de ces œuvres ? </strong></span></h2></div></div></div></div></div><div data-vce-boxed-width="true"><div data-vce-do-apply="all el-d9d9a9fd"><div data-vce-element-content="true"><div><div data-vce-do-apply="border margin background el-87440d41"><div data-vce-element-content="true" data-vce-do-apply="padding el-87440d41"><p><!-- /wp:vcwb/empty-comment-element-wrapper --></p></div></div></div><p><!-- /vcwb/dynamicElementComment:87440d41 --></p></div></div></div><p><!-- /vcwb/dynamicElementComment:d9d9a9fd --><!--vcv no format--></p>								</div>
				</div>
				<div class="elementor-element elementor-element-d4f50e5 elementor-align-center elementor-widget elementor-widget-button" data-id="d4f50e5" data-element_type="widget" data-e-type="widget" data-widget_type="button.default">
				<div class="elementor-widget-container">
									<div class="elementor-button-wrapper">
					<a class="elementor-button elementor-button-link elementor-size-sm" href="https://presentcomposedesign.fr/contact/">
						<span class="elementor-button-content-wrapper">
									<span class="elementor-button-text">Cliquez ici</span>
					</span>
					</a>
				</div>
								</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-82f52ee e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no e-con e-child" data-id="82f52ee" data-element_type="container" data-e-type="container">
		<div class="wpr-jarallax elementor-element elementor-element-a08143e e-con-full wpr-jarallax-yes e-flex wpr-particle-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no elementor-invisible e-con e-child" speed-data="1.4" bg-image="https://presentcomposedesign.fr/wp-content/uploads/2022/12/iridescent_iaRTWORKS_PresentComposedesign_2022-scaled.jpg" bg-image-mobile="" scroll-effect="scroll" data-id="a08143e" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;fadeInUp&quot;}">
				</div>
				</div>
					</div>
				</div>
		<div class="elementor-element elementor-element-a74a101 e-con-full e-flex wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no elementor-invisible e-con e-parent" data-id="a74a101" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;slideInUp&quot;}">
				<div class="elementor-element elementor-element-20c7748 wpr-grid-columns-2 wpr-grid-columns--tablet3 wpr-grid-columns--mobile1 wpr-grid-pagination-center elementor-widget elementor-widget-wpr-media-grid" data-id="20c7748" data-element_type="widget" data-e-type="widget" data-widget_type="wpr-media-grid.default">
				<div class="elementor-widget-container">
					<section class="wpr-grid wpr-media-grid elementor-clearfix" data-settings="{&quot;layout&quot;:&quot;fitRows&quot;,&quot;columns_desktop&quot;:&quot;2&quot;,&quot;gutter_hr&quot;:10,&quot;gutter_hr_mobile&quot;:10,&quot;gutter_hr_mobile_extra&quot;:10,&quot;gutter_hr_tablet&quot;:10,&quot;gutter_hr_tablet_extra&quot;:10,&quot;gutter_hr_laptop&quot;:10,&quot;gutter_hr_widescreen&quot;:10,&quot;gutter_vr&quot;:10,&quot;gutter_vr_mobile&quot;:10,&quot;gutter_vr_mobile_extra&quot;:10,&quot;gutter_vr_tablet&quot;:10,&quot;gutter_vr_tablet_extra&quot;:10,&quot;gutter_vr_laptop&quot;:10,&quot;gutter_vr_widescreen&quot;:10,&quot;animation&quot;:&quot;default&quot;,&quot;animation_duration&quot;:0.3,&quot;animation_delay&quot;:0.1,&quot;deeplinking&quot;:&quot;&quot;,&quot;filters_default_filter&quot;:&quot;&quot;,&quot;filters_linkable&quot;:&quot;no&quot;,&quot;filters_count&quot;:&quot;&quot;,&quot;filters_hide_empty&quot;:&quot;no&quot;,&quot;filters_animation&quot;:&quot;default&quot;,&quot;filters_animation_duration&quot;:0.3,&quot;filters_animation_delay&quot;:0.1,&quot;pagination_type&quot;:&quot;load-more&quot;,&quot;pagination_max_pages&quot;:1,&quot;lightbox&quot;:{&quot;selector&quot;:&quot;.wpr-grid-image-wrap&quot;,&quot;iframeMaxWidth&quot;:&quot;60%&quot;,&quot;hash&quot;:false,&quot;autoplay&quot;:&quot;true&quot;,&quot;pause&quot;:5000,&quot;progressBar&quot;:&quot;true&quot;,&quot;counter&quot;:&quot;true&quot;,&quot;controls&quot;:&quot;true&quot;,&quot;getCaptionFromTitleOrAlt&quot;:&quot;true&quot;,&quot;thumbnail&quot;:&quot;&quot;,&quot;showThumbByDefault&quot;:&quot;&quot;,&quot;share&quot;:&quot;&quot;,&quot;zoom&quot;:&quot;true&quot;,&quot;fullScreen&quot;:&quot;true&quot;,&quot;download&quot;:&quot;true&quot;}}"><article class="wpr-grid-item elementor-clearfix post-29980 attachment type-attachment status-inherit hentry entry"><div class="wpr-grid-item-inner"><div class="wpr-grid-media-wrap wpr-effect-size-small "><div class="wpr-grid-image-wrap" data-src="https://presentcomposedesign.fr/wp-content/uploads/2022/12/SPACE-DOME_PresentComposedesign_2022_1920.jpg"><img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/2022/12/SPACE-DOME_PresentComposedesign_2022_1920.jpg" alt="iaRTWORKS est la page sur laquelle j&#039;ai choisi de regrouper différentes créations personnelles obtenues en utilisant des outils d&#039;intelligence artificielle générative entre autres logiciels, retouches &amp; modifications dans leur processus de réalisation. PrésentComposédesign-2022" class="wpr-anim-timing-ease-default"></div><div class="wpr-grid-media-hover wpr-animation-wrap"><div class="wpr-grid-media-hover-bg  wpr-overlay-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency" data-url="https://presentcomposedesign.fr/iartworks/space-dome_presentcomposedesign_2022_1920/"></div><div class="wpr-cv-container"><div class="wpr-cv-outer"><div class="wpr-cv-inner"><div class="wpr-grid-media-hover-middle elementor-clearfix"><div class="wpr-grid-item-lightbox elementor-repeater-item-1433fcf wpr-grid-item-display-block wpr-grid-item-align-center wpr-element-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency"><div class="inner-block"><span data-src="https://presentcomposedesign.fr/wp-content/uploads/2022/12/SPACE-DOME_PresentComposedesign_2022_1920.jpg"><i class="fas fa-search"></i></span><div class="wpr-grid-lightbox-overlay"></div></div></div></div></div></div></div></div></div></div></article><article class="wpr-grid-item elementor-clearfix post-29981 attachment type-attachment status-inherit hentry entry"><div class="wpr-grid-item-inner"><div class="wpr-grid-media-wrap wpr-effect-size-small "><div class="wpr-grid-image-wrap" data-src="https://presentcomposedesign.fr/wp-content/uploads/2022/12/SPACE-FARM_PresentComposedesign_2022_1920.jpg"><img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/2022/12/SPACE-FARM_PresentComposedesign_2022_1920.jpg" alt="iaRTWORKS est la page sur laquelle j&#039;ai choisi de regrouper différentes créations personnelles obtenues en utilisant des outils d&#039;intelligence artificielle générative entre autres logiciels, retouches &amp; modifications dans leur processus de réalisation. PrésentComposédesign-2022" class="wpr-anim-timing-ease-default"></div><div class="wpr-grid-media-hover wpr-animation-wrap"><div class="wpr-grid-media-hover-bg  wpr-overlay-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency" data-url="https://presentcomposedesign.fr/iartworks/space-farm_presentcomposedesign_2022_1920/"></div><div class="wpr-cv-container"><div class="wpr-cv-outer"><div class="wpr-cv-inner"><div class="wpr-grid-media-hover-middle elementor-clearfix"><div class="wpr-grid-item-lightbox elementor-repeater-item-1433fcf wpr-grid-item-display-block wpr-grid-item-align-center wpr-element-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency"><div class="inner-block"><span data-src="https://presentcomposedesign.fr/wp-content/uploads/2022/12/SPACE-FARM_PresentComposedesign_2022_1920.jpg"><i class="fas fa-search"></i></span><div class="wpr-grid-lightbox-overlay"></div></div></div></div></div></div></div></div></div></div></article><article class="wpr-grid-item elementor-clearfix post-29978 attachment type-attachment status-inherit hentry entry"><div class="wpr-grid-item-inner"><div class="wpr-grid-media-wrap wpr-effect-size-small "><div class="wpr-grid-image-wrap" data-src="https://presentcomposedesign.fr/wp-content/uploads/2022/12/FLUFFY-BRICK_PresentComposedesign_2022_1920.jpg"><img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/2022/12/FLUFFY-BRICK_PresentComposedesign_2022_1920.jpg" alt="iaRTWORKS est la page sur laquelle j&#039;ai choisi de regrouper différentes créations personnelles obtenues en utilisant des outils d&#039;intelligence artificielle générative entre autres logiciels, retouches &amp; modifications dans leur processus de réalisation. PrésentComposédesign-2022" class="wpr-anim-timing-ease-default"></div><div class="wpr-grid-media-hover wpr-animation-wrap"><div class="wpr-grid-media-hover-bg  wpr-overlay-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency" data-url="https://presentcomposedesign.fr/iartworks/fluffy-brick_presentcomposedesign_2022_1920/"></div><div class="wpr-cv-container"><div class="wpr-cv-outer"><div class="wpr-cv-inner"><div class="wpr-grid-media-hover-middle elementor-clearfix"><div class="wpr-grid-item-lightbox elementor-repeater-item-1433fcf wpr-grid-item-display-block wpr-grid-item-align-center wpr-element-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency"><div class="inner-block"><span data-src="https://presentcomposedesign.fr/wp-content/uploads/2022/12/FLUFFY-BRICK_PresentComposedesign_2022_1920.jpg"><i class="fas fa-search"></i></span><div class="wpr-grid-lightbox-overlay"></div></div></div></div></div></div></div></div></div></div></article><article class="wpr-grid-item elementor-clearfix post-29979 attachment type-attachment status-inherit hentry entry"><div class="wpr-grid-item-inner"><div class="wpr-grid-media-wrap wpr-effect-size-small "><div class="wpr-grid-image-wrap" data-src="https://presentcomposedesign.fr/wp-content/uploads/2022/12/FLUFFY-HOME_PresentComposedesign_2022_1920.jpg"><img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/2022/12/FLUFFY-HOME_PresentComposedesign_2022_1920.jpg" alt="iaRTWORKS est la page sur laquelle j&#039;ai choisi de regrouper différentes créations personnelles obtenues en utilisant des outils d&#039;intelligence artificielle générative entre autres logiciels, retouches &amp; modifications dans leur processus de réalisation. PrésentComposédesign-2022" class="wpr-anim-timing-ease-default"></div><div class="wpr-grid-media-hover wpr-animation-wrap"><div class="wpr-grid-media-hover-bg  wpr-overlay-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency" data-url="https://presentcomposedesign.fr/iartworks/fluffy-home_presentcomposedesign_2022_1920/"></div><div class="wpr-cv-container"><div class="wpr-cv-outer"><div class="wpr-cv-inner"><div class="wpr-grid-media-hover-middle elementor-clearfix"><div class="wpr-grid-item-lightbox elementor-repeater-item-1433fcf wpr-grid-item-display-block wpr-grid-item-align-center wpr-element-fade-in wpr-anim-size-large wpr-anim-timing-ease-default wpr-anim-transparency"><div class="inner-block"><span data-src="https://presentcomposedesign.fr/wp-content/uploads/2022/12/FLUFFY-HOME_PresentComposedesign_2022_1920.jpg"><i class="fas fa-search"></i></span><div class="wpr-grid-lightbox-overlay"></div></div></div></div></div></div></div></div></div></div></article></section>				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-ce40ac1 e-flex e-con-boxed wpr-particle-no wpr-jarallax-no wpr-parallax-no wpr-sticky-section-no wpr-column-slider-no wpr-equal-height-no elementor-invisible e-con e-parent" data-id="ce40ac1" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;,&quot;animation&quot;:&quot;bounceInLeft&quot;}">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-288f536 elementor-view-default elementor-widget elementor-widget-icon" data-id="288f536" data-element_type="widget" data-e-type="widget" data-widget_type="icon.default">
				<div class="elementor-widget-container">
							<div class="elementor-icon-wrapper">
			<div class="elementor-icon">
			<svg xmlns="http://www.w3.org/2000/svg" id="Calque_1" data-name="Calque 1" viewBox="0 0 230 95"><defs><style>      .cls-1 {        fill: #fc6a00;      }      .cls-1, .cls-2, .cls-3 {        stroke-width: 0px;      }      .cls-2 {        fill: #0010ef;      }      .cls-3 {        fill: #fff;        opacity: .8;      }    </style></defs><rect class="cls-3" x="13.9" y="4" width="212.9" height="57.7" rx="4" ry="4"></rect><path class="cls-2" d="M222.8,62.2H17.9c-2.5,0-4.5-2-4.5-4.5V8c0-2.5,2-4.5,4.5-4.5h204.9c2.5,0,4.5,2,4.5,4.5v49.7c0,2.5-2,4.5-4.5,4.5ZM17.9,4.5c-1.9,0-3.5,1.6-3.5,3.5v49.7c0,1.9,1.6,3.5,3.5,3.5h204.9c1.9,0,3.5-1.6,3.5-3.5V8c0-1.9-1.6-3.5-3.5-3.5H17.9Z"></path><g><path class="cls-2" d="M43.8,17.6c-.6-.3-1.2-.5-2.1-.5h-4v10.6h1.9v-3.6h2c.8,0,1.5-.2,2.1-.5.6-.3,1-.7,1.3-1.2.3-.5.4-1.1.4-1.8s-.1-1.3-.4-1.8c-.3-.5-.7-1-1.3-1.3ZM43.3,21.7c-.1.3-.4.5-.7.7-.3.2-.7.2-1.2.2h-1.8v-3.8h1.8c.5,0,.9,0,1.2.2.3.2.5.4.7.7.1.3.2.6.2,1s0,.7-.2,1Z"></path><path class="cls-2" d="M51.9,20.2c-.6-.3-1.2-.5-2-.5s-1.5.2-2,.5c-.6.3-1,.8-1.3,1.4-.3.6-.5,1.3-.5,2.2s.2,1.5.5,2.2c.3.6.8,1.1,1.3,1.4.6.3,1.2.5,2,.5s1.5-.2,2-.5c.6-.3,1-.8,1.3-1.4.3-.6.5-1.3.5-2.2s-.2-1.5-.5-2.2c-.3-.6-.8-1.1-1.3-1.4ZM51.5,25.2c-.1.4-.4.7-.6.9s-.6.3-1.1.3-.8-.1-1.1-.3-.5-.5-.6-.9-.2-.8-.2-1.3,0-.9.2-1.3.4-.7.6-.9.6-.3,1.1-.3.8.1,1.1.3.5.5.6.9.2.8.2,1.3,0,.9-.2,1.3Z"></path><path class="cls-2" d="M57.3,27.9c.6,0,1.1-.1,1.5-.4.4-.3.7-.7.9-1.1h0v1.4h1.8v-8h-1.9v4.6c0,.4,0,.7-.2,1-.2.3-.4.5-.6.6-.3.1-.5.2-.8.2-.5,0-.8-.1-1.1-.4-.3-.3-.4-.7-.4-1.2v-4.8h-1.9v5.1c0,.6.1,1.2.3,1.6.2.4.5.8.9,1,.4.2.9.3,1.4.3Z"></path><path class="cls-2" d="M64.9,23.1c0-.3,0-.6.2-.9s.4-.5.6-.6c.3-.1.6-.2.9-.2s.3,0,.5,0c.2,0,.3,0,.4,0v-1.7c0,0-.2,0-.3,0-.1,0-.2,0-.3,0-.5,0-.9.1-1.2.4-.4.2-.6.6-.7,1.1h0v-1.3h-1.8v8h1.9v-4.7Z"></path><path class="cls-2" d="M72.5,27.3c.4.4,1,.6,1.7.6s.4,0,.6,0,.4,0,.6-.1l-.3-1.5c0,0-.2,0-.3,0,0,0-.2,0-.3,0-.3,0-.5,0-.7-.2-.1-.1-.2-.4-.2-.7v-4.1h1.6v-1.5h-1.6v-1.9h-1.9v1.9h-1.1v1.5h1.1v4.4c0,.7.2,1.2.6,1.6Z"></path><path class="cls-2" d="M77.7,27.4c.6.3,1.2.5,2,.5s1.5-.2,2-.5c.6-.3,1-.8,1.3-1.4.3-.6.5-1.3.5-2.2s-.2-1.5-.5-2.2c-.3-.6-.8-1.1-1.3-1.4-.6-.3-1.2-.5-2-.5s-1.5.2-2,.5c-.6.3-1,.8-1.3,1.4-.3.6-.5,1.3-.5,2.2s.2,1.5.5,2.2c.3.6.8,1.1,1.3,1.4ZM78,22.5c.1-.4.4-.7.6-.9s.6-.3,1.1-.3.8.1,1.1.3.5.5.6.9.2.8.2,1.3,0,.9-.2,1.3-.4.7-.6.9-.6.3-1.1.3-.8-.1-1.1-.3-.5-.5-.6-.9-.2-.8-.2-1.3,0-.9.2-1.3Z"></path><path class="cls-2" d="M91.6,27.8v-8h-1.9v4.6c0,.4,0,.7-.2,1-.2.3-.4.5-.6.6-.3.1-.5.2-.8.2-.5,0-.8-.1-1.1-.4-.3-.3-.4-.7-.4-1.2v-4.8h-1.9v5.1c0,.6.1,1.2.3,1.6.2.4.5.8.9,1,.4.2.9.3,1.4.3.6,0,1.1-.1,1.5-.4.4-.3.7-.7.9-1.1h0v1.4h1.8Z"></path><path class="cls-2" d="M93.5,25.7c0,.7.2,1.2.6,1.6.4.4,1,.6,1.7.6s.4,0,.6,0,.4,0,.6-.1l-.3-1.5c0,0-.2,0-.3,0,0,0-.2,0-.3,0-.3,0-.5,0-.7-.2-.1-.1-.2-.4-.2-.7v-4.1h1.6v-1.5h-1.6v-1.9h-1.9v1.9h-1.1v1.5h1.1v4.4Z"></path><path class="cls-2" d="M103,27.7c.5-.2.9-.5,1.2-.8s.5-.7.6-1.2l-1.8-.2c0,.2-.2.4-.4.6-.2.2-.3.3-.6.3-.2,0-.5.1-.7.1-.4,0-.8,0-1.1-.3-.3-.2-.5-.4-.7-.8-.2-.3-.2-.7-.2-1.1h5.5v-.6c0-.7,0-1.3-.3-1.8-.2-.5-.5-.9-.8-1.3-.3-.3-.7-.6-1.2-.7-.4-.2-.9-.2-1.4-.2-.8,0-1.4.2-2,.5-.6.3-1,.8-1.3,1.5-.3.6-.5,1.3-.5,2.2s.2,1.6.5,2.2c.3.6.8,1.1,1.3,1.4.6.3,1.3.5,2.1.5s1.2,0,1.6-.3ZM100.2,21.5c.3-.2.6-.3,1-.3s.7,0,1,.3c.3.2.5.5.6.8,0,.2.1.5.2.8h-3.7c0-.3,0-.6.2-.8.1-.3.4-.6.6-.8Z"></path><path class="cls-2" d="M112,26.7c.3-.4.4-.8.4-1.3s-.2-1-.6-1.4c-.4-.4-.9-.6-1.7-.8l-1.4-.3c-.4,0-.7-.2-.9-.4s-.3-.3-.3-.6.1-.5.4-.7c.3-.2.6-.3,1-.3s.8.1,1,.3.4.4.5.7l1.7-.2c-.1-.7-.5-1.2-1-1.6-.5-.4-1.3-.6-2.2-.6s-1.2,0-1.7.3c-.5.2-.9.5-1.1.8s-.4.8-.4,1.3.2,1.1.5,1.4c.4.4.9.6,1.7.8l1.4.3c.7.2,1.1.5,1.1.9s-.1.5-.4.7-.6.3-1.1.3-.8,0-1.1-.3c-.3-.2-.5-.5-.5-.8l-1.8.2c.1.7.5,1.3,1.1,1.7.6.4,1.4.6,2.3.6s1.3-.1,1.8-.3.9-.5,1.2-.9Z"></path><path class="cls-2" d="M117.7,27.5c.5.3,1.1.5,1.7.5s.8,0,1.1-.2c.3-.2.5-.3.7-.6.2-.2.3-.4.4-.6h.1v1.3h1.8v-10.6h-1.9v4h0c0-.2-.2-.4-.4-.6-.2-.2-.4-.4-.7-.6-.3-.2-.7-.2-1.2-.2s-1.2.2-1.7.5c-.5.3-.9.8-1.2,1.4-.3.6-.4,1.4-.4,2.2s.1,1.6.4,2.2.7,1.1,1.2,1.4ZM118.3,22.5c.1-.4.4-.7.6-.9s.6-.3,1-.3.7.1,1,.3.5.5.6.9c.1.4.2.8.2,1.4s0,1-.2,1.4c-.1.4-.4.7-.6.9-.3.2-.6.3-1,.3s-.8-.1-1-.3c-.3-.2-.5-.5-.6-.9s-.2-.8-.2-1.3,0-.9.2-1.3Z"></path><path class="cls-2" d="M130.2,27.7c.5-.2.9-.5,1.2-.8s.5-.7.6-1.2l-1.8-.2c0,.2-.2.4-.4.6-.2.2-.3.3-.6.3-.2,0-.5.1-.7.1-.4,0-.8,0-1.1-.3s-.5-.4-.7-.8c-.2-.3-.2-.7-.2-1.1h5.5v-.6c0-.7,0-1.3-.3-1.8-.2-.5-.5-.9-.8-1.3-.3-.3-.7-.6-1.2-.7-.4-.2-.9-.2-1.4-.2-.8,0-1.4.2-2,.5s-1,.8-1.3,1.5c-.3.6-.5,1.3-.5,2.2s.2,1.6.5,2.2.8,1.1,1.3,1.4c.6.3,1.3.5,2.1.5s1.2,0,1.6-.3ZM127.4,21.5c.3-.2.6-.3,1-.3s.7,0,1,.3c.3.2.5.5.6.8,0,.2.1.5.2.8h-3.7c0-.3,0-.6.2-.8.1-.3.4-.6.6-.8Z"></path><path class="cls-2" d="M142,21.7c.3.2.4.6.4,1.1v5.1h1.9v-5.3c0-.9-.2-1.6-.7-2-.5-.5-1.1-.7-1.8-.7s-1.1.1-1.5.4c-.4.3-.7.6-.9,1.1h0c-.1-.5-.4-.8-.8-1.1-.4-.3-.8-.4-1.4-.4s-1,.1-1.4.4-.7.6-.8,1.1h0v-1.4h-1.8v8h1.9v-4.8c0-.3,0-.6.2-.9.1-.2.3-.4.5-.6.2-.1.5-.2.7-.2.4,0,.7.1,1,.4.2.2.4.6.4,1v5.1h1.9v-5c0-.4.1-.8.4-1.1.3-.3.6-.4,1.1-.4s.7.1,1,.3Z"></path><path class="cls-2" d="M149.8,27.3c.2-.2.4-.4.5-.6h0v1.1h1.8v-5.3c0-.5,0-1-.3-1.3-.2-.4-.4-.6-.8-.8-.3-.2-.7-.4-1-.4-.4,0-.8-.1-1.1-.1-.6,0-1.1,0-1.5.2-.5.2-.8.4-1.2.7s-.5.7-.7,1.2l1.7.2c0-.3.3-.5.5-.7.3-.2.6-.3,1.1-.3s.8.1,1,.3c.2.2.3.5.3.9h0c0,.2,0,.3-.2.4-.1,0-.3.1-.6.2s-.7,0-1.1.1c-.4,0-.8.1-1.1.2s-.7.2-1,.4c-.3.2-.5.4-.7.7-.2.3-.2.7-.2,1.1s.1,1,.3,1.3c.2.4.6.6,1,.8.4.2.9.3,1.4.3s.8,0,1.1-.2c.3-.1.6-.3.8-.5ZM147.5,26.3c-.3-.2-.4-.4-.4-.8s0-.4.2-.6c.1-.2.3-.3.5-.4.2,0,.5-.2.8-.2.1,0,.3,0,.4,0,.2,0,.3,0,.5,0s.3,0,.5-.1c.1,0,.2,0,.3-.1v.9c0,.3,0,.6-.2.8-.2.2-.4.4-.6.6-.3.1-.6.2-1,.2s-.7,0-1-.3Z"></path><path class="cls-2" d="M159.3,20.1c-.4-.2-.9-.3-1.4-.3s-1.1.1-1.5.4c-.4.3-.7.6-.9,1.1h0v-1.4h-1.8v8h1.9v-4.7c0-.4,0-.7.2-1,.1-.3.3-.5.6-.6.3-.1.5-.2.9-.2.5,0,.9.2,1.1.5.3.3.4.7.4,1.3v4.8h1.9v-5.1c0-.6-.1-1.2-.3-1.6-.2-.4-.5-.8-1-1Z"></path><path class="cls-2" d="M166,27.7c.3-.2.5-.3.7-.6.2-.2.3-.4.4-.6h.1v1.3h1.8v-10.6h-1.9v4h0c0-.2-.2-.4-.4-.6-.2-.2-.4-.4-.7-.6-.3-.2-.7-.2-1.2-.2s-1.2.2-1.7.5c-.5.3-.9.8-1.2,1.4-.3.6-.4,1.4-.4,2.2s.1,1.6.4,2.2.7,1.1,1.2,1.4c.5.3,1.1.5,1.7.5s.8,0,1.1-.2ZM163.7,25.2c-.1-.4-.2-.8-.2-1.3s0-.9.2-1.3c.1-.4.4-.7.6-.9.3-.2.6-.3,1-.3s.7.1,1,.3c.3.2.5.5.6.9.1.4.2.8.2,1.4s0,1-.2,1.4c-.1.4-.4.7-.6.9-.3.2-.6.3-1,.3s-.8-.1-1-.3c-.3-.2-.5-.5-.6-.9Z"></path><path class="cls-2" d="M175.7,25.5c0,.2-.2.4-.4.6-.2.2-.3.3-.6.3-.2,0-.5.1-.7.1-.4,0-.8,0-1.1-.3s-.5-.4-.7-.8c-.2-.3-.2-.7-.2-1.1h5.5v-.6c0-.7,0-1.3-.3-1.8-.2-.5-.5-.9-.8-1.3-.3-.3-.7-.6-1.2-.7-.4-.2-.9-.2-1.4-.2-.8,0-1.4.2-2,.5s-1,.8-1.3,1.5c-.3.6-.5,1.3-.5,2.2s.2,1.6.5,2.2c.3.6.8,1.1,1.3,1.4.6.3,1.3.5,2.1.5s1.2,0,1.6-.3.9-.5,1.2-.8.5-.7.6-1.2l-1.8-.2ZM172.9,21.5c.3-.2.6-.3,1-.3s.7,0,1,.3c.3.2.5.5.6.8,0,.2.1.5.2.8h-3.7c0-.3,0-.6.2-.8.1-.3.4-.6.6-.8Z"></path><path class="cls-2" d="M182.8,23.3l-1.4-.3c-.4,0-.7-.2-.9-.4-.2-.1-.3-.3-.3-.6s.1-.5.4-.7c.3-.2.6-.3,1-.3s.8.1,1,.3.4.4.5.7l1.7-.2c-.1-.7-.5-1.2-1-1.6-.5-.4-1.3-.6-2.2-.6s-1.2,0-1.7.3c-.5.2-.9.5-1.1.8-.3.4-.4.8-.4,1.3s.2,1.1.5,1.4c.4.4.9.6,1.7.8l1.4.3c.7.2,1.1.5,1.1.9s-.1.5-.4.7c-.3.2-.6.3-1.1.3s-.8,0-1.1-.3-.5-.5-.5-.8l-1.8.2c.1.7.5,1.3,1.1,1.7s1.4.6,2.3.6,1.3-.1,1.8-.3.9-.5,1.2-.9c.3-.4.4-.8.4-1.3s-.2-1-.6-1.4c-.4-.4-.9-.6-1.7-.8Z"></path><path class="cls-2" d="M194.6,20.2c-.6-.3-1.2-.5-2-.5s-1.5.2-2,.5c-.6.3-1,.8-1.3,1.4-.3.6-.5,1.3-.5,2.2s.2,1.5.5,2.2c.3.6.8,1.1,1.3,1.4s1.2.5,2,.5,1.5-.2,2-.5,1-.8,1.3-1.4c.3-.6.5-1.3.5-2.2s-.2-1.5-.5-2.2c-.3-.6-.8-1.1-1.3-1.4ZM194.3,25.2c-.1.4-.4.7-.6.9-.3.2-.6.3-1.1.3s-.8-.1-1.1-.3-.5-.5-.6-.9c-.1-.4-.2-.8-.2-1.3s0-.9.2-1.3c.1-.4.4-.7.6-.9s.6-.3,1.1-.3.8.1,1.1.3c.3.2.5.5.6.9.1.4.2.8.2,1.3s0,.9-.2,1.3Z"></path><path class="cls-2" d="M198.7,27.6c.4.2.9.3,1.4.3.6,0,1.1-.1,1.5-.4.4-.3.7-.7.9-1.1h0v1.4h1.8v-8h-1.9v4.6c0,.4,0,.7-.2,1-.2.3-.4.5-.6.6-.3.1-.5.2-.8.2-.5,0-.8-.1-1.1-.4-.3-.3-.4-.7-.4-1.2v-4.8h-1.9v5.1c0,.6.1,1.2.3,1.6.2.4.5.8.9,1Z"></path><path class="cls-2" d="M61.4,37.7c-.5-.3-1-.5-1.7-.5s-.9,0-1.2.2c-.3.2-.5.3-.7.6-.2.2-.3.4-.4.6h-.1v-1.3h-1.8v10.9h1.9v-4.2h0c0,.2.2.4.4.6.2.2.4.4.7.6s.7.2,1.1.2,1.2-.2,1.7-.5c.5-.3.9-.8,1.2-1.4s.4-1.4.4-2.2-.1-1.6-.4-2.2c-.3-.6-.7-1.1-1.2-1.4ZM60.9,42.7c-.1.4-.4.7-.6.9-.3.2-.6.3-1,.3s-.7-.1-1-.3c-.3-.2-.5-.5-.6-.9-.1-.4-.2-.8-.2-1.4s0-1,.2-1.4c.1-.4.4-.7.6-.9s.6-.3,1-.3.8.1,1,.3c.3.2.5.5.6.9s.2.8.2,1.3,0,.9-.2,1.3Z"></path><path class="cls-2" d="M67.9,37.2c-.5,0-.9.1-1.2.4-.4.2-.6.6-.7,1.1h0v-1.3h-1.8v8h1.9v-4.7c0-.3,0-.6.2-.9s.4-.5.6-.6c.3-.1.6-.2.9-.2s.3,0,.5,0c.2,0,.3,0,.4,0v-1.7c0,0-.2,0-.3,0-.1,0-.2,0-.3,0Z"></path><path class="cls-2" d="M70.2,34.2c-.3,0-.6,0-.8.3-.2.2-.3.4-.3.7s.1.5.3.7.5.3.8.3.5,0,.8-.3.3-.4.3-.7-.1-.5-.3-.7c-.2-.2-.5-.3-.8-.3Z"></path><rect class="cls-2" x="69.3" y="37.4" width="1.9" height="8"></rect><path class="cls-2" d="M76.8,40.8l-1.4-.3c-.4,0-.7-.2-.9-.4s-.3-.3-.3-.6.1-.5.4-.7c.3-.2.6-.3,1-.3s.8.1,1,.3.4.4.5.7l1.7-.2c-.1-.7-.5-1.2-1-1.6-.5-.4-1.3-.6-2.2-.6s-1.2,0-1.7.3c-.5.2-.9.5-1.1.8s-.4.8-.4,1.3.2,1.1.5,1.4c.4.4.9.6,1.7.8l1.4.3c.7.2,1.1.5,1.1.9s-.1.5-.4.7-.6.3-1.1.3-.8,0-1.1-.3c-.3-.2-.5-.5-.5-.8l-1.8.2c.1.7.5,1.3,1.1,1.7.6.4,1.4.6,2.3.6s1.3-.1,1.8-.3.9-.5,1.2-.9c.3-.4.4-.8.4-1.3s-.2-1-.6-1.4c-.4-.4-.9-.6-1.7-.8Z"></path><path class="cls-2" d="M86,38.2c-.3-.3-.7-.6-1.2-.7-.4-.2-.9-.2-1.4-.2-.8,0-1.4.2-2,.5-.6.3-1,.8-1.3,1.5-.3.6-.5,1.3-.5,2.2s.2,1.6.5,2.2c.3.6.8,1.1,1.3,1.4.6.3,1.3.5,2.1.5s1.2,0,1.6-.3.9-.5,1.2-.8.5-.7.6-1.2l-1.8-.2c0,.2-.2.4-.4.6-.2.2-.3.3-.6.3-.2,0-.5.1-.7.1-.4,0-.8,0-1.1-.3-.3-.2-.5-.4-.7-.8-.2-.3-.2-.7-.2-1.1h5.5v-.6c0-.7,0-1.3-.3-1.8-.2-.5-.5-.9-.8-1.3ZM82.4,39c.3-.2.6-.3,1-.3s.7,0,1,.3c.3.2.5.5.6.8,0,.2.1.5.2.8h-3.7c0-.3,0-.6.2-.8.1-.3.4-.6.6-.8Z"></path><path class="cls-2" d="M96.5,38.7h0c0-.2-.2-.4-.4-.6-.2-.2-.4-.4-.7-.6-.3-.2-.7-.2-1.2-.2s-1.2.2-1.7.5-.9.8-1.2,1.4c-.3.6-.4,1.4-.4,2.2s.1,1.6.4,2.2.7,1.1,1.2,1.4c.5.3,1.1.5,1.7.5s.8,0,1.1-.2c.3-.2.5-.3.7-.6.2-.2.3-.4.4-.6h.1v1.3h1.8v-10.6h-1.9v4ZM96.3,42.7c-.1.4-.4.7-.6.9-.3.2-.6.3-1,.3s-.8-.1-1-.3c-.3-.2-.5-.5-.6-.9s-.2-.8-.2-1.3,0-.9.2-1.3.4-.7.6-.9.6-.3,1-.3.7.1,1,.3c.3.2.5.5.6.9s.2.8.2,1.4,0,1-.2,1.4Z"></path><path class="cls-2" d="M105.7,38.2c-.3-.3-.7-.6-1.2-.7-.4-.2-.9-.2-1.4-.2-.8,0-1.4.2-2,.5-.6.3-1,.8-1.3,1.5-.3.6-.5,1.3-.5,2.2s.2,1.6.5,2.2c.3.6.8,1.1,1.3,1.4.6.3,1.3.5,2.1.5s1.2,0,1.6-.3.9-.5,1.2-.8.5-.7.6-1.2l-1.8-.2c0,.2-.2.4-.4.6-.2.2-.3.3-.6.3-.2,0-.5.1-.7.1-.4,0-.8,0-1.1-.3-.3-.2-.5-.4-.7-.8-.2-.3-.2-.7-.2-1.1h5.5v-.6c0-.7,0-1.3-.3-1.8-.2-.5-.5-.9-.8-1.3ZM102.1,39c.3-.2.6-.3,1-.3s.7,0,1,.3c.3.2.5.5.6.8,0,.2.1.5.2.8h-3.7c0-.3,0-.6.2-.8.1-.3.4-.6.6-.8Z"></path><path class="cls-2" d="M113.4,39.1c.3-.2.6-.3,1-.3s.6,0,.8.2c.2.1.4.3.5.5.1.2.2.4.3.7h1.8c0-.6-.2-1.1-.5-1.5-.3-.4-.7-.8-1.2-1-.5-.2-1.1-.4-1.7-.4s-1.4.2-2,.5c-.6.3-1,.8-1.3,1.5-.3.6-.5,1.3-.5,2.1s.2,1.5.5,2.1c.3.6.7,1.1,1.3,1.5s1.3.5,2,.5,1.2-.1,1.7-.4c.5-.2.9-.6,1.2-1,.3-.4.5-.9.5-1.5h-1.8c0,.4-.3.8-.6,1s-.6.4-1.1.4-.7-.1-1-.3c-.3-.2-.5-.5-.7-.9-.2-.4-.2-.9-.2-1.4s0-1,.2-1.4.4-.7.7-.9Z"></path><path class="cls-2" d="M124.3,37.8c-.6-.3-1.2-.5-2-.5s-1.5.2-2,.5c-.6.3-1,.8-1.3,1.4-.3.6-.5,1.3-.5,2.2s.2,1.5.5,2.2c.3.6.8,1.1,1.3,1.4.6.3,1.2.5,2,.5s1.5-.2,2-.5c.6-.3,1-.8,1.3-1.4.3-.6.5-1.3.5-2.2s-.2-1.5-.5-2.2c-.3-.6-.8-1.1-1.3-1.4ZM124,42.7c-.1.4-.4.7-.6.9s-.6.3-1.1.3-.8-.1-1.1-.3-.5-.5-.6-.9c-.1-.4-.2-.8-.2-1.3s0-.9.2-1.3c.1-.4.4-.7.6-.9s.6-.3,1.1-.3.8.1,1.1.3.5.5.6.9c.1.4.2.8.2,1.3s0,.9-.2,1.3Z"></path><path class="cls-2" d="M132.8,37.6c-.4-.2-.9-.3-1.4-.3s-1.1.1-1.5.4c-.4.3-.7.6-.9,1.1h0v-1.4h-1.8v8h1.9v-4.7c0-.4,0-.7.2-1,.1-.3.3-.5.6-.6.3-.1.5-.2.9-.2.5,0,.9.2,1.1.5.3.3.4.7.4,1.3v4.8h1.9v-5.1c0-.6-.1-1.2-.3-1.6-.2-.4-.5-.8-1-1Z"></path><path class="cls-2" d="M139,43.8c0,0-.2,0-.3,0-.3,0-.5,0-.7-.2s-.2-.4-.2-.7v-4.1h1.6v-1.5h-1.6v-1.9h-1.9v1.9h-1.1v1.5h1.1v4.4c0,.7.2,1.2.6,1.6.4.4,1,.6,1.7.6s.4,0,.6,0c.2,0,.4,0,.6-.1l-.3-1.5c0,0-.2,0-.3,0Z"></path><path class="cls-2" d="M145.9,37.8c-.3-.2-.7-.4-1-.4-.4,0-.8-.1-1.1-.1-.6,0-1.1,0-1.5.2-.5.2-.8.4-1.2.7s-.5.7-.7,1.2l1.7.2c0-.3.3-.5.5-.7.3-.2.6-.3,1.1-.3s.8.1,1,.3c.2.2.3.5.3.9h0c0,.2,0,.3-.2.4-.1,0-.3.1-.6.2s-.7,0-1.1.1c-.4,0-.8.1-1.1.2s-.7.2-1,.4c-.3.2-.5.4-.7.7-.2.3-.2.7-.2,1.1s.1,1,.3,1.3c.2.4.6.6,1,.8.4.2.9.3,1.4.3s.8,0,1.1-.2c.3-.1.6-.3.8-.5s.4-.4.5-.6h0v1.1h1.8v-5.3c0-.5,0-1-.3-1.3-.2-.4-.4-.6-.8-.8ZM145.1,42.5c0,.3,0,.6-.2.8-.2.2-.4.4-.6.6-.3.1-.6.2-1,.2s-.7,0-1-.3-.4-.4-.4-.8,0-.4.2-.6c.1-.2.3-.3.5-.4.2,0,.5-.2.8-.2.1,0,.3,0,.4,0,.2,0,.3,0,.5,0s.3,0,.5-.1c.1,0,.2,0,.3-.1v.9Z"></path><path class="cls-2" d="M150.9,39.1c.3-.2.6-.3,1-.3s.6,0,.8.2c.2.1.4.3.5.5.1.2.2.4.3.7h1.8c0-.6-.2-1.1-.5-1.5-.3-.4-.7-.8-1.2-1-.5-.2-1.1-.4-1.7-.4s-1.4.2-2,.5c-.6.3-1,.8-1.3,1.5-.3.6-.5,1.3-.5,2.1s.2,1.5.5,2.1c.3.6.7,1.1,1.3,1.5s1.3.5,2,.5,1.2-.1,1.7-.4c.5-.2.9-.6,1.2-1,.3-.4.5-.9.5-1.5h-1.8c0,.4-.3.8-.6,1s-.6.4-1.1.4-.7-.1-1-.3c-.3-.2-.5-.5-.7-.9-.2-.4-.2-.9-.2-1.4s0-1,.2-1.4c.2-.4.4-.7.7-.9Z"></path><path class="cls-2" d="M159.8,43.8c0,0-.2,0-.3,0-.3,0-.5,0-.7-.2s-.2-.4-.2-.7v-4.1h1.6v-1.5h-1.6v-1.9h-1.9v1.9h-1.1v1.5h1.1v4.4c0,.7.2,1.2.6,1.6.4.4,1,.6,1.7.6s.4,0,.6,0c.2,0,.4,0,.6-.1l-.3-1.5c0,0-.2,0-.3,0Z"></path><path class="cls-2" d="M168.2,37.2c-.5,0-.9.1-1.2.4-.4.2-.6.6-.7,1.1h0v-1.3h-1.8v8h1.9v-4.7c0-.3,0-.6.2-.9s.4-.5.6-.6c.3-.1.6-.2.9-.2s.3,0,.5,0c.2,0,.3,0,.4,0v-1.7c0,0-.2,0-.3,0-.1,0-.2,0-.3,0Z"></path><path class="cls-2" d="M174.9,37.8c-.3-.2-.7-.4-1-.4-.4,0-.8-.1-1.1-.1-.6,0-1.1,0-1.5.2-.5.2-.8.4-1.2.7s-.5.7-.7,1.2l1.7.2c0-.3.3-.5.5-.7.3-.2.6-.3,1.1-.3s.8.1,1,.3c.2.2.3.5.3.9h0c0,.2,0,.3-.2.4-.1,0-.3.1-.6.2s-.7,0-1.1.1c-.4,0-.8.1-1.1.2s-.7.2-1,.4c-.3.2-.5.4-.7.7-.2.3-.2.7-.2,1.1s.1,1,.3,1.3c.2.4.6.6,1,.8.4.2.9.3,1.4.3s.8,0,1.1-.2c.3-.1.6-.3.8-.5s.4-.4.5-.6h0v1.1h1.8v-5.3c0-.5,0-1-.3-1.3-.2-.4-.4-.6-.8-.8ZM174.1,42.5c0,.3,0,.6-.2.8-.2.2-.4.4-.6.6-.3.1-.6.2-1,.2s-.7,0-1-.3-.4-.4-.4-.8,0-.4.2-.6c.1-.2.3-.3.5-.4.2,0,.5-.2.8-.2.1,0,.3,0,.4,0,.2,0,.3,0,.5,0s.3,0,.5-.1c.1,0,.2,0,.3-.1v.9Z"></path><path class="cls-2" d="M183.2,37.7c-.5-.3-1-.5-1.7-.5s-.9,0-1.2.2c-.3.2-.5.3-.7.6-.2.2-.3.4-.4.6h-.1v-1.3h-1.8v10.9h1.9v-4.2h0c0,.2.2.4.4.6.2.2.4.4.7.6.3.2.7.2,1.1.2s1.2-.2,1.7-.5c.5-.3.9-.8,1.2-1.4.3-.6.4-1.4.4-2.2s-.1-1.6-.4-2.2c-.3-.6-.7-1.1-1.2-1.4ZM182.7,42.7c-.1.4-.4.7-.6.9-.3.2-.6.3-1,.3s-.7-.1-1-.3c-.3-.2-.5-.5-.6-.9-.1-.4-.2-.8-.2-1.4s0-1,.2-1.4c.1-.4.4-.7.6-.9.3-.2.6-.3,1-.3s.8.1,1,.3c.3.2.5.5.6.9s.2.8.2,1.3,0,.9-.2,1.3Z"></path><path class="cls-2" d="M186.8,34.2c-.3,0-.6,0-.8.3-.2.2-.3.4-.3.7s.1.5.3.7.5.3.8.3.5,0,.8-.3.3-.4.3-.7-.1-.5-.3-.7c-.2-.2-.5-.3-.8-.3Z"></path><rect class="cls-2" x="185.9" y="37.4" width="1.9" height="8"></rect><path class="cls-2" d="M194.4,38.7h0c0-.2-.2-.4-.4-.6-.2-.2-.4-.4-.7-.6-.3-.2-.7-.2-1.2-.2s-1.2.2-1.7.5c-.5.3-.9.8-1.2,1.4-.3.6-.4,1.4-.4,2.2s.1,1.6.4,2.2.7,1.1,1.2,1.4c.5.3,1.1.5,1.7.5s.8,0,1.1-.2c.3-.2.5-.3.7-.6.2-.2.3-.4.4-.6h.1v1.3h1.8v-10.6h-1.9v4ZM194.3,42.7c-.1.4-.4.7-.6.9-.3.2-.6.3-1,.3s-.8-.1-1-.3c-.3-.2-.5-.5-.6-.9s-.2-.8-.2-1.3,0-.9.2-1.3c.1-.4.4-.7.6-.9.3-.2.6-.3,1-.3s.7.1,1,.3c.3.2.5.5.6.9.1.4.2.8.2,1.4s0,1-.2,1.4Z"></path><path class="cls-2" d="M204.8,41.3c0-.7,0-1.3-.3-1.8-.2-.5-.5-.9-.8-1.3-.3-.3-.7-.6-1.2-.7-.4-.2-.9-.2-1.4-.2-.8,0-1.4.2-2,.5s-1,.8-1.3,1.5c-.3.6-.5,1.3-.5,2.2s.2,1.6.5,2.2c.3.6.8,1.1,1.3,1.4.6.3,1.3.5,2.1.5s1.2,0,1.6-.3.9-.5,1.2-.8.5-.7.6-1.2l-1.8-.2c0,.2-.2.4-.4.6-.2.2-.3.3-.6.3-.2,0-.5.1-.7.1-.4,0-.8,0-1.1-.3s-.5-.4-.7-.8c-.2-.3-.2-.7-.2-1.1h5.5v-.6ZM200.1,39c.3-.2.6-.3,1-.3s.7,0,1,.3c.3.2.5.5.6.8,0,.2.1.5.2.8h-3.7c0-.3,0-.6.2-.8.1-.3.4-.6.6-.8Z"></path></g><path class="cls-1" d="M41.4,62.3S-10.4,45.2,28.6,1.1c0,0-64.5,42.1,10.1,74.6l-8.9,14.8,42.1-14.9-32.5-30.1,2,16.7Z"></path><path class="cls-2" d="M72.4,75.2c-3.6-3.4-7.3-6.7-10.9-10.1-5.9-5.4-11.7-10.8-17.6-16.2-1.3-1.2-2.7-2.5-4-3.7-.4-.4-1.3,0-1.3.6.6,5.2,1.3,10.4,1.9,15.6-4.9-1.8-9.6-4.5-13.6-7.7-3.8-3-7.1-6.6-9.3-10.9-2.6-5-3.3-10.7-2.3-16.3,1.7-9.5,7.7-17.9,14-25,.6-.7-.7-.9-1.1-.6-2.1,1.4-4.1,2.9-6,4.6-4.3,3.6-8.3,7.5-11.7,12-3.8,5-7,10.7-8.1,17-1.2,7,.6,13.9,4.6,19.6,6.3,9.1,16.4,15.2,26.2,19.9,1.5.7,3.1,1.5,4.7,2.2-2.9,4.8-5.8,9.6-8.7,14.4-.3.5.5.7.8.6,4.7-1.7,9.5-3.4,14.2-5l22.6-8c1.7-.6,3.5-1.2,5.2-1.9.4-.1.8-.6.4-1ZM57.5,80.1l-22.6,8c-1.2.4-2.5.9-3.7,1.3,2.7-4.6,5.5-9.1,8.2-13.7.2-.2,0-.5-.3-.6C23.6,68.4,1.4,56.1,3.6,36c1.3-11.8,9.9-21.7,18.6-29.1.9-.8,1.9-1.6,2.8-2.3-8.2,10.3-15.1,23.6-9.8,36.6,3.6,8.7,11.4,14.8,19.6,19,1.9,1,3.9,1.9,6,2.6.3.3,1.1,0,1.3-.4,0,0,0-.2,0-.3-.6-4.9-1.2-9.8-1.8-14.7,3.1,2.9,6.3,5.8,9.4,8.7,5.9,5.4,11.7,10.8,17.6,16.2,1.1,1,2.3,2.1,3.4,3.1-4.4,1.6-8.8,3.1-13.2,4.7Z"></path></svg>			</div>
		</div>
						</div>
				</div>
					</div>
				</div>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/iartworks/">[ia]RTWORKS</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
