<?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>Présent Composé design</title>
	<atom:link href="https://presentcomposedesign.fr/category/design-produit/feed/" rel="self" type="application/rss+xml" />
	<link>https://presentcomposedesign.fr/category/design-produit/</link>
	<description>Direction Artistique, concepts &#38; innovations, Design 2D/3D/ia/AR/VR</description>
	<lastBuildDate>Sat, 03 Oct 2026 02:31:29 +0000</lastBuildDate>
	<language>fr-FR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.4</generator>

<image>
	<url>https://presentcomposedesign.fr/wp-content/uploads/2025/07/cropped-logo_PCd_2025-512-32x32.png</url>
	<title>Présent Composé design</title>
	<link>https://presentcomposedesign.fr/category/design-produit/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Apps</title>
		<link>https://presentcomposedesign.fr/apps/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Mon, 28 Sep 2026 11:13:23 +0000</pubDate>
				<category><![CDATA[Design produit]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=36928</guid>

					<description><![CDATA[<p>Cet article <a href="https://presentcomposedesign.fr/apps/">Apps</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Cet article <a href="https://presentcomposedesign.fr/apps/">Apps</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Applications</title>
		<link>https://presentcomposedesign.fr/applications/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Sun, 27 Sep 2026 12:36:02 +0000</pubDate>
				<category><![CDATA[Design produit]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=36874</guid>

					<description><![CDATA[<p>Brouillon non publié : visible sur cet appareil seulement Ouvrir .dev PCdlabs · Présent Composé design Applications &#38; expériences Carrousel Grille Liste .dev .dev Mode développeur Code d&#8217;accès Annuler Entrer Mode développeur Administration Cartes Affichage En-tête Pied de page Publication Cartes de la vitrine + Ajouter une carte Version en ligne Versions précédentes Sauvegarde du contenu Le fichier JSON contient toutes les cartes et tous les réglages. Avec un serveur, c’est la version publiée qui s’affiche : le contenu collé dans le bloc Elementor ne sert que sans serveur, ou comme réserve à reprendre ici. Télécharger le JSON Copier pour le bloc Elementor Importer un JSON Reprendre le contenu du bloc Session Quitter le mode développeur Annuler les modifications Publier</p>
<p>Cet article <a href="https://presentcomposedesign.fr/applications/">Applications</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="36874" class="elementor elementor-36874">
				<article class="elementor-element elementor-element-b57d3b9 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="b57d3b9" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
				<div class="elementor-element elementor-element-6e5d796 e-transform elementor-widget elementor-widget-html" data-id="6e5d796" data-element_type="widget" data-e-type="widget" data-settings="{&quot;_transform_scale_effect&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;size&quot;:&quot;&quot;,&quot;sizes&quot;:[]},&quot;_transform_scale_effect_tablet&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;size&quot;:&quot;&quot;,&quot;sizes&quot;:[]},&quot;_transform_scale_effect_mobile&quot;:{&quot;unit&quot;:&quot;px&quot;,&quot;size&quot;:&quot;&quot;,&quot;sizes&quot;:[]}}" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!--
// ║  Tourniquet PCdlabs, © All rights reserved                                     ║
// ║  2026 · PCdlabs ·  Present Composé Design                                ║
// ║  Alban DESBARAX                                                          ║
// ║  360° Design & Creative Technologist - Toulouse, FRANCE                  ║
// ║                                                                          ║
// ║  · Expertise & Support 2D, 3D, AR, VR, XR, AI                            ║
// ║  · Product Design & Industrial Design                                    ║
// ║  · Digital communication                                                 ║
// ║  · Immersive experiences  -ᯅ- ✔️                                        ║
//                                                                            
//  · 🡺 https://presentcomposedesign.fr/      
//  · 🡺 https://pcdlabs.presentcomposedesign.fr/showcase/
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#*++==------===**%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#++-::::::::::::::::::::::-=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+-::::::::::::::::::::::::::::::::=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@#+::::::::::::::::::::::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@%+:::::::::::::::::=++***+=-::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@#=::::::::::::::::+%@@@@@@@@@@%+-:::::::::::::::::::=#@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@#-::::::::::::::::#@@@@@@@@@@@@@@@@%-:::::::::::::::::::=#@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@%=::::::::::::::::*@@@@@@**==-=+##@@@@@*-:::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@=:::::::::::::::::%@@@@@+::::::::::-*@@@@#-::::::::::::::::::::=@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@#::::::::::::::::::%@@@@=::::::::::::::-%@@@#-:::::::::::::::::::::#@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@=::::::::::::::::::*@@@@=::::::::::::::::-%@@@*::::::::::::::::::::::=@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@%-::::::::::::::::::-@@@@=::::::::::::::::::-%@@@-::::::::::::::::::::::-%@@@@@@@@@@@@@@
// @@@@@@@@@@@@#::::::::::::::::::::*@@@#::::::::::::::::::::=@@@*::::::::::::::::::::::::*@@@@@@@@@@@@@
// @@@@@@@@@@@#:::::::::::::::::::::%@@@+::::::::::::::::::::-@@@%:::::::::::::::::::::::::*@@@@@@@@@@@@
// @@@@@@@@@@+::::::::::::::::::::::@@@@-:::::::::::::::::::::%@@@::::::::::::::::::::::::::*@@@@@@@@@@@
// @@@@@@@@@*:::::::::::::::::::::::@@@@=:::::::::::::::::::::@@@@:::::::::::::::::::::::::::*@@@@@@@@@@
// @@@@@@@@@%::::::::::::::::::::::::@@@@*:::::::::::::::::::::@@@%::::::::::::::::::::::::::::*@@@@@@@@
// @@@@@@@%:::::::::::::::::::::::::@@@@%::::::::::::::::::::+@@@*:::::::::::::::::::::::::::::%@@@@@@@@
// @@@@@@@::::::::::::::::::::::::::@@@@@*::::::::::::::::::-@@@@::::::::::::::::::::::::::::::-@@@@@@@@
// @@@@@@=::::::::::::::::::::::::::@@@@@@*::::::::::::::::-@@@@+:::::::::::::::::::::::::::::::*@@@@@@@
// @@@@@%:::::::::::::::::::::::::::@@@@@@@*::::::::::::::-%@@@#:::::::::::::::::::::::::::::::::*@@@@@@
// @@@@@::::::::::::::::::::::::::::@@@@@@@@@*::::::::::+@@@@@*::::::::::::::::::::::::::::::::::-@@@@@@
// @@@@*::::::::::::::::::::::::::::@@@#=@@@@@@%#*+=**%@@@@@@=::::::::::::::::::::::::::::::::::::*@@@@@
// @@@@:::::::::::::::::::::::::::::@@@#::*@@@@@@@@@@@@@@@@*:::::::::::::::::::::::::::::::::::::::@@@@@
// @@@*:::::::::::::::::::::::::::::@@@#::::=*@@@@@@@@@%#=:::::::::::::::::::::::::::::::::::::::::#@@@@
// @@@=:::::::::::::::::::::::::::::@@@#::::::::-==+=-:::::::::::::::::::::::::::::::::::::::::::::=@@@@
// @@@::::::::::::::::::::::::::::::@@@#:::::::::::::::::::::::::::::::::::::::::::::::::--:::::::::@@@@
// @@*::::::::::::::::::::::::::::::@@@#:::::::::+######*:::::::::::::::::::::::::::::::+@=:::::::::#@@@
// @@*::::::::::::::::::::::::::::::@@@#:::--::::===+*+==:::::=-:::::::=-::::::-::::::::+@=:::::::::+@@@
// @@:::::::::::::::::::::::::::::::@@@#::+@#@@::-#@@%@%=::=@@@@+:::+@@@@@+-::@@@@%*-::#@@%#:::::::::@@@
// @@:::::::::::::::::+*##%%#*=-::::@@@#::+@@=::-%@:::-*@=:%@-:=:::*@*:::-@*::@@-:=@%::=*@*=:::::::::@@@
// @@::::::::::::::+%@@@@@@@@@@@%*-:@@@#::+@*:::*@@@@@@@@#:+@%*-:::@@@@@@@@@-:@%:::#@:::+@=::::::::::@@@
// @%::::::::::::*@@@@@@@@@@@@@@@@@#+@@#::+@+:::+@=-------:::=%@*::@%-------::@%:::#@:::+@=::::::::::%@@
// @%::::::::::-@@@@@@++-::::-=#@@@@@+*#::+@+:::-@%=-:+%%-:-=::#@::*@*-::+%=::@%:::#@:::+@=::::::::::#@@
// @%:::::::::=@@@@%+::::::::::::#@@@@**::+@+::::-#@@@@%=::#@%%@+:::+@@@@@*:::@%:::#@:::+@=::::::::::%@@
// @%::::::::-@@@@+:::::::::::::::-@@@@+:::-::::::::--:::::::=-:::::::-=-:::::--:::-=----=-::::::::::%@@
// @@:::::::-@@@@*:::::::::::::::::-%%%*:::::::::::::::::::::::::::::::::::::::::::+%%%%%%#::::::::::@@@
// @@:::::::*@@@#:::::::+#@%*-::::::=+*#*=+#*=-:::::+#%%#-::::::=#%%#=::::-#@@#-::::=##%#+-::::::::::@@@
// @@-::::::@@@@-:::::-%@#+*%@*:::::%@#+#@@+*@*::::%@+==#@*::::%@#+=%@*-::%@-=*=:::*@*+=+@%-::::::::-@@@
// @@+:::::-@@@@::::::#@=::::#@=::::%@::=@*::#@-::*@=::::-@+::*@*::::*@*::*@*-::::-@@####%@#::::::::*@@@
// @@#:::::-@@@*::::::@@:::::=@*::::%@::=@*::#@-::%@::::::@*::%@-::::-@%:::=%@%-::*@#******+::::::::*@@@
// @@@-::::-@@@%::::::#@+::::%@-::::%@::=@*::#@-::@@-::::=@-::*@*::::*@*:::-::@%::=@*::::-=-::::::::@@@@
// @@@+:::::@@@@-::::::%@%*#@@+:::::%@::=@*::#@-::@@@#=+#@+::::*@%*+@@*:::#@*+@*:::*@%*+%@%::::::::-@@@@
// @@@#:::::#@@@+:::::::=*##+-::::::+*::-*=::+*:::@%=*%#*-::::::=*%#*-:::::=*#+:::::-*##*=--=::::::*@@@@
// @@@@-::::=@@@@-::::::::::::::::::=**+::::::::::@%:::::::::::::::::::::::-==++**##%%@@@@@@@-::::-@@@@@
// @@@@*:::::*@@@@-::::::::::::::::+@@@#::::::::::::::::::::-==++**##=-#@@@@@@@@@@@@@@@@@@@@@=::::*@@@@@
// @@@@@-:::::#@@@@+-:::::::::::::*@@@@::::::-==++--##%@@@@@@@@@#*#@@%%@@@@@@@@@@@#@@@@@*#@@@+:::-@@@@@@
// @@@@@%::::::*@@@@@+-:::::::::+@@@@%:=@@@@@@@@@@==@@@**%@@@@*-#@*-@@*@@@*-+**@@@:%@@==@++@@*:::%@@@@@@
// @@@@@@*::::::=@@@@@@@%%**##@@@@@@=::=@@@@#-:--=-=@@=+@+=@@@:%@@@=+@-*@*-@@@+:@@=#@:*@@%-@@#::+@@@@@@@
// @@@@@@@-:::::::+%@@@@@@@@@@@@@@#-::::@@@+-%@@@*--@@:@@=*@@@%=*%@@@@*=@-*@@@*:+@=+=#@@@%:@@%:-@@@@@@@@
// @@@@@@@@-::::::::=*%@@@@@@@##-:::::::@@@:#@@@@@*:@@:%==@@@@@@%+-%@@#-@*+@@@:+:@*:*@@@@@:%@@-@@@@@@@@@
// @@@@@@@@#-:::::::::::::-:::::::::::::@@%:%@@@@@*-@@-#@@@#-@*%@@@-#@@:@@#=+:%@-#@:@@@@@@*=@@%@@@@@@@@@
// @@@@@@@@@*:::::::::::::::::::::::::::#@@--@@@@%:*@@%*+*=*@@=+@@@=+@@-#@@@@@@@=#@#@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@*-:::::::::::::::::::::::::#@@@+-==-:#@@@@@@@@@@@@#++*#@@@@@@@#@@@%:@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@*-::::::::::::::::::::::::+@@@@@%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@-=-:+**++==--::%@@@@@@@@@@@@
// @@@@@@@@@@@@#-:::::::::::::::::::::::=@@@@@@@@@@@@@@@@@@@@@%%##**++=---::::::::::::::::%@@@@@@@@@@@@@
// @@@@@@@@@@@@@@-::::::::::::::::::::::-@@@@@@%###**++=--::::::::::::::::::::::::::::::-%@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@+::::::::::::::::::::::--::::::::::::::::::::::::::::::::::::::::::::+@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@%-::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::-%@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@+-:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::+@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@%=::::::::::::::::::::::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@%=::::::::::::::::::::::::::::::::::::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@%=-:::::::::::::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@*-:::::::::::::::::::::::::::::::::::::::::::::#@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@%+-:::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%*=-:::::::::::::::::::::::::::::::+#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%**=-:::::::::::::::::::::=+#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%**++=====+++*##@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
-->
<!--
  TOURNIQUET PCdlabs: projects as a 3D carousel, a grid or a list, with an
  administration (developer mode) for cards, media, texts, links, sections,
  sizes, spacing, fonts, colours, footer logo, slogan and QR code.

  INSTALL
  1. Upload the "showcase" folder to https://pcdlabs.presentcomposedesign.fr/showcase/
     (api/, data/, media/). When updating later, NEVER re-upload data/ nor
     api/config.local.php: they hold the published content and the secrets.
  2. Paste this whole file into an Elementor "HTML" widget.
  3. Open ".dev" in the footer (or add #dev to the page address), enter the
     access code, edit, then "Publier".

  ATTRIBUTES of <section class="pcdc">
  - data-media-base : folder that relative media paths are resolved against.
  - data-api        : the administration endpoint. Empty = no server: the
                      block shows its own JSON below, and the developer mode
                      exports a JSON to paste back here.
  Everything else (cards, sections, sizes, footer) is in the JSON
  configuration, edited from the developer mode. A URL parameter can force
  the view: ?vue=carrousel | grille | liste
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@300;400;500;600;700;800;900&display=swap">

<section class="pcdc"
  data-media-base="https://pcdlabs.presentcomposedesign.fr/showcase/media/"
  data-api="https://pcdlabs.presentcomposedesign.fr/showcase/api/showcase.php"
  aria-label="Applications et expériences PCdlabs">

<style>
/* ------------------------------------------------------------------
   Scope: every rule starts with .pcdc so the block never leaks into the
   theme, and the few element resets below shield it from Elementor's
   global button / link / image styles.
   ------------------------------------------------------------------ */
/* Theme tokens. Dark is the default; light comes from data-theme="light",
   or from data-theme="auto" when the browser prefers a light scheme. */
.pcdc {
  --pcdc-blue: #1001ef;
  --pcdc-blue-2: #4a5bff;
  --pcdc-ink: #f4f6ff;
  --pcdc-title: #fff;
  --pcdc-muted: rgba(244, 246, 255, .62);
  --pcdc-line: rgba(255, 255, 255, .12);
  --pcdc-bg: #06070f;
  --pcdc-halo: rgba(16, 1, 239, .42);
  --pcdc-halo-2: rgba(124, 139, 255, .14);
  --pcdc-glass: rgba(255, 255, 255, .07);
  --pcdc-glass-line: rgba(255, 255, 255, .16);
  --pcdc-glass-hover: rgba(255, 255, 255, .16);
  --pcdc-dot: rgba(255, 255, 255, .28);
  --pcdc-card-bg-1: #151a36;
  --pcdc-card-bg-2: #06070f;
  --pcdc-shade: 5, 6, 14;
  --pcdc-card-ink: #fff;
  --pcdc-card-text: rgba(244, 246, 255, .84);
  --pcdc-chip: rgba(255, 255, 255, .09);
  --pcdc-chip-line: rgba(255, 255, 255, .14);
  --pcdc-pill: rgba(8, 10, 22, .5);
  --pcdc-dim-color: #04050c;
  --pcdc-tile-shadow: rgba(0, 0, 0, .6);
  --pcdc-row: rgba(255, 255, 255, .045);
  --pcdc-row-hover: rgba(255, 255, 255, .08);
  --pcdc-floor: rgba(16, 1, 239, .55);
  --pcdc-tile-w: .58;              /* glass square width, share of the card (data-tile) */
  --pcdc-tile-cy: .46;             /* its vertical centre, between the pills and the title */
  /* One fluid unit for every text and control outside the cards: it follows
     the width of the block AND the height of the screen, so a full-screen
     tablet, a 1080p monitor or a 4K TV in F11 all get proportionate sizes.
     --pcdc-ts is the global text size chosen in the developer mode. */
  --pcdc-u: calc(clamp(13.5px, min(6px + .7cqi, 2.15vh), 26px) * var(--pcdc-ts, 1));
  --pcdc-pad: calc(clamp(16px, 3.2vw, 44px) * var(--pcdc-kp, 1));
  --pcdc-size: 320px;              /* carousel card size, set by the script */
  --pcdc-persp: 760px;             /* carousel camera distance, set by the script */
  --pcdc-d: calc(var(--pcdc-size) * .028);   /* card thickness */
  --pcdc-view-i: 0;
  container: pcdc / inline-size;
  position: relative;
  display: flex; flex-direction: column;
  box-sizing: border-box;          /* "full screen" height includes the margins */
  isolation: isolate;
  overflow: hidden;
  padding: var(--pcdc-pad);
  border-radius: clamp(18px, 2.4vw, 32px);
  color: var(--pcdc-ink);
  font-family: var(--pcdc-font, "Inter Tight", system-ui, -apple-system, "Segoe UI", sans-serif);
  font-size: 16px;
  line-height: 1.4;
  text-align: left;
  -webkit-font-smoothing: antialiased;
  background:
    radial-gradient(1100px 520px at 50% -12%, var(--pcdc-halo), transparent 62%),
    radial-gradient(700px 380px at 105% 105%, var(--pcdc-halo-2), transparent 70%),
    var(--pcdc-bg);
  transition: background-color .4s, color .4s;
}
/* Transparent block (setting, or data-transparent on a banner block). */
.pcdc.is-transparent { background: transparent; }
.pcdc[data-theme="light"] {
  --pcdc-ink: #0b0e1c;
  --pcdc-title: #0b0e1c;
  --pcdc-muted: rgba(11, 14, 28, .6);
  --pcdc-line: rgba(11, 14, 28, .12);
  --pcdc-bg: #f3f5fc;
  --pcdc-halo: rgba(16, 1, 239, .16);
  --pcdc-halo-2: rgba(124, 139, 255, .16);
  --pcdc-glass: rgba(255, 255, 255, .75);
  --pcdc-glass-line: rgba(11, 14, 28, .1);
  --pcdc-glass-hover: #fff;
  --pcdc-dot: rgba(11, 14, 28, .2);
  --pcdc-card-bg-1: #ffffff;
  --pcdc-card-bg-2: #e6ebfa;
  --pcdc-shade: 246, 248, 255;
  --pcdc-card-ink: #0b0e1c;
  --pcdc-card-text: rgba(11, 14, 28, .78);
  --pcdc-chip: rgba(11, 14, 28, .06);
  --pcdc-chip-line: rgba(11, 14, 28, .12);
  --pcdc-pill: rgba(255, 255, 255, .72);
  --pcdc-dim-color: #eef1fa;
  --pcdc-tile-shadow: rgba(20, 24, 60, .32);
  --pcdc-row: rgba(255, 255, 255, .7);
  --pcdc-row-hover: #fff;
  --pcdc-floor: rgba(16, 1, 239, .22);
}
@media (prefers-color-scheme: light) {
  .pcdc[data-theme="auto"] {
    --pcdc-ink: #0b0e1c;
    --pcdc-title: #0b0e1c;
    --pcdc-muted: rgba(11, 14, 28, .6);
    --pcdc-line: rgba(11, 14, 28, .12);
    --pcdc-bg: #f3f5fc;
    --pcdc-halo: rgba(16, 1, 239, .16);
    --pcdc-halo-2: rgba(124, 139, 255, .16);
    --pcdc-glass: rgba(255, 255, 255, .75);
    --pcdc-glass-line: rgba(11, 14, 28, .1);
    --pcdc-glass-hover: #fff;
    --pcdc-dot: rgba(11, 14, 28, .2);
    --pcdc-card-bg-1: #ffffff;
    --pcdc-card-bg-2: #e6ebfa;
    --pcdc-shade: 246, 248, 255;
    --pcdc-card-ink: #0b0e1c;
    --pcdc-card-text: rgba(11, 14, 28, .78);
    --pcdc-chip: rgba(11, 14, 28, .06);
    --pcdc-chip-line: rgba(11, 14, 28, .12);
    --pcdc-pill: rgba(255, 255, 255, .72);
    --pcdc-dim-color: #eef1fa;
    --pcdc-tile-shadow: rgba(20, 24, 60, .32);
    --pcdc-row: rgba(255, 255, 255, .7);
    --pcdc-row-hover: #fff;
    --pcdc-floor: rgba(16, 1, 239, .22);
  }
}
/* Accent text: lightened accent on dark cards, darkened accent on light cards. */
.pcdc .pcdc-card { --pcdc-accent-ink: var(--pcdc-accent-light); }
.pcdc[data-theme="light"] .pcdc-card { --pcdc-accent-ink: var(--pcdc-accent-deep); }
@media (prefers-color-scheme: light) {
  .pcdc[data-theme="auto"] .pcdc-card { --pcdc-accent-ink: var(--pcdc-accent-deep); }
}
/* Doubled class: beats theme rules such as ".entry-content p" or ".elementor-kit-5 button". */
.pcdc *, .pcdc *::before, .pcdc *::after { box-sizing: border-box; }
.pcdc.pcdc :where(h2, h3, p, ul, li, figure) { margin: 0; padding: 0; font-family: inherit; }
.pcdc.pcdc :where(ul, li) { list-style: none; }
.pcdc.pcdc img, .pcdc.pcdc video {
  display: block; max-width: none; width: 100%; height: 100%;
  border: 0; border-radius: 0; box-shadow: none; margin: 0; padding: 0;
}
.pcdc.pcdc button {
  -webkit-appearance: none; appearance: none; margin: 0; padding: 0; border: 0; border-radius: 0; background: none;
  font: inherit; color: inherit; letter-spacing: inherit; text-transform: none;
  line-height: 1; min-height: 0; box-shadow: none; cursor: pointer;
}
.pcdc svg { display: block; flex: none; }
.pcdc .pcdc-sr {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

/* ---------------- Header + view switch ---------------- */
.pcdc .pcdc-head {
  flex: none;
  display: flex; flex-wrap: wrap; align-items: flex-end;
  justify-content: space-between; gap: 18px 24px;
  margin-bottom: calc(var(--pcdc-u) * 1.7 * var(--pcdc-kh, 1));
}
/* Every text element reads four variables set from the developer mode:
   --pcdc-s-<name> (size factor), --pcdc-f-<name> (font), --pcdc-w-<name>
   (weight) and --pcdc-c-<name> (colour). Unset, the theme decides. */
.pcdc .pcdc-kicker {
  display: flex; align-items: center; gap: .8em;
  font-size: calc(var(--pcdc-u) * .78 * var(--pcdc-s-kicker, 1));
  font-family: var(--pcdc-f-kicker, inherit); font-weight: var(--pcdc-w-kicker, 700);
  letter-spacing: .16em; text-transform: uppercase; color: var(--pcdc-c-kicker, var(--pcdc-muted));
}
.pcdc .pcdc-kicker::before {
  content: ""; width: .62em; height: .62em; border-radius: 50%; flex: none;
  background: var(--pcdc-blue-2); box-shadow: 0 0 12px var(--pcdc-blue-2);
}
.pcdc .pcdc-h {
  margin-top: .22em;
  font-size: calc(var(--pcdc-u) * 2.6 * var(--pcdc-s-title, 1));
  font-family: var(--pcdc-f-title, inherit); font-weight: var(--pcdc-w-title, 800);
  line-height: 1.02; letter-spacing: -.025em; color: var(--pcdc-c-title, var(--pcdc-title));
}
.pcdc .pcdc-kicker[hidden] + .pcdc-h { margin-top: 0; }
/* Centred header: texts centred, the view switch below them. */
.pcdc[data-align="center"] .pcdc-head { flex-direction: column; align-items: center; text-align: center; }
.pcdc[data-align="center"] .pcdc-kicker { justify-content: center; }
.pcdc .pcdc-switch {
  --pcdc-sw: calc(var(--pcdc-u) * var(--pcdc-s-switch, 1));
  position: relative; display: grid; grid-template-columns: repeat(3, 1fr);
  padding: 4px; border-radius: 999px;
  background: var(--pcdc-glass);
  border: 1px solid var(--pcdc-glass-line);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12), 0 12px 30px -18px rgba(0, 0, 0, .6);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
}
.pcdc .pcdc-switch-thumb {
  position: absolute; top: 4px; bottom: 4px; left: 4px;
  width: calc((100% - 8px) / 3); border-radius: 999px;
  background: linear-gradient(135deg, var(--pcdc-blue), var(--pcdc-blue-2));
  box-shadow: 0 6px 20px -6px rgba(16, 1, 239, .9), inset 0 1px 0 rgba(255, 255, 255, .35);
  transform: translateX(calc(100% * var(--pcdc-view-i)));
  transition: transform .45s cubic-bezier(.2, .8, .2, 1);
}
.pcdc button.pcdc-switch-btn {
  position: relative; z-index: 1; display: inline-flex; align-items: center;
  justify-content: center; gap: .5em; min-height: calc(var(--pcdc-sw) * 2.7); padding: 0 1.1em;
  border-radius: 999px; background: transparent; color: var(--pcdc-muted);
  font-size: calc(var(--pcdc-sw) * .92); font-weight: 600; transition: color .3s;
}
.pcdc button.pcdc-switch-btn svg { width: 1.25em; height: 1.25em; }
.pcdc button.pcdc-switch-btn:hover { background: transparent; color: var(--pcdc-title); }
.pcdc.pcdc button.pcdc-switch-btn[aria-pressed="true"]:hover { color: #fff; }
.pcdc button.pcdc-switch-btn[aria-pressed="true"] { color: #fff; }
.pcdc button.pcdc-switch-btn:focus-visible { outline: 2px solid var(--pcdc-title); outline-offset: 2px; }
@container pcdc (max-width: 560px) {
  .pcdc .pcdc-switch { width: 100%; }
  .pcdc .pcdc-switch-label { font-size: .92em; }
}

/* ---------------- Card anatomy (shared by every view) ---------------- */
.pcdc .pcdc-card { position: relative; }
.pcdc .pcdc-face {
  position: absolute; inset: 0; overflow: hidden; isolation: isolate;
  border-radius: 7.5%;
  -webkit-backface-visibility: hidden; backface-visibility: hidden;
  container: pcdc-face / inline-size;
  background: var(--pcdc-card-bg-2);
}
/* Everything inside a face is sized in em, and 1em follows the card width,
   so a 1024 px card and a 240 px card look the same, only smaller. */
.pcdc .pcdc-in { position: absolute; inset: 0; font-size: max(9px, calc(4.6cqi * var(--pcdc-ct, 1))); }

/* Iridescent rim, as on the BirdSignal cards. */
.pcdc .pcdc-face::before {
  content: ""; position: absolute; inset: 0; z-index: 8; pointer-events: none;
  border-radius: inherit; padding: 1.5px;
  background: linear-gradient(135deg, rgba(255,255,255,.55), rgba(180,196,255,.28) 50%, rgba(230,168,255,.4));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
}
/* Depth dimming (carousel): cards further back get darker. */
.pcdc .pcdc-in::after {
  content: ""; position: absolute; inset: 0; z-index: 7; pointer-events: none;
  background: var(--pcdc-dim-color); opacity: var(--pcdc-dim, 0);
}
/* Moving sheen: follows the card angle in the carousel, sweeps on hover elsewhere. */
.pcdc .pcdc-in::before {
  content: ""; position: absolute; inset: 0; z-index: 6; pointer-events: none;
  background: linear-gradient(112deg, transparent 32%, rgba(255,255,255,.13) 46%, rgba(255,255,255,.02) 54%, transparent 64%);
  background-size: 260% 100%;
  background-position: calc(var(--pcdc-sheen, -40) * 1%) 0;
  transition: background-position 1.1s cubic-bezier(.2, .8, .2, 1);
}

/* Featured media ("image à la une"): image, GIF or video, cropped square. */
.pcdc .pcdc-bg {
  position: absolute; inset: 0; overflow: hidden;
  background:
    radial-gradient(110% 85% at 18% 0%, var(--pcdc-accent-soft), transparent 62%),
    radial-gradient(90% 70% at 100% 100%, rgba(16, 1, 239, .35), transparent 70%),
    linear-gradient(160deg, var(--pcdc-card-bg-1), var(--pcdc-card-bg-2));
}
.pcdc .pcdc-bg-media { position: absolute; inset: 0; object-fit: cover; }
.pcdc .pcdc-bg.is-blurred .pcdc-bg-media { filter: blur(1.2em) saturate(1.35); transform: scale(1.3); opacity: .8; }
.pcdc .pcdc-bg.is-icon .pcdc-bg-media {
  object-fit: contain; filter: blur(2.4em) saturate(1.5);
  transform: scale(1.35) translateY(-14%); opacity: .5;
}
.pcdc .pcdc-shade {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(var(--pcdc-shade), .35) 0%, rgba(var(--pcdc-shade), 0) 26%, rgba(var(--pcdc-shade), .3) 60%, rgba(var(--pcdc-shade), .94) 100%);
}
.pcdc .pcdc-shade::after {
  content: ""; position: absolute; inset: 0; opacity: 0;
  background: linear-gradient(180deg, rgba(var(--pcdc-shade), .25), rgba(var(--pcdc-shade), .8) 55%, rgba(var(--pcdc-shade), .96));
  transition: opacity .45s;
}

/* Top row: index + kind of entry. */
.pcdc .pcdc-top {
  position: absolute; z-index: 3; top: 5.5%; left: 6%; right: 6%;
  display: flex; justify-content: space-between; align-items: center;
  pointer-events: none;
}
.pcdc .pcdc-pill {
  font-size: .54em; font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
  padding: .55em .9em; border-radius: 99em; color: var(--pcdc-card-ink);
  background: var(--pcdc-pill); border: 1px solid var(--pcdc-chip-line);
}
.pcdc .pcdc-pill.is-app { color: #fff; background: rgba(16, 1, 239, .7); border-color: rgba(140, 150, 255, .5); }
/* Card elements switched off in the developer mode. */
.pcdc.no-card-num .pcdc-pill.is-num,
.pcdc.no-card-badge .pcdc-pill.is-badge,
.pcdc.no-card-kind .pcdc-kind,
.pcdc.no-card-desc .pcdc-desc,
.pcdc.no-card-tags .pcdc-tags { display: none; }
.pcdc.no-card-num .pcdc-top { justify-content: flex-end; }

/* The glass tile: app icon, or the demo GIF / video when there is no icon.
   No backdrop-filter here (unreliable inside 3D transforms): the frosted look
   comes from a blurred copy of the featured image, aligned under the tile. */
/* Position and size come from two numbers, so the square stays centred
   whatever its size: --pcdc-tile-w (share of the card width) and
   --pcdc-tile-cy (vertical centre, midway between the pills and the title). */
.pcdc .pcdc-tile {
  --pcdc-tl: calc((1 - var(--pcdc-tile-w)) / 2);
  --pcdc-tt: calc(var(--pcdc-tile-cy) - var(--pcdc-tile-w) / 2);
  position: absolute; z-index: 2; aspect-ratio: 1;
  left: calc(var(--pcdc-tl) * 100%); top: calc(var(--pcdc-tt) * 100%);
  width: calc(var(--pcdc-tile-w) * 100%);
  border-radius: 23%; overflow: hidden;
  transform-origin: 50% 0;
  box-shadow:
    0 1.1em 2.2em -.5em var(--pcdc-tile-shadow),
    0 .35em .7em -.2em rgba(0, 0, 0, .35),
    0 0 2.6em -.6em var(--pcdc-accent-glow);
  transition: transform .55s cubic-bezier(.2, .8, .2, 1);
}
.pcdc .pcdc-tile-blur {
  /* Blurred copy of the card background, sized and shifted to line up with it. */
  position: absolute;
  left: calc(-100% * var(--pcdc-tl) / var(--pcdc-tile-w));
  top: calc(-100% * var(--pcdc-tt) / var(--pcdc-tile-w));
  width: calc(100% / var(--pcdc-tile-w)); height: calc(100% / var(--pcdc-tile-w));
  background-image: var(--pcdc-still, none), radial-gradient(80% 70% at 30% 20%, var(--pcdc-accent-soft), transparent 70%);
  background-color: #1a1f3f;
  background-size: cover; background-position: center;
  filter: blur(.75em) saturate(1.6) brightness(1.08);
}
.pcdc .pcdc-tile::before {  /* frosted tint */
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(150deg, rgba(255,255,255,.38), rgba(255,255,255,.1) 42%, rgba(255,255,255,.06) 60%, rgba(255,255,255,.18));
}
.pcdc .pcdc-tile::after {   /* bevel, rim light and specular spot: the relief */
  content: ""; position: absolute; inset: 0; z-index: 3; pointer-events: none;
  border-radius: inherit;
  border: 1px solid rgba(255, 255, 255, .5);
  background: radial-gradient(60% 40% at 22% 8%, rgba(255,255,255,.55), transparent 70%);
  box-shadow:
    inset 0 .12em .1em rgba(255, 255, 255, .6),
    inset 0 -.25em .5em rgba(0, 0, 0, .28),
    inset .2em 0 .45em rgba(255, 255, 255, .12);
  mix-blend-mode: screen;
}
.pcdc .pcdc-tile-content { position: absolute; z-index: 2; }
.pcdc .pcdc-tile-content.is-icon { inset: 17%; filter: drop-shadow(0 .35em .45em rgba(0, 0, 0, .38)); }
.pcdc .pcdc-tile-content.is-icon img { object-fit: contain; border-radius: 18%; }
.pcdc .pcdc-tile-content.is-media {
  inset: 6.5%; border-radius: 18%; overflow: hidden;
  box-shadow: 0 .2em .6em rgba(0, 0, 0, .45);
}
.pcdc .pcdc-tile-content.is-media :is(img, video) { object-fit: cover; }
.pcdc .pcdc-tile-content.is-letter {
  inset: 0; display: grid; place-items: center;
  font-size: 3.2em; font-weight: 800; color: #fff;
}

/* Bottom stack: kind, title, then (on hover) description, tags and CTAs. */
.pcdc .pcdc-bottom {
  position: absolute; z-index: 4; left: 6.5%; right: 6.5%; bottom: 6%;
  display: flex; flex-direction: column; pointer-events: none;
}
.pcdc .pcdc-kind {
  font-size: .62em; font-weight: 700; letter-spacing: .15em; text-transform: uppercase;
  color: var(--pcdc-accent-ink);
}
.pcdc h3.pcdc-title {
  margin-top: .22em; font-family: inherit;
  font-size: 1.55em; font-weight: 800; line-height: 1.02; letter-spacing: -.02em;
  color: var(--pcdc-card-ink); text-wrap: balance;
}
.pcdc .pcdc-more, .pcdc .pcdc-actions-wrap {
  display: grid; grid-template-rows: 0fr; opacity: 0;
  transition: grid-template-rows .5s cubic-bezier(.2, .8, .2, 1), opacity .35s;
}
.pcdc .pcdc-more > div, .pcdc .pcdc-actions-wrap > div { min-height: 0; overflow: hidden; }
.pcdc .pcdc-desc {
  margin-top: .75em; font-size: max(11px, .76em); line-height: 1.42; color: var(--pcdc-card-text);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.pcdc .pcdc-tags { display: flex; flex-wrap: wrap; gap: .3em; margin-top: .6em; }
.pcdc .pcdc-tags li {
  font-size: .56em; font-weight: 600; letter-spacing: .02em;
  padding: .4em .75em; border-radius: 99em; color: var(--pcdc-card-text);
  background: var(--pcdc-chip); border: 1px solid var(--pcdc-chip-line);
}
.pcdc .pcdc-actions { display: flex; flex-wrap: wrap; gap: .4em; padding-top: .8em; }
.pcdc a.pcdc-cta, .pcdc a.pcdc-cta:visited {
  position: relative; z-index: 5; pointer-events: auto;
  display: inline-flex; align-items: center; gap: .5em;
  font-size: .7em; font-weight: 700; line-height: 1; letter-spacing: 0;
  padding: .85em 1.2em; border-radius: 99em; text-decoration: none; white-space: nowrap;
  transition: transform .25s, box-shadow .25s, background .25s;
}
.pcdc a.pcdc-cta svg { width: 1.05em; height: 1.05em; }
.pcdc a.pcdc-cta.is-primary, .pcdc a.pcdc-cta.is-primary:visited {
  color: #fff; background: linear-gradient(135deg, var(--pcdc-blue), var(--pcdc-blue-2));
  box-shadow: 0 .6em 1.4em -.4em rgba(16, 1, 239, .95), inset 0 1px 0 rgba(255, 255, 255, .35);
}
.pcdc a.pcdc-cta.is-secondary, .pcdc a.pcdc-cta.is-secondary:visited {
  color: var(--pcdc-card-ink); background: var(--pcdc-chip);
  border: 1px solid var(--pcdc-chip-line);
}
.pcdc a.pcdc-cta:hover, .pcdc a.pcdc-cta:focus-visible { transform: translateY(-1px); text-decoration: none; }
.pcdc a.pcdc-cta.is-primary:is(:hover, :focus-visible) { color: #fff; }
.pcdc a.pcdc-cta.is-secondary:is(:hover, :focus-visible) { color: var(--pcdc-card-ink); }
.pcdc a.pcdc-cta.is-primary:hover { box-shadow: 0 .8em 1.8em -.4em rgba(16, 1, 239, 1), inset 0 1px 0 rgba(255, 255, 255, .45); }
.pcdc a.pcdc-cta:focus-visible { outline: 2px solid var(--pcdc-card-ink); outline-offset: 2px; }

/* The whole card is a link (the "hit" layer); the CTAs sit above it. */
.pcdc a.pcdc-hit { position: absolute; inset: 0; z-index: 3; border-radius: inherit; }

/* Revealed state: mouse hover, keyboard focus, or a first tap on touch screens. */
/* Revealed: the square shrinks to 32 % of the card and moves up to 8 % from the top. */
.pcdc .pcdc-card:is(.is-hover, .is-open, :focus-within) .pcdc-tile {
  transform: translateY(calc((.08 - var(--pcdc-tt)) / var(--pcdc-tile-w) * 100%)) scale(calc(.32 / var(--pcdc-tile-w)));
}
.pcdc .pcdc-card:is(.is-hover, .is-open, :focus-within) .pcdc-shade::after { opacity: 1; }
.pcdc .pcdc-card:is(.is-hover, .is-open, :focus-within) :is(.pcdc-more, .pcdc-actions-wrap) { grid-template-rows: 1fr; opacity: 1; }
.pcdc:not([data-view="carousel"]) .pcdc-card:is(.is-hover, .is-open) { --pcdc-sheen: 140; }

/* Small cards: keep the essentials. */
@container pcdc-face (max-width: 300px) {
  .pcdc .pcdc-tags { display: none; }
  .pcdc .pcdc-desc { -webkit-line-clamp: 2; }
}

/* Back face and edges: only visible in the 3D carousel. */
.pcdc .pcdc-back {
  background:
    radial-gradient(100% 80% at 80% 0%, var(--pcdc-accent-soft), transparent 65%),
    linear-gradient(200deg, var(--pcdc-card-bg-1), var(--pcdc-card-bg-2));
}
.pcdc img.pcdc-back-mark {
  position: absolute; left: 20%; top: 16%; width: 60%; height: 60%;
  object-fit: contain; opacity: .22; filter: saturate(1.6) blur(.02em);
}
.pcdc img.pcdc-back-mark.is-photo { left: 0; top: 0; width: 100%; height: 100%; object-fit: cover; opacity: .28; filter: blur(.5em) saturate(1.4); }
.pcdc .pcdc-back-text { position: absolute; left: 7%; right: 7%; bottom: 7%; }
.pcdc .pcdc-back-text strong { display: block; font-size: 1.3em; font-weight: 800; color: var(--pcdc-card-ink); letter-spacing: -.02em; }
.pcdc .pcdc-back-text span { display: block; margin-top: .5em; font-size: .5em; font-weight: 700; letter-spacing: .16em; text-transform: uppercase; color: var(--pcdc-muted); }
.pcdc .pcdc-edge {
  position: absolute; display: block; pointer-events: none;
  background: linear-gradient(180deg, #eef1ff, #9aa6ff 35%, #d8aaff 66%, #ffd9a3);
  opacity: calc(1 - var(--pcdc-dim, 0) * 1.1);
}
.pcdc .pcdc-edge-l { top: 7%; bottom: 7%; left: calc(var(--pcdc-d) / -2); width: var(--pcdc-d); transform: rotateY(-90deg); filter: brightness(.7); }
.pcdc .pcdc-edge-r { top: 7%; bottom: 7%; right: calc(var(--pcdc-d) / -2); width: var(--pcdc-d); transform: rotateY(90deg); }
.pcdc .pcdc-edge-t { left: 7%; right: 7%; top: calc(var(--pcdc-d) / -2); height: var(--pcdc-d); transform: rotateX(90deg); filter: brightness(1.1); }
.pcdc:not([data-view="carousel"]) :is(.pcdc-back, .pcdc-edge) { display: none; }

/* ---------------- View 1: the 3D carousel ("tourniquet") ---------------- */
.pcdc .pcdc-views { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.pcdc[data-view="carousel"] .pcdc-stage {
  position: relative; flex: none; height: calc(var(--pcdc-size) * 1.32);
  margin-inline: calc(var(--pcdc-pad) * -1);
  perspective: var(--pcdc-persp); perspective-origin: 50% 38%;
  overflow: hidden; touch-action: pan-y; cursor: grab;
  -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent;
  outline: none;
}
/* Keyboard focus only: a thin rounded ring inside the stage, never a frame on click. */
.pcdc[data-view="carousel"] .pcdc-stage:focus { outline: none; }
.pcdc[data-view="carousel"] .pcdc-stage:focus-visible { outline: 2px solid rgba(74, 91, 255, .6); outline-offset: -10px; border-radius: 28px; }
.pcdc[data-view="carousel"] .pcdc-stage::after {   /* soft glow on the "floor" */
  content: ""; position: absolute; left: 50%; bottom: 2%; width: calc(var(--pcdc-size) * 2.6); height: calc(var(--pcdc-size) * .34);
  transform: translateX(-50%); border-radius: 50%; pointer-events: none;
  background: radial-gradient(closest-side, var(--pcdc-floor), transparent);
  filter: blur(18px);
}
.pcdc.is-dragging .pcdc-stage { cursor: grabbing; }
/* Full-screen mode: the block is as tall as the screen and the ring takes
   whatever height the header, the controls and the footer leave; the script
   then sizes the cards from that height (no fixed maximum any more). */
/* --pcdc-wrap: the space the page puts around the block (measured by the
   script), so that the whole section, margins included, fills the screen. */
.pcdc.is-fill {
  min-height: calc(100vh * var(--pcdc-hf, 1) - var(--pcdc-wrap, 0px));
  min-height: calc(100svh * var(--pcdc-hf, 1) - var(--pcdc-wrap, 0px));
}
/* In an Elementor container set to "wrap" (often on mobile), the widget that
   holds the block is first measured at its natural width; the block, being a
   size container, has none, so it was measured too narrow, hence too tall,
   and a gap was left under it. A full width removes the gap; Elementor's own
   width settings, more specific, still win. */
:where(.elementor-widget):has(> .elementor-widget-container > .pcdc, > .pcdc) { width: 100%; }
.pcdc.is-fill[data-view="carousel"] .pcdc-stage { flex: 1 1 0; height: auto; min-height: 220px; }
/* On a phone the card matters more than seeing the footer without scrolling. */
@container pcdc (max-width: 560px) { .pcdc.is-fill[data-view="carousel"] .pcdc-stage { min-height: 84cqi; } }
.pcdc[data-view="carousel"] .pcdc-track {
  /* Centre of the ring: the engine lifts the front card a little, so the
     centre sits a little lower, plus the offset chosen in the developer mode. */
  position: absolute; z-index: 1; left: 50%; width: 0; height: 0;
  top: calc(50% + var(--pcdc-size) * (.05 + var(--pcdc-off, 0)));
  transform-style: preserve-3d;
}
.pcdc[data-view="carousel"] .pcdc-card {
  position: absolute; width: var(--pcdc-size); height: var(--pcdc-size);
  left: calc(var(--pcdc-size) / -2); top: calc(var(--pcdc-size) / -2);
  transform-style: preserve-3d; will-change: transform; cursor: pointer;
}
.pcdc[data-view="carousel"] .pcdc-front { transform: translateZ(calc(var(--pcdc-d) / 2)); }
.pcdc[data-view="carousel"] .pcdc-back { transform: rotateY(180deg) translateZ(calc(var(--pcdc-d) / 2)); }
.pcdc[data-view="carousel"] .pcdc-card:not(.is-front) :is(.pcdc-cta) { pointer-events: none; }

.pcdc .pcdc-nav {
  flex: none;
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: calc(var(--pcdc-u) * .9);
  width: min(100%, calc(var(--pcdc-u) * 30)); margin: calc(var(--pcdc-u) * .7 * var(--pcdc-kn, 1)) auto 0;
}
.pcdc .pcdc-nav.no-arrows { grid-template-columns: minmax(0, 1fr); }
.pcdc .pcdc-nav.no-caption { grid-template-columns: auto auto; justify-content: center; gap: calc(var(--pcdc-u) * 2); }
.pcdc:not([data-view="carousel"]) :is(.pcdc-nav, .pcdc-dots, .pcdc-hint) { display: none; }
.pcdc button.pcdc-arrow {
  --pcdc-ar: calc(var(--pcdc-u) * 3 * var(--pcdc-s-arrows, 1));
  display: grid; place-items: center; width: var(--pcdc-ar); height: var(--pcdc-ar); border-radius: 50%;
  color: var(--pcdc-title); background: var(--pcdc-glass);
  border: 1px solid var(--pcdc-glass-line);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18), 0 10px 24px -14px rgba(0, 0, 0, .6);
  transition: background .25s, transform .25s;
}
.pcdc button.pcdc-arrow:hover { background: var(--pcdc-glass-hover); color: var(--pcdc-title); }
.pcdc button.pcdc-arrow:active { transform: scale(.94); }
.pcdc button.pcdc-arrow:focus-visible { outline: 2px solid var(--pcdc-title); outline-offset: 2px; }
.pcdc button.pcdc-arrow svg { width: 42%; height: 42%; }
.pcdc .pcdc-caption {
  --pcdc-cp: calc(var(--pcdc-u) * var(--pcdc-s-caption, 1));
  text-align: center; min-width: 0; font-family: var(--pcdc-f-caption, inherit);
}
.pcdc .pcdc-count { font-size: calc(var(--pcdc-cp) * .74); font-weight: 700; letter-spacing: .16em; color: var(--pcdc-muted); }
.pcdc .pcdc-cap-title {
  display: block; margin-top: .15em; font-size: calc(var(--pcdc-cp) * 1.4); line-height: 1.15;
  font-weight: var(--pcdc-w-caption, 800); color: var(--pcdc-c-caption, var(--pcdc-title)); letter-spacing: -.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pcdc .pcdc-cap-kind { display: block; margin-top: .1em; font-size: calc(var(--pcdc-cp) * .95); color: var(--pcdc-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pcdc .pcdc-dots {
  --pcdc-dt: calc(var(--pcdc-u) * .42 * var(--pcdc-s-dots, 1));
  flex: none; display: flex; justify-content: center; flex-wrap: wrap; gap: 0; margin-top: calc(var(--pcdc-u) * .5);
}
.pcdc button.pcdc-dot { width: calc(var(--pcdc-dt) * 3.4); height: calc(var(--pcdc-dt) * 3.4); display: grid; place-items: center; background: transparent; }
.pcdc button.pcdc-dot:hover { background: transparent; }
.pcdc button.pcdc-dot::before {
  content: ""; width: var(--pcdc-dt); height: var(--pcdc-dt); border-radius: 99px; background: var(--pcdc-dot);
  transition: width .35s, background .35s;
}
.pcdc button.pcdc-dot[aria-current="true"]::before { width: calc(var(--pcdc-dt) * 2.8); background: var(--pcdc-title); }
.pcdc button.pcdc-dot:focus-visible { outline: 2px solid var(--pcdc-title); outline-offset: 0; border-radius: 6px; }
.pcdc .pcdc-hint {
  flex: none; margin-top: calc(var(--pcdc-u) * .5); text-align: center;
  font-size: calc(var(--pcdc-u) * .82 * var(--pcdc-s-hint, 1));
  font-family: var(--pcdc-f-hint, inherit); font-weight: var(--pcdc-w-hint, 400); color: var(--pcdc-c-hint, var(--pcdc-muted));
  transition: opacity .6s;
}
.pcdc.has-interacted .pcdc-hint { opacity: 0; }

/* ---------------- View 2: grid ---------------- */
/* 3 columns on a desktop or a landscape tablet, 2 on a portrait tablet,
   1 on a phone held upright (widths of the block itself, not of the screen). */
.pcdc[data-view="grid"] .pcdc-track {
  display: grid; gap: clamp(12px, 1.8vw, 22px);
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
@container pcdc (max-width: 900px) { .pcdc[data-view="grid"] .pcdc-track { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@container pcdc (max-width: 560px) { .pcdc[data-view="grid"] .pcdc-track { grid-template-columns: minmax(0, 1fr); } }
.pcdc[data-view="grid"] .pcdc-card {
  aspect-ratio: 1; transition: transform .45s cubic-bezier(.2, .8, .2, 1);
}
.pcdc[data-view="grid"] .pcdc-card.is-hover { transform: translateY(-4px); }
.pcdc[data-view="grid"] .pcdc-face { box-shadow: 0 24px 50px -30px #000; }

/* ---------------- View 3: list (fastest access on a stand) ---------------- */
.pcdc[data-view="list"] .pcdc-track { display: flex; flex-direction: column; gap: calc(var(--pcdc-u) * .6); }
.pcdc[data-view="list"] .pcdc-face {
  position: relative; inset: auto; border-radius: 18px; container-type: normal;
  background: var(--pcdc-row);
}
.pcdc[data-view="list"] .pcdc-face::before { padding: 1px; opacity: .6; }
.pcdc[data-view="list"] .pcdc-in {
  position: relative; inset: auto; font-size: var(--pcdc-u);
  display: grid; grid-template-columns: 4.3em 1fr; align-items: center; gap: 1em;
  padding: .75em 1em .75em .75em;
}
.pcdc[data-view="list"] :is(.pcdc-bg, .pcdc-shade, .pcdc-top) { display: none; }
.pcdc[data-view="list"] .pcdc-card.is-hover .pcdc-face { background: var(--pcdc-row-hover); }
.pcdc[data-view="list"] .pcdc-tile {
  position: relative; left: auto; top: auto; width: 4.3em; font-size: .44em;
  transform: none !important; border-radius: 1.1em;
}
.pcdc[data-view="list"] .pcdc-tile-blur { left: 0; top: 0; width: 100%; height: 100%; background-image: radial-gradient(80% 70% at 30% 20%, var(--pcdc-accent-soft), transparent 70%); }
.pcdc[data-view="list"] .pcdc-bottom {
  position: static; display: grid; grid-template-columns: 1fr; gap: 10px; align-items: center;
}
.pcdc[data-view="list"] .pcdc-kind { font-size: .72em; }
.pcdc[data-view="list"] h3.pcdc-title { font-size: 1.3em; margin-top: 2px; }
.pcdc[data-view="list"] :is(.pcdc-more, .pcdc-actions-wrap) { grid-template-rows: 1fr; opacity: 1; }
.pcdc[data-view="list"] .pcdc-desc { margin-top: 4px; font-size: .92em; -webkit-line-clamp: 2; }
.pcdc[data-view="list"] .pcdc-tags { display: none; }
.pcdc[data-view="list"] .pcdc-actions { padding-top: 0; }
.pcdc[data-view="list"] a.pcdc-cta { font-size: .86em; padding: .85em 1.1em; }
@container pcdc (min-width: 760px) {
  .pcdc[data-view="list"] .pcdc-bottom { grid-template-columns: 1fr auto; gap: 24px; }
  .pcdc[data-view="list"] .pcdc-tags { display: flex; }
  .pcdc[data-view="list"] .pcdc-tags li { font-size: .72em; }
}

/* View change: short fade. */
.pcdc .pcdc-views.is-entering { animation: pcdc-fade .5s cubic-bezier(.2, .8, .2, 1); }
@keyframes pcdc-fade { from { opacity: 0; } to { opacity: 1; } }

/* ---------------- Footer: logo, texts, slogan, QR code ---------------- */
.pcdc { --pcdc-logo: #fff; }
.pcdc[data-theme="light"] { --pcdc-logo: var(--pcdc-blue); }
@media (prefers-color-scheme: light) { .pcdc[data-theme="auto"] { --pcdc-logo: var(--pcdc-blue); } }

.pcdc .pcdc-foot {
  /* QR card width: large by default, it is meant to be scanned from a few steps away. */
  --pcdc-qr-w: calc(var(--pcdc-u) * 9.5 * var(--pcdc-s-qr, 1));
  flex: none; position: relative;
  /* Two equal side columns: the middle one is centred on the block, whatever
     the logo on the left or the QR code on the right weigh. */
  display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center;
  gap: calc(var(--pcdc-u) * .8) calc(var(--pcdc-u) * 1.4);
  margin-top: calc(var(--pcdc-u) * 1.6 * var(--pcdc-kf, 1));
  padding-top: calc(var(--pcdc-u) * .9);
  border-top: 1px solid var(--pcdc-line);
  font-size: var(--pcdc-u); line-height: 1.3; color: var(--pcdc-muted);
}
.pcdc.no-foot-line .pcdc-foot { border-top-color: transparent; }
.pcdc .pcdc-foot:first-child { margin-top: 0; }
/* Room above the footer line for the part of the QR card that sticks out
   (measured by the script: --pcdc-qr-band). */
.pcdc.has-qr .pcdc-foot { margin-top: max(calc(var(--pcdc-u) * 1.6 * var(--pcdc-kf, 1)), var(--pcdc-qr-band, 0px)); }
.pcdc .pcdc-foot a, .pcdc .pcdc-foot a:visited { color: inherit; text-decoration: none; }
.pcdc .pcdc-foot a:hover { text-decoration: underline; }

.pcdc .pcdc-foot-left { grid-column: 1; display: flex; flex-wrap: wrap; align-items: center; gap: .5em 1em; min-width: 0; }
.pcdc .pcdc-brand { --pcdc-lh: calc(var(--pcdc-u) * 3.2 * var(--pcdc-s-logo, 1)); }
.pcdc .pcdc-foot a.pcdc-brand, .pcdc .pcdc-foot a.pcdc-brand:hover { display: inline-flex; color: var(--pcdc-logo); text-decoration: none; }
.pcdc .pcdc-brand svg { display: block; width: auto; height: var(--pcdc-lh); }
.pcdc.pcdc .pcdc-brand img { display: block; width: auto; height: var(--pcdc-lh); max-width: calc(var(--pcdc-lh) * 5); object-fit: contain; }
.pcdc .pcdc-copy {
  font-size: calc(var(--pcdc-u) * .82 * var(--pcdc-s-copy, 1));
  font-family: var(--pcdc-f-copy, inherit); font-weight: var(--pcdc-w-copy, 500); color: var(--pcdc-c-copy, var(--pcdc-muted));
}

/* Slogan and subtitle. Centred on the block by the grid, then on the SCREEN
   by the script (--pcdc-mid-shift) when the block is not centred itself. */
.pcdc .pcdc-foot-mid {
  grid-column: 2; display: flex; flex-direction: column; align-items: center; text-align: center;
  max-width: 58cqi; transform: translateX(var(--pcdc-mid-shift, 0px));
}
.pcdc .pcdc-slogan {
  margin: 0; font-size: calc(var(--pcdc-u) * 1.55 * var(--pcdc-s-slogan, 1));
  font-family: var(--pcdc-f-slogan, inherit); font-weight: var(--pcdc-w-slogan, 700);
  line-height: 1.15; letter-spacing: -.012em; color: var(--pcdc-c-slogan, var(--pcdc-title)); text-wrap: balance;
}
.pcdc .pcdc-sub {
  margin: .35em 0 0; font-size: calc(var(--pcdc-u) * 1 * var(--pcdc-s-sub, 1));
  font-family: var(--pcdc-f-sub, inherit); font-weight: var(--pcdc-w-sub, 500);
  line-height: 1.3; color: var(--pcdc-c-sub, var(--pcdc-muted)); text-wrap: balance;
}
.pcdc .pcdc-slogan[hidden] + .pcdc-sub { margin-top: 0; }

.pcdc .pcdc-foot-right { grid-column: 3; justify-self: end; display: flex; align-items: center; gap: 1em; min-width: 0; }
.pcdc .pcdc-foot a.pcdc-site {
  font-size: calc(var(--pcdc-u) * .82 * var(--pcdc-s-site, 1));
  font-family: var(--pcdc-f-site, inherit); font-weight: var(--pcdc-w-site, 600); color: var(--pcdc-c-site, var(--pcdc-muted));
}
.pcdc .pcdc-qr-slot { display: none; width: var(--pcdc-qr-w); flex: none; }
.pcdc.has-qr .pcdc-qr-slot { display: block; }

/* The QR card sticks out of the footer on purpose: it crosses the footer
   line, tilted, and floats a little, to invite visitors to scan it. */
.pcdc .pcdc-foot a.pcdc-qr, .pcdc .pcdc-foot a.pcdc-qr:hover {
  position: absolute; right: 4px; bottom: 0; z-index: 2;
  display: flex; flex-direction: column; width: var(--pcdc-qr-w);
  padding: calc(var(--pcdc-qr-w) * .055) calc(var(--pcdc-qr-w) * .055) 0;
  border-radius: calc(var(--pcdc-qr-w) * .12); background: #fff; text-decoration: none;
  box-shadow: 0 22px 44px -18px rgba(0, 0, 0, .7), 0 0 0 1px rgba(255, 255, 255, .5), 0 0 40px -8px rgba(74, 91, 255, .55);
  transform: rotate(-4deg); transform-origin: 50% 100%;
  animation: pcdc-qr-float 4.8s ease-in-out infinite;
}
.pcdc a.pcdc-qr:hover, .pcdc a.pcdc-qr:focus-visible { animation-play-state: paused; }
.pcdc a.pcdc-qr:focus-visible { outline: 3px solid var(--pcdc-blue-2); outline-offset: 4px; }
.pcdc .pcdc-qr-code svg { display: block; width: 100%; height: auto; }
.pcdc .pcdc-qr-cap {
  display: block; margin: calc(var(--pcdc-qr-w) * .04) calc(var(--pcdc-qr-w) * -.055) 0;
  padding: .55em .4em .65em; border-radius: 0 0 calc(var(--pcdc-qr-w) * .12) calc(var(--pcdc-qr-w) * .12);
  font-size: calc(var(--pcdc-qr-w) * .085); font-weight: 800; letter-spacing: .02em; line-height: 1.15; text-align: center;
  color: #fff; background: linear-gradient(135deg, var(--pcdc-blue), var(--pcdc-blue-2));
}
@keyframes pcdc-qr-float {
  0%, 100% { transform: rotate(-4deg) translateY(0); }
  50% { transform: rotate(-2deg) translateY(-7px); }
}

/* Floating note with a hand-drawn arrow pointing at the QR card. */
.pcdc .pcdc-foot .pcdc-qr-hint {
  position: absolute; z-index: 2; margin: 0;
  right: calc(var(--pcdc-qr-w) + var(--pcdc-u) * .6); bottom: calc(var(--pcdc-qr-w) * .62);
  display: flex; align-items: flex-end; gap: .15em;
  max-width: calc(100% - var(--pcdc-qr-w) - var(--pcdc-u) * 2);
  font-size: calc(var(--pcdc-u) * 1.05 * var(--pcdc-s-qrHint, 1)); line-height: 1.15; text-align: right;
  font-family: var(--pcdc-f-qrHint, inherit); font-weight: var(--pcdc-w-qrHint, 700);
  color: var(--pcdc-c-qrHint, var(--pcdc-title)); pointer-events: none;
  animation: pcdc-hint-float 4.8s ease-in-out infinite;
}
.pcdc .pcdc-qr-hint-text { max-width: 10.5em; padding-bottom: 1.5em; text-wrap: balance; }
.pcdc .pcdc-qr-arrow { width: 3.4em; height: auto; flex: none; color: var(--pcdc-blue-2); transform: translateY(.3em); }
@keyframes pcdc-hint-float {
  0%, 100% { transform: translate(0, 0); }
  50% { transform: translate(-4px, -6px); }
}

/* Narrower blocks (portrait tablet, phone, side column): the footer stacks,
   centred. The QR card then straddles the footer line (half above it, so it
   still sticks out) and its note sits in the free band above that line;
   room is kept on both sides so the centred texts stay centred. */
@container pcdc (max-width: 900px) {
  .pcdc .pcdc-foot { grid-template-columns: minmax(0, 1fr); justify-items: center; text-align: center; }
  .pcdc .pcdc-foot-left, .pcdc .pcdc-foot-mid, .pcdc .pcdc-foot-right { grid-column: 1; justify-self: center; justify-content: center; }
  .pcdc .pcdc-foot-mid { transform: none; max-width: 100%; }
  .pcdc.has-qr .pcdc-qr-slot { display: none; }
  .pcdc.has-qr .pcdc-foot { padding-inline: calc(var(--pcdc-qr-w) + 16px); }
  .pcdc .pcdc-foot a.pcdc-qr, .pcdc .pcdc-foot a.pcdc-qr:hover { bottom: auto; top: calc(var(--pcdc-qr-w) * -.55); }
  .pcdc .pcdc-foot .pcdc-qr-hint { bottom: calc(100% - var(--pcdc-qr-w) * .02); max-width: calc(100% - var(--pcdc-qr-w) - var(--pcdc-u) * 1.5); }
}
/* Phone held upright: a smaller card, and the texts keep the left side. */
@container pcdc (max-width: 560px) {
  .pcdc .pcdc-foot { --pcdc-qr-w: calc(var(--pcdc-u) * 7.6 * var(--pcdc-s-qr, 1)); }
  .pcdc.has-qr .pcdc-foot { padding-inline: 0 calc(var(--pcdc-qr-w) + 14px); justify-items: start; text-align: left; }
  .pcdc.has-qr .pcdc-foot :is(.pcdc-foot-left, .pcdc-foot-mid, .pcdc-foot-right) { justify-self: start; justify-content: flex-start; align-items: flex-start; text-align: left; }
  .pcdc .pcdc-foot .pcdc-qr-hint { font-size: calc(var(--pcdc-u) * .95 * var(--pcdc-s-qrHint, 1)); }
}

/* Way into the developer mode when the footer is switched off. */
.pcdc button.pcdc-dev-corner {
  position: absolute; left: 6px; bottom: 4px; z-index: 5; padding: 6px;
  font: 600 12px/1 ui-monospace, "SF Mono", Consolas, monospace;
  color: var(--pcdc-muted); background: transparent; opacity: .22; transition: opacity .25s;
}
.pcdc button.pcdc-dev-corner:hover, .pcdc button.pcdc-dev-corner:focus-visible { opacity: 1; background: transparent; color: var(--pcdc-title); }

/* Draft banner: only in the developer's own browser, never for visitors. */
.pcdc .pcdc-draft {
  position: absolute; z-index: 6; top: 10px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 10px; max-width: calc(100% - 24px);
  padding: 6px 6px 6px 14px; border-radius: 99px; white-space: nowrap;
  font-size: .78rem; font-weight: 700; color: #1d1400; background: #ffb020;
  box-shadow: 0 10px 30px -10px rgba(0, 0, 0, .6);
}
.pcdc .pcdc-draft span { overflow: hidden; text-overflow: ellipsis; }
.pcdc button.pcdc-draft-btn { padding: 6px 12px; border-radius: 99px; font-size: .74rem; font-weight: 700; color: #fff; background: #1d1400; }
.pcdc button.pcdc-draft-btn:hover { color: #fff; background: #3a2a00; }

/* While the published version is on its way (first visit only). */
.pcdc .pcdc-head, .pcdc .pcdc-views, .pcdc .pcdc-foot { transition: opacity .35s; }
.pcdc.is-loading :is(.pcdc-head, .pcdc-views, .pcdc-foot) { opacity: 0; }

/* No visible card. */
.pcdc .pcdc-empty {
  padding: 48px 20px; text-align: center; font-size: .95rem; color: var(--pcdc-muted);
  border: 1px dashed var(--pcdc-glass-line); border-radius: 22px;
}
.pcdc[data-view="carousel"] .pcdc-empty { position: absolute; left: 50%; top: 50%; width: min(420px, 80%); transform: translate(-50%, -50%); }

/* Settings: the view switch can be hidden from the developer panel. */
.pcdc[data-switch="false"] .pcdc-switch { display: none; }
.pcdc.pcdc [hidden] { display: none !important; }   /* [hidden] must win over any display rule */

/* ---------------- Developer mode ---------------- */
.pcdc button.pcdc-dev-link {
  font-family: ui-monospace, "SF Mono", Consolas, monospace; font-size: calc(var(--pcdc-u) * .72);
  color: var(--pcdc-muted); opacity: .45; padding: 4px 2px; background: transparent;
  transition: opacity .25s;
}
.pcdc button.pcdc-dev-link:hover, .pcdc button.pcdc-dev-link:focus-visible { opacity: 1; background: transparent; color: var(--pcdc-title); }
.pcdc button.pcdc-dev-link:focus-visible { outline: 1px dashed currentColor; outline-offset: 2px; }

.pcdc dialog.pcdc-dlg {
  margin: auto; padding: 0; overflow: hidden;
  border: 1px solid var(--pcdc-glass-line); border-radius: 22px;
  color: var(--pcdc-ink); background: var(--pcdc-bg);
  background-image: radial-gradient(420px 220px at 50% -20%, var(--pcdc-halo), transparent 70%);
  box-shadow: 0 40px 90px -30px rgba(0, 0, 0, .75);
  font: 400 15px/1.45 "Inter Tight", system-ui, sans-serif; text-align: left;
}
.pcdc dialog.pcdc-dlg.is-lock { width: min(420px, calc(100vw - 24px)); }
.pcdc dialog.pcdc-dlg::backdrop { background: rgba(4, 5, 12, .5); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); }
/* Administration: a drawer on the right, the showcase stays visible as a live preview. */
.pcdc dialog.pcdc-dlg.is-admin {
  width: min(560px, 100vw); height: 100dvh; max-height: 100dvh; max-width: 100vw;
  margin: 0 0 0 auto; border-radius: 22px 0 0 22px; border-right: 0;
}
.pcdc dialog.pcdc-dlg.is-admin::backdrop { background: rgba(4, 5, 12, .22); -webkit-backdrop-filter: none; backdrop-filter: none; }
@media (max-width: 600px) { .pcdc dialog.pcdc-dlg.is-admin { border-radius: 0; border-left: 0; } }

.pcdc .pcdc-dlg-body { padding: 22px 22px 20px; }
.pcdc .pcdc-dlg-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
.pcdc .pcdc-dlg-kicker { font-size: .7rem; font-weight: 700; letter-spacing: .16em; text-transform: uppercase; color: var(--pcdc-muted); }
.pcdc .pcdc-dlg-title { margin-top: 4px; font-size: 1.35rem; font-weight: 800; letter-spacing: -.02em; color: var(--pcdc-title); }
.pcdc .pcdc-dlg-note { font-size: .82rem; color: var(--pcdc-muted); margin-bottom: 14px; }
.pcdc button.pcdc-x {
  width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center; flex: none;
  color: var(--pcdc-title); background: var(--pcdc-glass); border: 1px solid var(--pcdc-glass-line);
}
.pcdc button.pcdc-x:hover { background: var(--pcdc-glass-hover); color: var(--pcdc-title); }
.pcdc button.pcdc-x svg, .pcdc button.pcdc-ico svg { width: 16px; height: 16px; }

/* Admin layout: head, tabs, scrolling body, sticky publish bar. */
.pcdc .pcdc-adm { display: flex; flex-direction: column; height: 100%; }
.pcdc .pcdc-adm-head { padding: 18px 20px 12px; display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.pcdc .pcdc-server { display: inline-flex; align-items: center; gap: 7px; margin-top: 6px; font-size: .76rem; color: var(--pcdc-muted); }
.pcdc .pcdc-server::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: #9aa0b4; }
.pcdc .pcdc-server[data-state="online"]::before { background: #2fd27a; box-shadow: 0 0 10px #2fd27a; }
.pcdc .pcdc-server[data-state="offline"]::before, .pcdc .pcdc-server[data-state="error"]::before { background: #ff5d6e; }
.pcdc .pcdc-server[data-state="local"]::before { background: #ffb020; }
.pcdc .pcdc-server[data-state="outdated"]::before { background: #ffb020; box-shadow: 0 0 10px #ffb020; }
.pcdc .pcdc-server[data-state="outdated"] { color: #ffb020; font-weight: 600; }
.pcdc .pcdc-tabs {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(max-content, 1fr); gap: 2px; margin: 0 20px; padding: 3px;
  border-radius: 99px; background: var(--pcdc-glass); border: 1px solid var(--pcdc-glass-line);
  overflow-x: auto; scrollbar-width: none;
}
.pcdc .pcdc-tabs::-webkit-scrollbar { display: none; }
.pcdc button.pcdc-tab {
  padding: 9px 10px; border-radius: 99px; font-size: .8rem; font-weight: 700; white-space: nowrap;
  color: var(--pcdc-muted); background: transparent;
}
.pcdc button.pcdc-tab:hover { color: var(--pcdc-title); background: transparent; }
.pcdc button.pcdc-tab[aria-selected="true"], .pcdc button.pcdc-tab[aria-selected="true"]:hover { color: #fff; background: linear-gradient(135deg, var(--pcdc-blue), var(--pcdc-blue-2)); }
.pcdc button.pcdc-tab:focus-visible { outline: 2px solid var(--pcdc-blue-2); outline-offset: 1px; }
.pcdc .pcdc-adm-body { flex: 1; min-height: 0; overflow: auto; padding: 16px 20px 24px; overscroll-behavior: contain; }
.pcdc .pcdc-adm-foot {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; padding: 12px 20px 16px;
  border-top: 1px solid var(--pcdc-line); background: var(--pcdc-bg);
}
.pcdc .pcdc-state { flex: 1 1 200px; min-width: 0; font-size: .8rem; color: var(--pcdc-muted); }
.pcdc .pcdc-state strong { color: var(--pcdc-title); }
.pcdc .pcdc-state.is-dirty strong { color: #ffb020; }
.pcdc .pcdc-state.is-error strong { color: #ff5d6e; }
.pcdc .pcdc-state.is-ok strong { color: #2fd27a; }

.pcdc .pcdc-sec-title { margin: 18px 0 8px; font-size: .72rem; font-weight: 800; letter-spacing: .14em; text-transform: uppercase; color: var(--pcdc-muted); }
.pcdc .pcdc-sec-title:first-child { margin-top: 0; }
.pcdc .pcdc-row {
  display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 6px 16px;
  padding: 12px 0; border-top: 1px solid var(--pcdc-line);
}
.pcdc .pcdc-row > span { font-size: .9rem; font-weight: 600; color: var(--pcdc-title); }
.pcdc .pcdc-row small { display: block; font-size: .76rem; font-weight: 400; color: var(--pcdc-muted); }
.pcdc .pcdc-row.is-wide { grid-template-columns: 1fr; }
@media (max-width: 520px) {
  .pcdc .pcdc-row:has(.pcdc-seg) { grid-template-columns: 1fr; }
  .pcdc .pcdc-row .pcdc-seg { width: 100%; }
}

/* Form controls. */
.pcdc .pcdc-lbl { display: block; margin: 14px 0 6px; font-size: .8rem; font-weight: 700; color: var(--pcdc-title); }
.pcdc .pcdc-lbl small { font-weight: 400; color: var(--pcdc-muted); }
.pcdc .pcdc-help { margin-top: 5px; font-size: .74rem; color: var(--pcdc-muted); }
.pcdc .pcdc-warn { margin-top: 5px; font-size: .74rem; color: #ff8a3d; }
.pcdc.pcdc :is(input.pcdc-field, textarea.pcdc-field, select.pcdc-field) {
  width: 100%; margin: 0; padding: 11px 13px; border-radius: 12px; box-shadow: none; min-height: 0;
  font: 500 .95rem/1.35 "Inter Tight", system-ui, sans-serif; color: var(--pcdc-title);
  background: var(--pcdc-glass); border: 1px solid var(--pcdc-glass-line);
}
.pcdc.pcdc textarea.pcdc-field { resize: vertical; min-height: 84px; }
.pcdc.pcdc textarea.pcdc-field.is-code { font: 500 .74rem/1.5 ui-monospace, Consolas, monospace; min-height: 110px; }
.pcdc.pcdc select.pcdc-field { -webkit-appearance: none; appearance: none; background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%); background-position: calc(100% - 18px) 55%, calc(100% - 13px) 55%; background-size: 5px 5px; background-repeat: no-repeat; padding-right: 34px; }
.pcdc.pcdc select.pcdc-field option { color: #0b0e1c; }
.pcdc.pcdc .pcdc-field:focus { outline: 2px solid var(--pcdc-blue-2); outline-offset: 1px; }
.pcdc.pcdc input[type="color"].pcdc-color { width: 52px; height: 40px; padding: 3px; border-radius: 10px; border: 1px solid var(--pcdc-glass-line); background: var(--pcdc-glass); cursor: pointer; }
.pcdc .pcdc-err { min-height: 1.3em; margin-top: 8px; font-size: .82rem; color: #ff5d6e; }
.pcdc .pcdc-lock.is-wrong .pcdc-field { animation: pcdc-shake .35s; }
@keyframes pcdc-shake { 25% { transform: translateX(-6px); } 50% { transform: translateX(6px); } 75% { transform: translateX(-3px); } }
.pcdc .pcdc-inline { display: flex; align-items: center; gap: 10px; }
.pcdc .pcdc-inline > .pcdc-field { flex: 1; }

/* On / off switch built on a checkbox. */
.pcdc.pcdc input.pcdc-toggle {
  -webkit-appearance: none; appearance: none; position: relative; margin: 0; padding: 0; cursor: pointer;
  width: 46px; height: 28px; border-radius: 99px; flex: none;
  background: var(--pcdc-dot); border: 1px solid var(--pcdc-glass-line); transition: background .25s;
}
.pcdc.pcdc input.pcdc-toggle::after {
  content: ""; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%;
  background: #fff; box-shadow: 0 2px 6px rgba(0, 0, 0, .35); transition: transform .25s cubic-bezier(.2, .8, .2, 1);
}
.pcdc.pcdc input.pcdc-toggle:checked { background: linear-gradient(135deg, var(--pcdc-blue), var(--pcdc-blue-2)); }
.pcdc.pcdc input.pcdc-toggle:checked::after { transform: translateX(18px); }
.pcdc.pcdc input.pcdc-toggle:focus-visible { outline: 2px solid var(--pcdc-blue-2); outline-offset: 2px; }

/* Segmented choice built on radio buttons. */
.pcdc .pcdc-seg {
  display: inline-grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 2px; padding: 3px;
  border-radius: 99px; background: var(--pcdc-glass); border: 1px solid var(--pcdc-glass-line);
}
.pcdc .pcdc-seg label { position: relative; margin: 0; cursor: pointer; }
.pcdc .pcdc-seg input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }
.pcdc .pcdc-seg span {
  display: block; padding: 7px 12px; border-radius: 99px; text-align: center; white-space: nowrap;
  font-size: .8rem; font-weight: 600; color: var(--pcdc-muted); transition: background .2s, color .2s;
}
.pcdc .pcdc-seg input:checked + span { color: #fff; background: linear-gradient(135deg, var(--pcdc-blue), var(--pcdc-blue-2)); }
.pcdc .pcdc-seg input:focus-visible + span { outline: 2px solid var(--pcdc-blue-2); outline-offset: 1px; }

/* Slider. */
.pcdc .pcdc-range { display: flex; align-items: center; gap: 12px; }
.pcdc.pcdc .pcdc-range input { flex: 1; margin: 0; accent-color: var(--pcdc-blue-2); }
.pcdc .pcdc-range output { min-width: 5.2em; text-align: right; font-size: .82rem; font-weight: 700; color: var(--pcdc-title); font-variant-numeric: tabular-nums; }

/* Buttons. */
.pcdc .pcdc-dlg-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-top: 16px; }
.pcdc button.pcdc-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  min-height: 42px; padding: 0 18px; border-radius: 99px; font-size: .86rem; font-weight: 700;
  color: var(--pcdc-title); background: var(--pcdc-glass); border: 1px solid var(--pcdc-glass-line);
}
.pcdc button.pcdc-btn:hover { background: var(--pcdc-glass-hover); color: var(--pcdc-title); }
.pcdc button.pcdc-btn.is-primary, .pcdc button.pcdc-btn.is-primary:hover {
  color: #fff; border-color: transparent; background: linear-gradient(135deg, var(--pcdc-blue), var(--pcdc-blue-2));
}
.pcdc button.pcdc-btn.is-danger, .pcdc button.pcdc-btn.is-danger:hover { color: #fff; border-color: transparent; background: #d92d43; }
.pcdc button.pcdc-btn.is-quiet { color: var(--pcdc-muted); background: transparent; border-color: transparent; }
.pcdc button.pcdc-btn.is-small { min-height: 34px; padding: 0 12px; font-size: .78rem; }
.pcdc button.pcdc-btn:disabled { opacity: .45; cursor: not-allowed; }
.pcdc button.pcdc-btn:focus-visible, .pcdc button.pcdc-ico:focus-visible { outline: 2px solid var(--pcdc-blue-2); outline-offset: 2px; }
.pcdc button.pcdc-ico {
  width: 34px; height: 34px; display: grid; place-items: center; flex: none; border-radius: 10px;
  color: var(--pcdc-title); background: var(--pcdc-glass); border: 1px solid var(--pcdc-glass-line);
}
.pcdc button.pcdc-ico:hover { background: var(--pcdc-glass-hover); color: var(--pcdc-title); }
.pcdc button.pcdc-ico:disabled { opacity: .3; cursor: default; }

/* Cards tab: list of cards. */
.pcdc .pcdc-adm-cards { display: grid; gap: 8px; }
.pcdc .pcdc-adm-card {
  display: grid; grid-template-columns: 48px 1fr auto; align-items: center; gap: 12px;
  padding: 8px 8px 8px 8px; border-radius: 14px; background: var(--pcdc-glass); border: 1px solid var(--pcdc-glass-line);
}
.pcdc .pcdc-adm-card.is-hidden { opacity: .55; }
.pcdc .pcdc-thumb {
  width: 48px; height: 48px; border-radius: 12px; overflow: hidden; display: grid; place-items: center;
  font-weight: 800; color: #fff; background: linear-gradient(135deg, var(--pcdc-accent, #1001ef), #0b0e1c);
}
.pcdc.pcdc .pcdc-thumb img { width: 100%; height: 100%; object-fit: cover; }
.pcdc.pcdc .pcdc-thumb.is-icon img { object-fit: contain; padding: 4px; }
.pcdc button.pcdc-adm-open { min-width: 0; text-align: left; background: transparent; padding: 4px 0; }
.pcdc button.pcdc-adm-open:hover { background: transparent; }
.pcdc .pcdc-adm-open strong { display: block; font-size: .92rem; font-weight: 700; color: var(--pcdc-title); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pcdc .pcdc-adm-open small { display: block; font-size: .74rem; color: var(--pcdc-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pcdc .pcdc-adm-open em { font-style: normal; font-size: .66rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: #ffb020; margin-left: 6px; }
.pcdc .pcdc-adm-tools { display: flex; gap: 4px; }

/* Card editor. */
.pcdc .pcdc-edit-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 4px; }
.pcdc .pcdc-media-box { display: grid; grid-template-columns: 72px 1fr; gap: 12px; align-items: start; }
.pcdc .pcdc-media-box .pcdc-thumb { width: 72px; height: 72px; border-radius: 14px; }
.pcdc .pcdc-media-btns { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.pcdc .pcdc-progress { height: 4px; margin-top: 8px; border-radius: 4px; background: var(--pcdc-dot); overflow: hidden; }
.pcdc .pcdc-progress i { display: block; height: 100%; width: 0; background: linear-gradient(90deg, var(--pcdc-blue), var(--pcdc-blue-2)); transition: width .2s; }
.pcdc .pcdc-danger-zone { margin-top: 22px; padding-top: 14px; border-top: 1px solid var(--pcdc-line); display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; }

/* Footer tab: QR preview. */
.pcdc .pcdc-qr-preview { display: grid; grid-template-columns: 96px 1fr; gap: 14px; align-items: center; margin-top: 10px; }
.pcdc .pcdc-qr-preview > div:first-child { padding: 6px; border-radius: 12px; background: #fff; }
.pcdc .pcdc-qr-preview svg { display: block; width: 100%; height: auto; }
.pcdc .pcdc-qr-preview p { font-size: .76rem; color: var(--pcdc-muted); word-break: break-all; }

/* Settings tabs: one box per element of the page, a switch to show it and
   the details (text, size, font, weight, colour) folded underneath. */
.pcdc .pcdc-set-intro { font-size: .8rem; color: var(--pcdc-muted); margin-bottom: 6px; }
.pcdc .pcdc-el { margin-top: 8px; border-radius: 14px; background: var(--pcdc-glass); border: 1px solid var(--pcdc-glass-line); }
.pcdc .pcdc-el-head { display: flex; align-items: center; gap: 10px; padding: 4px 12px 4px 4px; }
.pcdc button.pcdc-el-name {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; padding: 10px 8px; text-align: left;
  font-size: .9rem; font-weight: 700; line-height: 1.2; color: var(--pcdc-title); background: transparent;
}
.pcdc button.pcdc-el-name:hover { background: transparent; color: var(--pcdc-title); }
.pcdc button.pcdc-el-name:focus-visible { outline: 2px solid var(--pcdc-blue-2); outline-offset: 1px; border-radius: 10px; }
.pcdc .pcdc-el-name small { display: block; font-size: .72rem; font-weight: 400; color: var(--pcdc-muted); }
.pcdc .pcdc-el-name svg { width: 14px; height: 14px; flex: none; transition: transform .2s; }
.pcdc .pcdc-el-name[aria-expanded="true"] svg { transform: rotate(90deg); }
.pcdc .pcdc-el.is-off > .pcdc-el-head .pcdc-el-name { opacity: .5; }
.pcdc .pcdc-el-body { padding: 2px 14px 14px; border-top: 1px solid var(--pcdc-line); }
.pcdc .pcdc-el-body > .pcdc-row:first-child { border-top: 0; }
.pcdc .pcdc-el-body .pcdc-lbl:first-child { margin-top: 10px; }
.pcdc .pcdc-duo { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.pcdc .pcdc-duo .pcdc-lbl { margin-top: 12px; }
.pcdc .pcdc-colorctl { display: flex; align-items: center; gap: 10px; min-height: 40px; }
.pcdc .pcdc-colorctl label { display: inline-flex; align-items: center; gap: 6px; font-size: .8rem; color: var(--pcdc-muted); cursor: pointer; }
.pcdc.pcdc .pcdc-colorctl input[type="checkbox"] { width: 16px; height: 16px; margin: 0; accent-color: var(--pcdc-blue-2); }
.pcdc .pcdc-colorctl input[type="color"]:disabled { opacity: .35; cursor: default; }

/* Banner export: choices on the full width, preview on a checkerboard
   (dark behind light texts, light behind dark texts). */
.pcdc .pcdc-row.is-choice .pcdc-seg { width: 100%; }
.pcdc .pcdc-row.is-choice .pcdc-seg span { padding-inline: 6px; }
.pcdc .pcdc-exp-prev {
  margin-top: 12px; border-radius: 12px; overflow: hidden; border: 1px solid var(--pcdc-glass-line);
  background: repeating-conic-gradient(#2c2f3b 0% 25%, #1c1e27 0% 50%) 50% / 18px 18px;
}
.pcdc .pcdc-exp-prev.is-light { background: repeating-conic-gradient(#dfe2ea 0% 25%, #f5f6fa 0% 50%) 50% / 18px 18px; }
.pcdc.pcdc .pcdc-exp-canvas { display: block; width: 100%; height: auto; }

/* Publication tab. */
.pcdc .pcdc-versions { display: grid; gap: 6px; margin-top: 8px; }
.pcdc .pcdc-version { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 10px; border-radius: 12px; background: var(--pcdc-glass); border: 1px solid var(--pcdc-glass-line); font-size: .8rem; }
.pcdc .pcdc-version small { color: var(--pcdc-muted); }

/* Dots and hint as wide as their content: the script measures them to let
   the QR card rise beside the controls. */
.pcdc .pcdc-dots, .pcdc .pcdc-hint { width: fit-content; max-width: 100%; margin-inline: auto; }

@media (prefers-reduced-motion: reduce) {
  .pcdc *, .pcdc *::before, .pcdc *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
  .pcdc a.pcdc-qr, .pcdc .pcdc-qr-hint { animation: none; }
}
.pcdc .pcdc-dlg-actions.is-start { justify-content: flex-start; }
</style>

  <div class="pcdc-draft" hidden>
    <span>Brouillon non publié : visible sur cet appareil seulement</span>
    <button type="button" class="pcdc-draft-btn" data-open-dev>Ouvrir .dev</button>
  </div>

  <header class="pcdc-head">
    <div>
      <p class="pcdc-kicker">PCdlabs · Présent Composé design</p>
      <h2 class="pcdc-h">Applications &amp; expériences</h2>
    </div>
    <div class="pcdc-switch" role="group" aria-label="Mode d'affichage">
      <span class="pcdc-switch-thumb" aria-hidden="true"></span>
      <button type="button" class="pcdc-switch-btn" data-view="carousel" aria-pressed="true">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="5" width="8" height="12" rx="1.6"/><path d="M5 7.5 3 8.5v7l2 1M19 7.5l2 1v7l-2 1"/><path d="M6 20.5c3.8 1 8.2 1 12 0" stroke-linecap="round"/></svg>
        <span class="pcdc-switch-label">Carrousel</span>
      </button>
      <button type="button" class="pcdc-switch-btn" data-view="grid" aria-pressed="false">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><rect x="3.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="13.5" width="7" height="7" rx="1.6"/></svg>
        <span class="pcdc-switch-label">Grille</span>
      </button>
      <button type="button" class="pcdc-switch-btn" data-view="list" aria-pressed="false">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><rect x="3.5" y="4" width="5" height="5" rx="1.2"/><rect x="3.5" y="15" width="5" height="5" rx="1.2"/><path d="M12 6.5h8.5M12 17.5h8.5"/></svg>
        <span class="pcdc-switch-label">Liste</span>
      </button>
    </div>
  </header>

  <div class="pcdc-views">
    <div class="pcdc-stage" tabindex="0" role="region" aria-roledescription="carrousel" aria-label="Projets, utilisez les flèches gauche et droite">
      <div class="pcdc-track"></div>
    </div>
    <div class="pcdc-nav">
      <button type="button" class="pcdc-arrow" data-dir="-1" aria-label="Projet précédent">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg>
      </button>
      <div class="pcdc-caption" aria-live="polite">
        <span class="pcdc-count"></span>
        <strong class="pcdc-cap-title"></strong>
        <span class="pcdc-cap-kind"></span>
      </div>
      <button type="button" class="pcdc-arrow" data-dir="1" aria-label="Projet suivant">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 5l7 7-7 7"/></svg>
      </button>
    </div>
    <div class="pcdc-dots"></div>
    <p class="pcdc-hint"></p>
  </div>

  <footer class="pcdc-foot">
    <div class="pcdc-foot-left">
      <a class="pcdc-brand" href="https://presentcomposedesign.fr/" aria-label="Présent Composé design">
        <svg class="pcdc-brand-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140.31 140.32" fill="currentColor" aria-hidden="true" focusable="false"><path d="M67.46,46.96c8.95,0,16.24-7.28,16.24-16.24s-7.28-16.24-16.24-16.24-16.24,7.28-16.24,16.24,7.28,16.24,16.24,16.24h0Z"></path><path d="M72.99,78.96c-2.59,0-4.69,2.1-4.69,4.68s2.1,4.69,4.69,4.69,4.68-2.1,4.68-4.69-2.1-4.68-4.68-4.68Z"></path><path d="M119.78,79.07c-2.09,0-3.43.89-4.02,2.48h7.99c-.57-1.6-1.89-2.48-3.98-2.48Z"></path><path d="M108.58,113.12c.75-.32,1.36-.71,1.84-1.2.8-.8,1.12-2.24.95-4.32-.01-.15-.03-.32-.06-.51-.09-.82-.35-2.14-.78-3.98-.11-.46-.34-1.13-.68-2.03-.18-.4-.33-.74-.43-1-.07-.14-.14-.25-.19-.32-.05-.06-.09-.11-.13-.15-.04-.03-.08-.07-.13-.1-.14-.07-.29-.1-.45-.1-.07,0-.13.02-.17.03-.35-.63-.74-1.11-1.17-1.44-.59-.46-1.44-.68-2.54-.65-.06,0-.12.02-.18.02-.1,0-.2-.01-.3-.01-.82.03-1.55.18-2.17.45-.63.27-1.13.63-1.49,1.07-.37.45-.65.92-.84,1.43-.19.51-.3,1.05-.31,1.62-.03.64.04,1.25.2,1.84.12.57.32,1.11.62,1.63.6,1.08,1.45,1.77,2.53,2.08.65.19,1.24.25,1.81.19.67.01,1.29-.11,1.85-.43.69-.4,1.25-1.04,1.68-1.91.29-.59.54-1.31.73-2.16.05.16.09.3.11.41.34,1.48.58,2.61.7,3.41.02.16.04.31.05.43.19,1.85-.05,3.09-.71,3.74-.39.37-.88.68-1.45.93-.5.22-.99.33-1.47.37-1.06-.02-1.72-.5-1.94-1.46-.03-.13-.11-.22-.25-.29-.14-.06-.3-.09-.47-.06-.05,0-.09.02-.13.04-.11-.03-.23-.05-.35-.03-.17.03-.31.09-.4.19-.1.1-.13.2-.1.31.02.08.05.15.07.22,0,.02,0,.05,0,.07.18.77.58,1.33,1.2,1.69.29.17.61.29.94.37l-51.89,4.89-1.95-20.67,15.3-1.44.28,4.18c-1.29-1.09-2.99-1.7-4.8-1.58-3.72.25-6.54,3.47-6.29,7.19.25,3.72,3.47,6.54,7.19,6.29,3.72-.25,6.54-3.47,6.29-7.19l-.61-9.09,26.63-2.51s-.07.05-.1.08c-.11.02-.21.05-.3.09-.05.03-.1.07-.17.1-.14.04-.25.09-.34.16-.35.24-.42.54-.23.89,0,.12.04.25.13.39.2.27.54.45,1.02.52.25.04.47.03.68,0,.33.03.63,0,.9-.13.33-.15.53-.35.62-.61.07-.21.05-.41-.05-.6,0-.15-.06-.3-.16-.45-.17-.23-.47-.4-.89-.52-.02,0-.04,0-.06-.01l32.39-3.05,1.58,16.75c6.69-10.75,10.56-23.44,10.56-37.03C140.31,31.41,108.9,0,70.16,0S0,31.42,0,70.16s31.41,70.16,70.16,70.16c23.33,0,43.99-11.38,56.75-28.9l-18.44,1.74s.07-.03.11-.04h0ZM106.32,104.78c-.35.71-.77,1.21-1.26,1.49-.23.13-.48.2-.75.23-.22-.02-.45-.05-.69-.12-.77-.23-1.4-.78-1.91-1.64-.21-.38-.36-.78-.47-1.2-.09-.43-.13-.87-.11-1.34.04-1.08.36-1.96.98-2.64.56-.62,1.34-.96,2.31-1.03.61.01,1.1.11,1.41.35.33.27.65.68.95,1.23l.26.46c.1.21.18.37.26.5-.23,1.62-.56,2.85-.98,3.7h0ZM61.59,90.08h-2.53v-9.66c-.12-.74-1-1.11-2.1-1.11-.91,0-1.68.35-2.02,1.04v9.72h-2.5v-9.66h-.02c-.11-.74-.86-1.11-1.96-1.11-.76,0-1.61.24-2.05.72v10.04h-2.55v-12.19h1.75c.76-.44,1.71-.65,2.85-.65,1.32,0,2.41.28,3.26.85.84-.57,1.91-.85,3.23-.85s2.43.39,3.34,1.17c.82.72,1.24,1.56,1.28,2.53v9.14h0ZM61.03,61.68s-1.04-.27-2.11.92c-1.27,1.41-1.35,3.16-1.35,3.16v6.82h-2.42v-12.99h2.42v1.87s.76-1.21,1.94-1.56c1.07-.32,2.6-.02,2.6-.02l-1.08,1.79h0ZM63.49,66.1c0-1.84.67-3.41,2.01-4.75,1.34-1.33,2.97-1.97,4.9-1.97s3.6.75,4.95,2.1c1.17,1.17,1.75,2.8,1.75,4.83h-11.51c.07,1.2.68,2.21,1.5,3.03.88.88,2.11,1.32,3.31,1.32s2.24-.45,3.14-1.35c.12-.12.36-.4.72-.84l2.27.8c-.34.57-.73,1.07-1.17,1.51-1.35,1.36-3,2.04-4.95,2.04s-3.56-.66-4.9-2c-1.34-1.34-2.01-2.91-2.01-4.73h0ZM64.93,58.33v-1.75h10.73v1.75h-10.73ZM62.28,110.32c-2.58.17-4.81-1.78-4.99-4.36s1.78-4.81,4.36-4.99c2.58-.17,4.81,1.78,4.99,4.36.17,2.58-1.78,4.81-4.36,4.99h0ZM73.01,90.4c-1.82,0-3.5-.73-4.67-1.89v4.51h-2.32v-9.37c0-3.73,3.13-6.75,6.86-6.75s6.82,3.02,6.82,6.75-2.96,6.75-6.69,6.75h0ZM80.28,72.12c-1.19-.9-1.45-1.26-1.45-1.26l1.53-1.74c1.19,1.5,2.41,1.78,2.81,1.8.25.02,1.1.04,1.58-.39.35-.32.64-.74.63-1.38,0-.49-.26-.9-.7-1.22-.31-.23-1.12-.58-1.74-.85-1.19-.51-3.84-1.37-3.79-4.09.02-1,.4-1.96,1.24-2.64.88-.71,1.93-.97,3.07-.92,0,0,2.11.03,3.88,1.97l-1.52,1.47c-1.31-1.11-1.83-1.31-2.28-1.37-.26-.03-.93-.08-1.38.16-.51.27-.73.56-.78,1.09-.08.74.51,1.27.96,1.51.9.49,3.48,1.31,4.4,2.49.55.71.82,1.42.82,2.38s-.4,2.08-1.2,2.83c-.87.81-1.91,1.06-3.05,1.06s-2.12-.25-3.01-.92h0ZM95.37,88.39c-1.33,1.34-2.96,2.01-4.91,2.01s-3.56-.67-4.9-2.01c-1.34-1.34-2.01-2.92-2.01-4.74s.67-3.43,2.01-4.76c1.34-1.33,2.97-2,4.9-2s3.58.67,4.91,2c1.32,1.33,1.99,2.92,1.99,4.76s-.66,3.41-1.99,4.74h0ZM116.52,86.92c.88.88,2.05,1.32,3.25,1.32s2.24-.45,3.14-1.35c.12-.12.36-.4.72-.84l2.27.8c-.34.57-.73,1.07-1.17,1.51-1.35,1.36-3,2.04-4.95,2.04s-3.56-.67-4.9-2.01c-1.34-1.34-2.01-2.92-2.01-4.74s.67-3.42,2.01-4.76c1.34-1.33,2.97-1.99,4.9-1.99s3.6.71,4.95,2.06c1.17,1.17,1.75,2.58,1.75,4.76h-11.41c.02,1.26.59,2.33,1.45,3.19h0ZM114.3,75.9v-1.75h10.74v1.75h-10.74ZM119.6,60.34h2.27v-4.74h2.4v4.74h2.27v2.08h-2.27v10.11h-2.4v-10.11h-2.27v-2.08h0ZM106.51,60.34v-.72h2.46v.68c.42-.42,1.25-.63,2.49-.63,1.54,0,2.78.47,3.72,1.41.94.94,1.42,2.1,1.44,3.45v7.99h-2.4v-7.88c0-.83-.28-1.51-.84-2.06-.56-.55-1.2-.82-1.92-.82s-1.34.26-1.88.77c-.31.31-.53.61-.65.89v9.1h-2.42v-12.19h0ZM105.05,76.69s2.1.03,3.87,1.97l-1.52,1.48c-1.31-1.11-1.83-1.31-2.28-1.37-.25-.03-.92-.08-1.38.16-.51.27-.73.56-.78,1.09-.08.74.51,1.27.95,1.51.9.49,3.49,1.31,4.4,2.49.55.71.82,1.43.82,2.38,0,1-.4,2.09-1.2,2.83-.87.81-1.91,1.06-3.04,1.06s-2.12-.25-3.01-.92c-1.19-.9-1.45-1.26-1.45-1.26l1.52-1.74c1.19,1.5,2.41,1.78,2.81,1.8.25.02,1.1.04,1.58-.39.35-.32.64-.74.63-1.38,0-.49-.26-.89-.7-1.22-.31-.23-1.12-.58-1.75-.84-1.18-.51-3.84-1.37-3.79-4.09.02-1,.39-1.96,1.23-2.63.89-.71,1.93-.97,3.07-.92h0ZM92.29,61.37c1.34-1.33,2.97-1.97,4.9-1.97s3.6.74,4.95,2.09c1.16,1.17,1.75,2.79,1.75,4.82h-11.31c.06,1.2.57,2.22,1.4,3.06.88.88,2.01,1.32,3.21,1.32s2.24-.45,3.14-1.35c.12-.12.36-.4.72-.84l2.27.8c-.34.57-.73,1.07-1.17,1.51-1.35,1.36-3,2.04-4.95,2.04s-3.56-.66-4.9-2c-1.34-1.34-2.01-2.91-2.01-4.74s.67-3.41,2.01-4.75h0ZM45.56,30.72c0-11.94,9.84-21.65,21.78-21.65s21.6,9.71,21.6,21.65-9.76,21.65-21.7,21.65c-6.46,0-12.25-2.85-16.46-7.35v28.01s0,.02,0,.03h0s0-.03,0-.03c-.8-3.22-3.05-5.47-5.22-6.94V30.72h0ZM31.97,105.82c-11.94,0-21.65-9.71-21.65-21.65s9.71-21.65,21.65-21.65c8.15,0,17.47,5.64,19.19,13.39h-5.58c-2.8-5.46-7.49-7.98-13.61-7.98-8.95,0-16.24,7.28-16.24,16.24s7.28,16.24,16.24,16.24c6.43,0,11.41-2.48,14.25-8.32l5.52.16c-2.84,8.21-11.2,13.57-19.78,13.57h0ZM38.99,83.65c0,1.82-.66,3.41-1.99,4.74-1.32,1.34-2.96,2.01-4.91,2.01s-3.56-.67-4.9-2.01c-1.34-1.34-2.01-2.92-2.01-4.74s.67-3.43,2.01-4.76c1.34-1.33,2.97-2,4.9-2s3.59.67,4.91,2c1.32,1.33,1.99,2.92,1.99,4.76Z"></path><path d="M97.2,61.51c-2.09,0-3.43.89-4.02,2.48h7.99c-.57-1.6-1.89-2.48-3.97-2.48Z"></path><path d="M90.46,79.02c-1.2,0-2.24.44-3.12,1.33-.88.89-1.32,1.99-1.32,3.29s.44,2.39,1.32,3.27c.88.88,1.92,1.32,3.12,1.32s2.23-.44,3.12-1.32,1.33-1.97,1.33-3.27-.44-2.4-1.33-3.29c-.89-.89-1.92-1.33-3.12-1.33Z"></path><path d="M28.97,80.36c-.88.89-1.32,1.99-1.32,3.29s.44,2.39,1.32,3.27c.88.88,1.92,1.32,3.12,1.32s2.23-.44,3.12-1.32,1.33-1.97,1.33-3.27-.44-2.4-1.33-3.29c-.89-.89-1.92-1.33-3.12-1.33s-2.24.44-3.12,1.33h0Z"></path><path d="M82.09,105.9c0-.06,0-.12-.04-.19-.06-.11-.17-.18-.32-.22-.15-.04-.31-.04-.46.01,0,0,0,0,0,0,0,0,0,0,0,0-.15-.04-.31-.03-.46.01-.16.05-.27.13-.33.24-.28.46-.7.96-1.28,1.51-.58.55-1.1.93-1.57,1.13-.37.15-.77.24-1.17.28-.27,0-.55-.03-.84-.08-.88-.17-1.49-.51-1.84-1.02-.16-.34-.3-.82-.41-1.46-.16-.84-.24-1.72-.24-2.66.02-1.01.12-1.87.31-2.58.24-.71.52-1.14.86-1.3.02,0,.07-.01.14-.03.11-.02.27-.04.48-.05.18-.02.32-.02.47-.02.59.02,1.01.18,1.23.49.11.3.13.68.05,1.13-.11.59-.33,1.19-.66,1.8-.34.61-.67,1.08-.99,1.39l-.23.18s-.1.07-.18.12c-.08.04-.15.08-.2.11-.06.02-.16.07-.32.12-.16.06-.29.11-.4.16-.16.06-.26.14-.3.27-.04.12-.02.23.05.33.03.03.07.06.11.09,0,.08.02.15.06.21.07.1.19.16.37.18.16.02.29,0,.42-.04.02,0,.03.03.06.03.17.02.32,0,.46-.05.07-.04.22-.1.45-.18.24-.08.39-.15.47-.19.08-.04.19-.12.35-.22s.31-.22.46-.36c.39-.37.78-.89,1.15-1.58.38-.68.62-1.35.72-2.01.12-.72.04-1.3-.23-1.75-.07-.18-.15-.34-.26-.48-.47-.64-1.28-.96-2.42-.95-.13,0-.28,0-.45.01-.01,0-.02,0-.03,0-.25,0-.57.01-.96.05-.23.01-.42.03-.58.05-.25.04-.44.1-.57.17-.65.3-1.11.93-1.38,1.88-.24.76-.36,1.67-.38,2.75,0,.97.08,1.9.24,2.8.16.9.38,1.55.65,1.95,0,.01.02.02.03.03.05.09.1.19.15.26.52.75,1.39,1.24,2.63,1.47.62.12,1.22.15,1.79.1.74-.01,1.44-.13,2.11-.41.65-.27,1.3-.72,1.95-1.35.65-.63,1.13-1.21,1.42-1.75.06-.1.06-.2,0-.31-.03-.04-.06-.08-.1-.11h0Z"></path><path d="M97.61,105.16c-.03-.17-.1-.84-.2-2.01-.09-.88-.18-1.56-.27-2.03l-.25-.91c-.11-.39-.17-.64-.18-.75,0-.11,0-.38.03-.81.01-.09-.02-.17-.09-.25,0-.02,0-.04,0-.06.02-.11-.03-.21-.13-.31-.11-.1-.24-.15-.4-.16-.1,0-.2.02-.3.05-.05-.02-.11-.03-.18-.04-.16,0-.31.03-.46.11-.14.08-.22.17-.24.29-.02.51-.03.84-.02.99,0,.15.08.44.2.88.13.44.2.72.23.84.11.44.2,1.09.27,1.96.11,1.18.18,1.86.21,2.03.07.44.19,1.15.38,2.12.19,1.02.3,1.71.35,2.07,0,.06.03.12.07.17,0,.04.01.09.02.13,0,.11.07.2.2.27.13.07.28.11.45.1.08,0,.14-.02.2-.04.08.02.17.03.27.03.17,0,.31-.06.42-.15.11-.09.16-.2.15-.33-.05-.5-.16-1.21-.35-2.12-.21-.95-.33-1.64-.38-2.07h0Z"></path><path d="M70.41,61.51c-2.09,0-3.43.89-4.02,2.48h7.99c-.57-1.6-1.89-2.48-3.98-2.48h0Z"></path><path d="M92.73,107.14c-.02-.06-.03-.11-.04-.17-.11-.31-.29-.62-.53-.92-.25-.3-.46-.56-.66-.76-.19-.2-.52-.45-.96-.75l-.92-.59c-.17-.1-.53-.31-1.1-.61-.56-.3-.88-.47-.95-.5-1.15-.64-1.95-1.22-2.39-1.76-.07-.09-.12-.19-.17-.28-.12-.4-.1-.83.1-1.32.14-.34.42-.69.82-1.04.4-.35.83-.61,1.28-.78.43-.17.82-.26,1.18-.3.13.01.27.03.39.06.48.12.9.41,1.24.86.25.32.49.81.73,1.45.14.43.29.92.43,1.48,0,.04.03.07.05.1.02.07.04.13.05.2.03.13.11.22.25.28.14.06.3.08.47.06.05,0,.09-.02.13-.04.11.03.22.04.35.03.17-.02.31-.09.4-.19.1-.1.13-.2.1-.31-.16-.65-.34-1.23-.51-1.73-.31-.95-.64-1.65-.97-2.08-.52-.65-1.14-1.07-1.89-1.26-.36-.09-.76-.11-1.16-.09-.73-.04-1.54.09-2.42.43-.59.22-1.13.55-1.65,1-.52.45-.86.89-1.05,1.32-.14.36-.22.7-.24,1.02-.02.32.04.63.17.91.03.06.06.12.1.18.01.04.02.08.04.13.13.28.28.54.47.77.18.24.43.47.74.7.31.23.6.43.87.6.27.17.59.36.97.56.14.09.43.25.86.48.44.23.74.4.9.5l.74.44c.35.22.6.4.75.53l.57.5c.24.21.42.42.55.62.11.18.19.36.26.56.15.47.17.82.03,1.04-.21.42-.6.75-1.17,1-.53.22-1.12.35-1.78.4-.59,0-1.14-.07-1.64-.25-.56-.2-.95-.48-1.18-.84-.03-.08-.06-.15-.09-.23-.04-.18-.07-.34-.07-.47,0-.13-.01-.32-.01-.57,0-.24,0-.43-.01-.56,0-.08-.05-.15-.13-.22,0-.02,0-.06,0-.08,0-.11-.07-.21-.2-.29-.13-.08-.28-.12-.44-.11-.08,0-.15.02-.22.04-.08-.02-.17-.04-.26-.03-.16,0-.3.06-.42.15-.12.1-.18.2-.17.31,0,.08,0,.25,0,.51,0,.26,0,.48.01.64,0,.17.04.37.1.6.06.22.13.41.22.57.04.12.09.23.15.32.35.58.92,1.01,1.73,1.29.8.28,1.67.4,2.6.37.05,0,.1,0,.15-.01.11,0,.21,0,.33,0,.93-.03,1.79-.22,2.57-.57.78-.35,1.34-.82,1.67-1.42.2-.42.19-1.01-.06-1.76-.01-.04-.04-.08-.06-.12h0Z"></path><path d="M125.53,104.85c-.1-.27-.18-.61-.22-1.02-.04-.42-.08-.98-.1-1.69-.02-.71-.05-1.25-.07-1.62-.08-1.03-.22-1.83-.43-2.43-.27-.86-.71-1.45-1.32-1.76-.43-.22-.89-.29-1.37-.24-.74-.06-1.54.21-2.41.84-.84.61-1.76,1.56-2.74,2.85-.81,1.09-1.55,2.26-2.24,3.47-.03-.21-.06-.4-.09-.58-.24-1.21-.42-2.54-.55-3.98-.04-.46-.07-.94-.09-1.42,0,0,0-.01,0-.02,0-.1-.01-.19-.02-.29,0-.11-.07-.21-.21-.29-.13-.08-.27-.12-.43-.11-.08,0-.15.02-.22.04-.08-.02-.17-.04-.26-.03-.16,0-.3.06-.42.16-.12.09-.18.19-.17.3.06,1.11.14,2.15.26,3.14.12,1.06.26,2.05.44,2.97.03.08.11.76.26,2.06.15,1.3.37,2.28.68,2.94.05.11.15.19.3.23.15.05.3.05.46.01.01,0,.02-.01.03-.02.14.04.29.05.45.01.16-.04.27-.11.35-.22.07-.11.08-.21.03-.32,0-.02-.01-.04-.02-.06.03-.08.04-.16,0-.24-.2-.42-.33-.9-.44-1.43.15-.06.25-.15.29-.28.14-.4.52-1.15,1.15-2.24.61-1.07,1.3-2.12,2.07-3.14.92-1.22,1.75-2.1,2.5-2.64.37-.27.68-.43.96-.5.06.01.13.02.18.05.4.2.71.63.92,1.28.19.56.32,1.31.39,2.26.02.31.03.84.06,1.58.02.73.06,1.33.11,1.8.04.47.12.85.23,1.14.16.4.55.98,1.18,1.72.1.11.22.17.39.2.13.02.26,0,.38-.04.03,0,.06.02.09.03.16.02.31,0,.46-.06.14-.06.24-.15.28-.26.03-.09.02-.17-.02-.25.01-.02.03-.03.04-.06.04-.11.02-.22-.05-.31-.58-.7-.93-1.21-1.04-1.54h0Z"></path></svg>
        <img class="pcdc-brand-img" alt="" hidden>
      </a>
      <span class="pcdc-copy"></span>
      <button type="button" class="pcdc-dev-link" aria-haspopup="dialog" title="Mode développeur" data-open-dev>.dev</button>
    </div>
    <!-- Centred on the screen (or on the block), whatever sits left and right. -->
    <div class="pcdc-foot-mid">
      <p class="pcdc-slogan" hidden></p>
      <p class="pcdc-sub" hidden></p>
    </div>
    <div class="pcdc-foot-right">
      <a class="pcdc-site"></a>
      <span class="pcdc-qr-slot" aria-hidden="true"></span>
    </div>
    <a class="pcdc-qr" hidden>
      <span class="pcdc-qr-code"></span>
      <span class="pcdc-qr-cap"></span>
    </a>
    <p class="pcdc-qr-hint" aria-hidden="true" hidden>
      <span class="pcdc-qr-hint-text"></span>
      <svg class="pcdc-qr-arrow" viewBox="0 0 120 80" fill="none" stroke="currentColor" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 10C40 4 88 14 104 58"/><path d="M93.3 49 104 58l2.4-13.8"/></svg>
    </p>
  </footer>
  <!-- Way into the developer mode when the footer is switched off. -->
  <button type="button" class="pcdc-dev-corner" aria-haspopup="dialog" title="Mode développeur" data-open-dev hidden>.dev</button>

  <!-- Developer mode: access code, then the administration drawer. -->
  <dialog class="pcdc-dlg is-lock" aria-label="Mode développeur">
    <form class="pcdc-lock pcdc-dlg-body" autocomplete="off">
      <div class="pcdc-dlg-head">
        <div>
          <p class="pcdc-dlg-kicker">Mode développeur</p>
          <p class="pcdc-dlg-title">Code d'accès</p>
        </div>
        <button type="button" class="pcdc-x" data-close aria-label="Fermer">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg>
        </button>
      </div>
      <p class="pcdc-dlg-note pcdc-lock-note" hidden></p>
      <input class="pcdc-field" type="password" name="code" aria-label="Code d'accès" autocomplete="off" autocapitalize="off" spellcheck="false" required>
      <p class="pcdc-err" aria-live="assertive"></p>
      <div class="pcdc-dlg-actions">
        <button type="button" class="pcdc-btn" data-close>Annuler</button>
        <button type="submit" class="pcdc-btn is-primary">Entrer</button>
      </div>
    </form>
    <div class="pcdc-adm" hidden></div>
  </dialog>

  <!-- Administration drawer, cloned on the first unlock. -->
  <template class="pcdc-adm-tpl">
    <div class="pcdc-adm-head">
      <div>
        <p class="pcdc-dlg-kicker">Mode développeur</p>
        <p class="pcdc-dlg-title">Administration</p>
        <span class="pcdc-server" data-state="unknown"></span>
      </div>
      <button type="button" class="pcdc-x" data-close aria-label="Fermer">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg>
      </button>
    </div>
    <div class="pcdc-tabs" role="tablist" aria-label="Sections">
      <button type="button" class="pcdc-tab" role="tab" data-tab="cards" aria-selected="true">Cartes</button>
      <button type="button" class="pcdc-tab" role="tab" data-tab="display" aria-selected="false">Affichage</button>
      <button type="button" class="pcdc-tab" role="tab" data-tab="header" aria-selected="false">En-tête</button>
      <button type="button" class="pcdc-tab" role="tab" data-tab="footer" aria-selected="false">Pied de page</button>
      <button type="button" class="pcdc-tab" role="tab" data-tab="publish" aria-selected="false">Publication</button>
    </div>

    <div class="pcdc-adm-body">
      <!-- Cards: list, then the editor of one card. -->
      <section class="pcdc-panel" data-panel="cards" role="tabpanel">
        <div class="pcdc-cards-view">
          <p class="pcdc-sec-title">Cartes de la vitrine</p>
          <div class="pcdc-adm-cards"></div>
          <div class="pcdc-dlg-actions">
            <button type="button" class="pcdc-btn is-primary" data-add>+ Ajouter une carte</button>
          </div>
        </div>
        <div class="pcdc-editor" hidden></div>
      </section>

      <!-- Settings: built by the script, one control per setting. -->
      <div class="pcdc-panel pcdc-set" data-panel="display" role="tabpanel" hidden></div>
      <div class="pcdc-panel pcdc-set" data-panel="header" role="tabpanel" hidden></div>
      <div class="pcdc-panel pcdc-set" data-panel="footer" role="tabpanel" hidden></div>

      <!-- Publication, versions, import / export, exit. -->
      <section class="pcdc-panel" data-panel="publish" role="tabpanel" hidden>
        <p class="pcdc-sec-title">Version en ligne</p>
        <p class="pcdc-dlg-note pcdc-pub-info"></p>
        <div class="pcdc-dlg-actions is-start">
          <button type="button" class="pcdc-btn is-small" data-versions>Versions précédentes</button>
        </div>
        <div class="pcdc-versions" hidden></div>

        <p class="pcdc-sec-title">Sauvegarde du contenu</p>
        <p class="pcdc-dlg-note">Le fichier JSON contient toutes les cartes et tous les réglages. Avec un serveur, c’est la version publiée qui s’affiche : le contenu collé dans le bloc Elementor ne sert que sans serveur, ou comme réserve à reprendre ici.</p>
        <div class="pcdc-dlg-actions is-start">
          <button type="button" class="pcdc-btn is-small" data-export>Télécharger le JSON</button>
          <button type="button" class="pcdc-btn is-small" data-copy-block>Copier pour le bloc Elementor</button>
          <button type="button" class="pcdc-btn is-small" data-import>Importer un JSON</button>
          <button type="button" class="pcdc-btn is-small" data-from-block>Reprendre le contenu du bloc</button>
        </div>
        <p class="pcdc-help pcdc-io-state" aria-live="polite"></p>

        <p class="pcdc-sec-title">Session</p>
        <div class="pcdc-dlg-actions is-start">
          <button type="button" class="pcdc-btn is-small" data-logout>Quitter le mode développeur</button>
        </div>
      </section>
    </div>

    <div class="pcdc-adm-foot">
      <p class="pcdc-state" aria-live="polite"></p>
      <button type="button" class="pcdc-btn is-small is-quiet" data-discard>Annuler les modifications</button>
      <button type="button" class="pcdc-btn is-primary" data-publish>Publier</button>
    </div>
  </template>

  <!-- Content shown until something is published from the developer mode. -->
  <script type="application/json" class="pcdc-data">
{
  "schema": 2,
  "rev": 0,
  "updatedAt": null,
  "header": {
    "kicker": "PCdlabs · Présent Composé design",
    "title": "Applications & expériences",
    "hint": "Faites glisser pour tourner · Touchez la carte de face pour l'ouvrir",
    "align": "left"
  },
  "settings": {
    "view": "carousel",
    "theme": "light",
    "switch": true,
    "wheel": true,
    "tile": 63,
    "attract": 20,
    "cardLink": "app",
    "newTab": true,
    "font": "inter-tight",
    "bg": ""
  },
  "layout": {
    "header": true,
    "showcase": true,
    "controls": true,
    "footer": true,
    "height": 100,
    "offset": 0,
    "pad": 100,
    "gapHead": 100,
    "gapNav": 100,
    "gapFoot": 100,
    "text": 100,
    "cardSize": 100,
    "cardText": 100,
    "cardNum": true,
    "cardBadge": true,
    "cardKind": true,
    "cardDesc": true,
    "cardTags": true,
    "footLine": true,
    "sloganAnchor": "screen"
  },
  "els": {
    "kicker": {
      "show": false,
      "size": 100,
      "font": "",
      "weight": "",
      "color": ""
    },
    "title": {
      "show": true,
      "size": 100,
      "font": "",
      "weight": "",
      "color": ""
    },
    "switch": {
      "size": 100
    },
    "arrows": {
      "show": true,
      "size": 100
    },
    "caption": {
      "show": true,
      "size": 100,
      "font": "",
      "weight": "",
      "color": ""
    },
    "dots": {
      "show": true,
      "size": 100
    },
    "hint": {
      "show": false,
      "size": 100,
      "font": "",
      "weight": "",
      "color": ""
    },
    "logo": {
      "size": 200
    },
    "copy": {
      "show": true,
      "size": 100,
      "font": "",
      "weight": "",
      "color": ""
    },
    "site": {
      "show": true,
      "size": 100,
      "font": "",
      "weight": "",
      "color": ""
    },
    "slogan": {
      "size": 85,
      "font": "",
      "weight": "",
      "color": ""
    },
    "sub": {
      "show": true,
      "size": 90,
      "font": "",
      "weight": "",
      "color": ""
    },
    "qr": {
      "size": 110
    },
    "qrHint": {
      "show": true,
      "size": 100,
      "font": "",
      "weight": "",
      "color": ""
    }
  },
  "footer": {
    "logo": true,
    "logoUrl": "",
    "copyText": "© PCdlabs · Présent Composé design",
    "siteText": "presentcomposedesign.fr",
    "siteUrl": "https://presentcomposedesign.fr/",
    "slogan": true,
    "sloganText": "Design 360° & Technologies créatives · Toulouse",
    "subText": "Applications web, expériences immersives & réalité augmentée",
    "qr": true,
    "qrUrl": "https://www.linkedin.com/in/alban-desbarax-present-compose-design/",
    "qrCaption": "Contactez moi sur Linkedin",
    "qrHintText": "Retrouvez toutes les apps sur votre téléphone"
  },
  "projects": [
    {
      "id": "enginr",
      "title": "enginR",
      "kind": "Visualiseur 3D · AR · VR",
      "badge": "",
      "description": "Un fichier 3D, dix minutes, une page web immersive. Modèles, gaussian splatting, 360 et données, visibles en AR et en VR sans écrire une ligne de code.",
      "tags": [
        "3D",
        "Gaussian splatting",
        "WebXR",
        "QR code"
      ],
      "icon": "icons/enginr.webp",
      "media": {
        "type": "image",
        "src": "featured/enginr.webp",
        "poster": ""
      },
      "appUrl": "https://pcdlabs.presentcomposedesign.fr/tools/enginR",
      "articleUrl": "",
      "cta": "",
      "accent": "#1001ef",
      "hidden": false
    },
    {
      "id": "birdsignal",
      "title": "BirdSignal",
      "kind": "Bioacoustique",
      "badge": "",
      "description": "Écouter, identifier, collectionner et signaler les oiseaux qui vous entourent, depuis le micro ou un fichier audio. Cartes à collectionner et statut UICN.",
      "tags": [
        "BirdNET",
        "Audio",
        "Collection",
        "PWA"
      ],
      "icon": "icons/birdsignal.webp",
      "media": {
        "type": "video",
        "src": "featured/birdsignal.mp4",
        "poster": "featured/birdsignal.jpg"
      },
      "appUrl": "https://pcdlabs.presentcomposedesign.fr/tools/birdsignal/",
      "articleUrl": "https://presentcomposedesign.fr/birdsignal-infos/",
      "cta": "",
      "accent": "#3a3cf0",
      "hidden": false
    },
    {
      "id": "pcd-1210",
      "title": "PCd-1210",
      "kind": "Platine vinyle virtuelle",
      "badge": "",
      "description": "Une platine direct drive dans le navigateur : bibliothèque de vinyles, scratch et lecteur média, installable sur mobile et ordinateur.",
      "tags": [
        "Audio",
        "Scratch",
        "3D",
        "PWA"
      ],
      "icon": "icons/platine.webp",
      "media": {
        "type": "image",
        "src": "featured/platine.webp",
        "poster": ""
      },
      "appUrl": "https://pcdlabs.presentcomposedesign.fr/experiences/platine-virtuelle/",
      "articleUrl": "https://presentcomposedesign.fr/pcd-1210_infos/",
      "cta": "",
      "accent": "#ff6f91",
      "hidden": false
    },
    {
      "id": "convrt",
      "title": "CONVRT",
      "kind": "Convertisseur de fichiers",
      "badge": "",
      "description": "Convertir images, vidéos, sons et modèles 3D sans rien téléverser : tout se passe dans le navigateur.",
      "tags": [
        "Images",
        "Vidéo",
        "Audio",
        "3D"
      ],
      "icon": "icons/convrt.webp",
      "media": null,
      "appUrl": "https://pcdlabs.presentcomposedesign.fr/tools/CONVRT/",
      "articleUrl": "",
      "cta": "",
      "accent": "#c8fe44",
      "hidden": false
    },
    {
      "id": "le-prompteur",
      "title": "Le Prompteur",
      "kind": "Générateur de prompts",
      "badge": "",
      "description": "Des prompts optimisés pour Mistral, Claude, ChatGPT, Gemini, Suno et Rodin, construits en quelques choix.",
      "tags": [
        "IA",
        "Texte",
        "Image",
        "Musique"
      ],
      "icon": "icons/leprompteur.webp",
      "media": {
        "type": "image",
        "src": "featured/leprompteur.webp",
        "poster": ""
      },
      "appUrl": "https://pcdlabs.presentcomposedesign.fr/tools/leprompteur",
      "articleUrl": "",
      "cta": "",
      "accent": "#ff8a1e",
      "hidden": false
    },
    {
      "id": "spotbaignade",
      "title": "Spotbaignade",
      "kind": "Carte des baignades",
      "badge": "",
      "description": "Lacs, rivières et bases de loisirs pour se baigner autour de vous, en Occitanie. Géolocalisé, gratuit et open source.",
      "tags": [
        "Carte",
        "Géolocalisation",
        "Open source"
      ],
      "icon": "icons/spotbaignade.webp",
      "media": null,
      "appUrl": "https://pcdlabs.presentcomposedesign.fr/tools/Spotbaignade/",
      "articleUrl": "https://presentcomposedesign.fr/spotbaignade/",
      "cta": "",
      "accent": "#1d7fe0",
      "hidden": false
    },
    {
      "id": "splatgate",
      "title": "SplatGate",
      "kind": "Hub VR",
      "badge": "Expérience immersive",
      "description": "Un portail de voyage virtuel pour explorer des paysages réalistes depuis un ordinateur, un mobile ou un casque XR.",
      "tags": [
        "VR",
        "Gaussian splatting",
        "Voyage"
      ],
      "icon": "",
      "media": {
        "type": "video",
        "src": "featured/splatgate.mp4",
        "poster": "featured/splatgate.jpg"
      },
      "appUrl": "",
      "articleUrl": "https://presentcomposedesign.fr/splatgate/",
      "cta": "",
      "accent": "#52b2f9",
      "hidden": false
    },
    {
      "id": "paper-plane",
      "title": "Paper Plane",
      "kind": "Avatar WebXR",
      "badge": "Expérience immersive",
      "description": "Un avatar-avion en papier, simple et universel, pensé pour traverser les mondes immersifs avec légèreté.",
      "tags": [
        "WebXR",
        "Avatar",
        "Arrival.space"
      ],
      "icon": "",
      "media": {
        "type": "video",
        "src": "featured/paperplane.mp4",
        "poster": "featured/paperplane.jpg"
      },
      "appUrl": "",
      "articleUrl": "https://presentcomposedesign.fr/paper-plane-mode_for_arrival-space/",
      "cta": "",
      "accent": "#e79bb0",
      "hidden": false
    },
    {
      "id": "curiosity",
      "title": "Curiosity",
      "kind": "IA culturelle",
      "badge": "",
      "description": "Un oracle culturel français qui invite le public à poser des questions et à explorer un savoir vivant.",
      "tags": [
        "IA",
        "Culture",
        "Médiation"
      ],
      "icon": "",
      "media": {
        "type": "image",
        "src": "featured/curiosity.webp",
        "poster": ""
      },
      "appUrl": "",
      "articleUrl": "https://presentcomposedesign.fr/curiosity/",
      "cta": "",
      "accent": "#7ecf73",
      "hidden": false
    },
    {
      "id": "rizom",
      "title": "RIZOM",
      "kind": "WebXR · hologrammes",
      "badge": "",
      "description": "Une exploration WebXR autour du passage de la vidéo vers des présences 3D et des hologrammes navigables.",
      "tags": [
        "WebXR",
        "Hologrammes",
        "Vidéo volumétrique"
      ],
      "icon": "",
      "media": {
        "type": "video",
        "src": "featured/rizom.mp4",
        "poster": "featured/rizom.jpg"
      },
      "appUrl": "",
      "articleUrl": "https://presentcomposedesign.fr/rizom/",
      "cta": "",
      "accent": "#4b63ff",
      "hidden": false
    },
    {
      "id": "les-moliaires",
      "title": "Les MoliAires",
      "kind": "Réalité augmentée · IA",
      "badge": "Exposition augmentée",
      "description": "Neuf expressions françaises transformées par l’IA en tableaux absurdes et poétiques, visitables en réalité augmentée.",
      "tags": [
        "AR",
        "IA",
        "Exposition"
      ],
      "icon": "",
      "media": {
        "type": "video",
        "src": "featured/moliaires.mp4",
        "poster": "featured/moliaires.jpg"
      },
      "appUrl": "https://pcdlabs.presentcomposedesign.fr/experiences/les-moliaires/",
      "articleUrl": "https://presentcomposedesign.fr/les-moliaires-exposition-augmentee/",
      "cta": "",
      "accent": "#e2564f",
      "hidden": false
    }
  ]
}
  </script>

<script>
(() => {
  'use strict';

  /* ==================================================================
     TOURNIQUET PCdlabs
     Projects shown as a 3D carousel, a grid or a list, with a developer
     mode (".dev" in the footer) to administer everything.

     The content is ONE JSON configuration (schema 2):
       header   texts of the header and of the carousel hint, alignment
       settings view, theme, font, background, behaviour
       layout   sections on / off, height, spacing, sizes, card elements
       els      one entry per element of the page: show, size, font, weight, colour
       footer   logo, texts, slogan, subtitle, QR code and its note
       projects [ { id, title, kind, badge, description, tags[], icon,
                    media: { type: "image" | "gif" | "video", src, poster } | null,
                    appUrl, articleUrl, cta, accent, hidden } ]
     Every field of header / settings / layout / els / footer is described
     once, in SPEC (type, bounds, default): the same table cleans the
     configuration here and on the server (showcase.php).
     Media paths are relative to data-media-base, or full https URLs.

     Where it comes from, highest priority first:
       1. the developer's working copy (only in the developer's own browser,
          while the developer mode is open, until it is published);
       2. the version published on the server (data-api);
       3. the JSON embedded in this block (<script class="pcdc-data">).
     ================================================================== */

  const UI = {
    openApp: 'Ouvrir l’application',
    discover: 'Découvrir le projet',
    readArticle: 'Lire l’article',
    badgeApp: 'Application',
    badgeProject: 'Projet',
    goTo: 'Afficher',
    empty: 'Aucune carte à afficher pour le moment.',
  };

  const ARROW_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7M9 7h8v8"/></svg>';
  const ICONS = {
    up: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 15l6-6 6 6"/></svg>',
    down: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>',
    eye: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>',
    eyeOff: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3l18 18M10.6 5.1A10.8 10.8 0 0 1 12 5c6.4 0 10 7 10 7a17.7 17.7 0 0 1-3.2 4.2M6.6 6.6C3.8 8.4 2 12 2 12s3.6 7 10 7a9.7 9.7 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2"/></svg>',
    edit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20h4L19 9l-4-4L4 16v4z"/><path d="M13.5 6.5l4 4"/></svg>',
  };
  const CHEVRON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 5l7 7-7 7"/></svg>';
  const TAU = Math.PI * 2;

  /* Every setting: ["bool", d] ["int", min, max, d] ["enum", list, d]
     ["str", max, d] ["link", d] ["media", d] ["color", d]. Generated from
     spec.py, identical to the table of showcase.php. */
  const SPEC = {"header":{"kicker":["str",120,"PCdlabs · Présent Composé design"],"title":["str",120,"Applications & expériences"],"hint":["str",160,"Faites glisser pour tourner · Touchez la carte de face pour l'ouvrir"],"align":["enum",["left","center"],"left"]},"settings":{"view":["enum",["carousel","grid","list"],"carousel"],"theme":["enum",["auto","dark","light"],"dark"],"switch":["bool",true],"wheel":["bool",false],"tile":["int",40,70,58],"attract":["int",0,600,20],"cardLink":["enum",["app","article"],"app"],"newTab":["bool",true],"font":["enum",["inter-tight","inter","manrope","dm-sans","space-grotesk","syne","unbounded","montserrat","poppins","bricolage","fraunces","playfair","dm-serif","instrument-serif","bebas","caveat","jetbrains","system"],"inter-tight"],"bg":["color",""]},"layout":{"header":["bool",true],"showcase":["bool",true],"controls":["bool",true],"footer":["bool",true],"height":["int",0,100,100],"offset":["int",-30,30,0],"pad":["int",0,250,100],"gapHead":["int",0,300,100],"gapNav":["int",0,300,100],"gapFoot":["int",0,300,100],"text":["int",70,170,100],"cardSize":["int",50,150,100],"cardText":["int",70,160,100],"cardNum":["bool",true],"cardBadge":["bool",true],"cardKind":["bool",true],"cardDesc":["bool",true],"cardTags":["bool",true],"footLine":["bool",true],"sloganAnchor":["enum",["screen","block"],"screen"],"bgNone":["bool",false]},"els":{"kicker":{"show":["bool",true],"size":["int",40,300,100],"font":["enum",["","inter-tight","inter","manrope","dm-sans","space-grotesk","syne","unbounded","montserrat","poppins","bricolage","fraunces","playfair","dm-serif","instrument-serif","bebas","caveat","jetbrains","system"],""],"weight":["enum",["","300","400","500","600","700","800","900"],""],"color":["color",""]},"title":{"show":["bool",true],"size":["int",40,300,100],"font":["enum",["","inter-tight","inter","manrope","dm-sans","space-grotesk","syne","unbounded","montserrat","poppins","bricolage","fraunces","playfair","dm-serif","instrument-serif","bebas","caveat","jetbrains","system"],""],"weight":["enum",["","300","400","500","600","700","800","900"],""],"color":["color",""]},"switch":{"size":["int",40,300,100]},"arrows":{"show":["bool",true],"size":["int",40,300,100]},"caption":{"show":["bool",true],"size":["int",40,300,100],"font":["enum",["","inter-tight","inter","manrope","dm-sans","space-grotesk","syne","unbounded","montserrat","poppins","bricolage","fraunces","playfair","dm-serif","instrument-serif","bebas","caveat","jetbrains","system"],""],"weight":["enum",["","300","400","500","600","700","800","900"],""],"color":["color",""]},"dots":{"show":["bool",true],"size":["int",40,300,100]},"hint":{"show":["bool",true],"size":["int",40,300,100],"font":["enum",["","inter-tight","inter","manrope","dm-sans","space-grotesk","syne","unbounded","montserrat","poppins","bricolage","fraunces","playfair","dm-serif","instrument-serif","bebas","caveat","jetbrains","system"],""],"weight":["enum",["","300","400","500","600","700","800","900"],""],"color":["color",""]},"logo":{"size":["int",40,300,100]},"copy":{"show":["bool",true],"size":["int",40,300,100],"font":["enum",["","inter-tight","inter","manrope","dm-sans","space-grotesk","syne","unbounded","montserrat","poppins","bricolage","fraunces","playfair","dm-serif","instrument-serif","bebas","caveat","jetbrains","system"],""],"weight":["enum",["","300","400","500","600","700","800","900"],""],"color":["color",""]},"site":{"show":["bool",true],"size":["int",40,300,100],"font":["enum",["","inter-tight","inter","manrope","dm-sans","space-grotesk","syne","unbounded","montserrat","poppins","bricolage","fraunces","playfair","dm-serif","instrument-serif","bebas","caveat","jetbrains","system"],""],"weight":["enum",["","300","400","500","600","700","800","900"],""],"color":["color",""]},"slogan":{"size":["int",40,300,100],"font":["enum",["","inter-tight","inter","manrope","dm-sans","space-grotesk","syne","unbounded","montserrat","poppins","bricolage","fraunces","playfair","dm-serif","instrument-serif","bebas","caveat","jetbrains","system"],""],"weight":["enum",["","300","400","500","600","700","800","900"],""],"color":["color",""]},"sub":{"show":["bool",true],"size":["int",40,300,100],"font":["enum",["","inter-tight","inter","manrope","dm-sans","space-grotesk","syne","unbounded","montserrat","poppins","bricolage","fraunces","playfair","dm-serif","instrument-serif","bebas","caveat","jetbrains","system"],""],"weight":["enum",["","300","400","500","600","700","800","900"],""],"color":["color",""]},"qr":{"size":["int",40,300,100]},"qrHint":{"show":["bool",true],"size":["int",40,300,100],"font":["enum",["","inter-tight","inter","manrope","dm-sans","space-grotesk","syne","unbounded","montserrat","poppins","bricolage","fraunces","playfair","dm-serif","instrument-serif","bebas","caveat","jetbrains","system"],""],"weight":["enum",["","300","400","500","600","700","800","900"],""],"color":["color",""]}},"footer":{"logo":["bool",true],"logoUrl":["media",""],"copyText":["str",120,"© PCdlabs · Présent Composé design"],"siteText":["str",80,"presentcomposedesign.fr"],"siteUrl":["link","https://presentcomposedesign.fr/"],"slogan":["bool",true],"sloganText":["str",140,"Design 360° & technologies créatives · Toulouse"],"subText":["str",160,"Applications web, expériences immersives & réalité augmentée"],"qr":["bool",false],"qrUrl":["link",""],"qrCaption":["str",40,"Scannez-moi"],"qrHintText":["str",120,"Retrouvez toutes les apps sur votre téléphone"]}};

  /* Fonts offered in the developer mode: [family, Google Fonts query, fallback].
     A font is downloaded only once something uses it. */
  const FONTS = {
    'inter-tight': ['Inter Tight', 'Inter+Tight:wght@300;400;500;600;700;800;900', 'system-ui, sans-serif'],
    inter: ['Inter', 'Inter:wght@300;400;500;600;700;800;900', 'system-ui, sans-serif'],
    manrope: ['Manrope', 'Manrope:wght@300;400;500;600;700;800', 'system-ui, sans-serif'],
    'dm-sans': ['DM Sans', 'DM+Sans:wght@300;400;500;600;700;800;900', 'system-ui, sans-serif'],
    'space-grotesk': ['Space Grotesk', 'Space+Grotesk:wght@300;400;500;600;700', 'system-ui, sans-serif'],
    syne: ['Syne', 'Syne:wght@400;500;600;700;800', 'system-ui, sans-serif'],
    unbounded: ['Unbounded', 'Unbounded:wght@300;400;500;600;700;800;900', 'system-ui, sans-serif'],
    montserrat: ['Montserrat', 'Montserrat:wght@300;400;500;600;700;800;900', 'system-ui, sans-serif'],
    poppins: ['Poppins', 'Poppins:wght@300;400;500;600;700;800;900', 'system-ui, sans-serif'],
    bricolage: ['Bricolage Grotesque', 'Bricolage+Grotesque:wght@300;400;500;600;700;800', 'system-ui, sans-serif'],
    fraunces: ['Fraunces', 'Fraunces:wght@300;400;500;600;700;800;900', 'Georgia, serif'],
    playfair: ['Playfair Display', 'Playfair+Display:wght@400;500;600;700;800;900', 'Georgia, serif'],
    'dm-serif': ['DM Serif Display', 'DM+Serif+Display', 'Georgia, serif'],
    'instrument-serif': ['Instrument Serif', 'Instrument+Serif', 'Georgia, serif'],
    bebas: ['Bebas Neue', 'Bebas+Neue', 'Impact, sans-serif'],
    caveat: ['Caveat', 'Caveat:wght@400;500;600;700', 'cursive'],
    jetbrains: ['JetBrains Mono', 'JetBrains+Mono:wght@300;400;500;600;700;800', 'ui-monospace, monospace'],
    system: ['', '', 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif'],
  };
  const fontStack = (k) => { const f = FONTS[k]; return !f ? '' : f[0] ? `"${f[0]}", ${f[2]}` : f[2]; };
  const fontLabel = (k) => (FONTS[k] && FONTS[k][0]) || 'Police du système';
  function loadFont(k) {
    const f = FONTS[k];
    if (!f || !f[1]) return;
    // The block itself links Inter Tight (400 to 800).
    if (document.querySelector(`link[data-pcdc-font="${k}"]`)) return;
    const l = document.createElement('link');
    l.rel = 'stylesheet';
    l.href = `https://fonts.googleapis.com/css2?family=${f[1]}&display=swap`;
    l.dataset.pcdcFont = k;
    document.head.append(l);
  }

  /* Schema 1 had no badge. Corrections asked for on 2026-09-27 for three
     cards already published, applied when their badge has never been set. */
  const LEGACY_BADGES = { splatgate: 'Expérience immersive', 'paper-plane': 'Expérience immersive', 'les-moliaires': 'Exposition augmentée' };
  const VIEWS = ['carousel', 'grid', 'list'];
  const VIEW_PARAM = { carrousel: 'carousel', grille: 'grid', liste: 'list', carousel: 'carousel', grid: 'grid', list: 'list' };
  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
  const noHover = window.matchMedia('(hover: none)');

  /* Offline unlock only (no server reachable): a slow PBKDF2 fingerprint of
     the access code. When a server answers, the server alone checks the code. */
  const DEV_KDF = { salt: '9ef7933f6a148b5e13f2d200c057a50c', iter: 600000, hash: '1c09e8aefde1cfbaa2c42f12e73a789ae9db0bfb2dd3ae936f57b5a29e82dd90' };

  /* Browser storage: conveniences only (last view, cache, draft of the
     developer). Every access may fail (private mode): wrapped. */
  const store = {
    get(k) { try { return window.localStorage.getItem(k); } catch (e) { return null; } },
    set(k, v) { try { window.localStorage.setItem(k, v); return true; } catch (e) { return false; } },
    del(k) { try { window.localStorage.removeItem(k); } catch (e) { /* ignore */ } },
  };
  const sess = {
    get(k) { try { return window.sessionStorage.getItem(k); } catch (e) { return null; } },
    set(k, v) { try { window.sessionStorage.setItem(k, v); } catch (e) { /* ignore */ } },
    del(k) { try { window.sessionStorage.removeItem(k); } catch (e) { /* ignore */ } },
  };

  /* Tiny DOM helper: h('a', { class: 'x', href }, child, 'text'). */
  function h(tag, attrs, ...children) {
    const el = document.createElement(tag);
    for (const [k, v] of Object.entries(attrs || {})) {
      if (v === null || v === undefined || v === false) continue;
      el.setAttribute(k, v === true ? '' : String(v));
    }
    for (const c of children) if (c !== null && c !== undefined && c !== false) el.append(c);
    return el;
  }

  function hexRgb(hex) {
    const m = /^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(hex || '');
    return m ? m.slice(1).map((x) => parseInt(x, 16)) : [16, 1, 239];
  }
  const rgba = (c, a) => `rgba(${c[0]}, ${c[1]}, ${c[2]}, ${a})`;
  const tint = (c, t) => `rgb(${c.map((v) => Math.round(v + (255 - v) * t)).join(', ')})`;
  const deepen = (c, t) => `rgb(${c.map((v) => Math.round(v * (1 - t))).join(', ')})`;
  const clone = (o) => JSON.parse(JSON.stringify(o));
  const slugify = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '')
    .toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 32);

    /* ------------------------------------------------------------------
     QR code encoder (ISO/IEC 18004), byte mode, error correction level M.
     Written from the standard and checked against three independent
     decoders (zxing-cpp, ZBar, OpenCV). Returns a square boolean matrix
     (true = dark module), without the quiet zone, or null if the text is
     too long for a version 40 symbol.
     ------------------------------------------------------------------ */
  const QR = (() => {
    // Level M, indexed by version (1 to 40).
    const ECC_PER_BLOCK = [0, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28];
    const NUM_BLOCKS = [0, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49];
    const ECL_BITS_M = 0;                          // format bits of level M

    // Modules available for data and error correction in a given version.
    function rawDataModules(ver) {
      let r = (16 * ver + 128) * ver + 64;
      if (ver >= 2) {
        const na = Math.floor(ver / 7) + 2;
        r -= (25 * na - 10) * na - 55;
        if (ver >= 7) r -= 36;
      }
      return r;
    }
    const dataCodewords = (ver) => Math.floor(rawDataModules(ver) / 8) - ECC_PER_BLOCK[ver] * NUM_BLOCKS[ver];

    // Arithmetic in GF(256), reducing polynomial x^8 + x^4 + x^3 + x^2 + 1.
    function gfMul(x, y) {
      let z = 0;
      for (let i = 7; i >= 0; i--) {
        z = (z << 1) ^ ((z >>> 7) * 0x11d);
        z ^= ((y >>> i) & 1) * x;
      }
      return z & 0xff;
    }
    function rsDivisor(degree) {
      const res = new Array(degree).fill(0);
      res[degree - 1] = 1;
      let root = 1;
      for (let i = 0; i < degree; i++) {
        for (let j = 0; j < degree; j++) {
          res[j] = gfMul(res[j], root);
          if (j + 1 < degree) res[j] ^= res[j + 1];
        }
        root = gfMul(root, 0x02);
      }
      return res;
    }
    function rsRemainder(data, div) {
      const res = div.map(() => 0);
      for (const b of data) {
        const factor = b ^ res.shift();
        res.push(0);
        div.forEach((coef, i) => { res[i] ^= gfMul(coef, factor); });
      }
      return res;
    }

    function utf8(text) {
      return Array.from(new TextEncoder().encode(text));
    }

    function encode(text) {
      const bytes = utf8(text);
      const eci = bytes.some((b) => b > 0x7f);     // non-ASCII: declare UTF-8 (ECI 26)
      // Smallest version that holds the segment.
      let ver = 1;
      let bitLen = 0;
      for (; ver <= 40; ver++) {
        const ccBits = ver <= 9 ? 8 : 16;
        bitLen = (eci ? 12 : 0) + 4 + ccBits + bytes.length * 8;
        if (bitLen <= dataCodewords(ver) * 8) break;
      }
      if (ver > 40) return null;

      // Bit stream: [ECI] mode, count, data, terminator, padding.
      const bits = [];
      const put = (val, len) => { for (let i = len - 1; i >= 0; i--) bits.push((val >>> i) & 1); };
      if (eci) { put(0x7, 4); put(26, 8); }
      put(0x4, 4);
      put(bytes.length, ver <= 9 ? 8 : 16);
      bytes.forEach((b) => put(b, 8));
      const capacity = dataCodewords(ver) * 8;
      put(0, Math.min(4, capacity - bits.length));
      put(0, (8 - (bits.length % 8)) % 8);
      for (let pad = 0xec; bits.length < capacity; pad ^= 0xec ^ 0x11) put(pad, 8);
      const data = [];
      for (let i = 0; i < bits.length; i += 8) {
        let b = 0;
        for (let j = 0; j < 8; j++) b = (b << 1) | bits[i + j];
        data.push(b);
      }

      // Split into blocks, add error correction, interleave.
      const numBlocks = NUM_BLOCKS[ver];
      const eccLen = ECC_PER_BLOCK[ver];
      const raw = Math.floor(rawDataModules(ver) / 8);
      const numShort = numBlocks - (raw % numBlocks);
      const shortLen = Math.floor(raw / numBlocks);
      const div = rsDivisor(eccLen);
      const blocks = [];
      for (let i = 0, k = 0; i < numBlocks; i++) {
        const dat = data.slice(k, k + shortLen - eccLen + (i < numShort ? 0 : 1));
        k += dat.length;
        const ecc = rsRemainder(dat, div);
        if (i < numShort) dat.push(0);
        blocks.push(dat.concat(ecc));
      }
      const codewords = [];
      for (let i = 0; i < blocks[0].length; i++) {
        blocks.forEach((blk, j) => {
          if (i !== shortLen - eccLen || j >= numShort) codewords.push(blk[i]);
        });
      }

      // Matrix with function patterns.
      const size = ver * 4 + 17;
      const mods = Array.from({ length: size }, () => new Array(size).fill(false));
      const isFn = Array.from({ length: size }, () => new Array(size).fill(false));
      const setFn = (x, y, dark) => { mods[y][x] = dark; isFn[y][x] = true; };

      for (let i = 0; i < size; i++) { setFn(6, i, i % 2 === 0); setFn(i, 6, i % 2 === 0); }
      const finder = (cx, cy) => {
        for (let dy = -4; dy <= 4; dy++) {
          for (let dx = -4; dx <= 4; dx++) {
            const x = cx + dx;
            const y = cy + dy;
            if (x < 0 || y < 0 || x >= size || y >= size) continue;
            const d = Math.max(Math.abs(dx), Math.abs(dy));
            setFn(x, y, d !== 2 && d !== 4);
          }
        }
      };
      finder(3, 3); finder(size - 4, 3); finder(3, size - 4);

      if (ver > 1) {
        const na = Math.floor(ver / 7) + 2;
        const stepA = ver === 32 ? 26 : Math.ceil((ver * 4 + 4) / (na * 2 - 2)) * 2;
        const pos = [6];
        for (let p = size - 7; pos.length < na; p -= stepA) pos.splice(1, 0, p);
        pos.forEach((ay, i) => pos.forEach((ax, j) => {
          if ((i === 0 && j === 0) || (i === 0 && j === na - 1) || (i === na - 1 && j === 0)) return;
          for (let dy = -2; dy <= 2; dy++) {
            for (let dx = -2; dx <= 2; dx++) setFn(ax + dx, ay + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
          }
        }));
      }

      const drawFormat = (mask) => {
        const dat = (ECL_BITS_M << 3) | mask;
        let rem = dat;
        for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
        const f = ((dat << 10) | rem) ^ 0x5412;
        const bit = (i) => ((f >>> i) & 1) === 1;
        for (let i = 0; i <= 5; i++) setFn(8, i, bit(i));
        setFn(8, 7, bit(6));
        setFn(8, 8, bit(7));
        setFn(7, 8, bit(8));
        for (let i = 9; i < 15; i++) setFn(14 - i, 8, bit(i));
        for (let i = 0; i < 8; i++) setFn(size - 1 - i, 8, bit(i));
        for (let i = 8; i < 15; i++) setFn(8, size - 15 + i, bit(i));
        setFn(8, size - 8, true);                  // the dark module
      };
      drawFormat(0);                               // reserves the format area

      if (ver >= 7) {
        let rem = ver;
        for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);
        const v = (ver << 12) | rem;
        for (let i = 0; i < 18; i++) {
          const dark = ((v >>> i) & 1) === 1;
          const a = size - 11 + (i % 3);
          const b = Math.floor(i / 3);
          setFn(a, b, dark);
          setFn(b, a, dark);
        }
      }

      // Data, in the two-column zigzag from the bottom-right corner.
      let bi = 0;
      for (let right = size - 1; right >= 1; right -= 2) {
        if (right === 6) right = 5;
        for (let vert = 0; vert < size; vert++) {
          for (let j = 0; j < 2; j++) {
            const x = right - j;
            const upward = ((right + 1) & 2) === 0;
            const y = upward ? size - 1 - vert : vert;
            if (!isFn[y][x] && bi < codewords.length * 8) {
              mods[y][x] = ((codewords[bi >>> 3] >>> (7 - (bi & 7))) & 1) === 1;
              bi++;
            }
          }
        }
      }

      const MASKS = [
        (x, y) => (x + y) % 2 === 0,
        (x, y) => y % 2 === 0,
        (x) => x % 3 === 0,
        (x, y) => (x + y) % 3 === 0,
        (x, y) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0,
        (x, y) => ((x * y) % 2) + ((x * y) % 3) === 0,
        (x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0,
        (x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0,
      ];
      const applyMask = (m) => {
        for (let y = 0; y < size; y++) {
          for (let x = 0; x < size; x++) if (!isFn[y][x] && MASKS[m](x, y)) mods[y][x] = !mods[y][x];
        }
      };

      // Penalty rules of the standard: runs, 2x2 blocks, finder look-alikes, balance.
      const penalty = () => {
        let p = 0;
        const line = (get) => {
          let s = 0;
          for (let a = 0; a < size; a++) {
            let run = 1;
            for (let b = 1; b < size; b++) {
              if (get(a, b) === get(a, b - 1)) { run++; if (run === 5) s += 3; else if (run > 5) s++; } else run = 1;
            }
            for (let b = 0; b + 11 <= size; b++) {
              const w = [];
              for (let k = 0; k < 11; k++) w.push(get(a, b + k) ? 1 : 0);
              const t = w.join('');
              if (t === '10111010000' || t === '00001011101') s += 40;
            }
          }
          return s;
        };
        p += line((a, b) => mods[a][b]);
        p += line((a, b) => mods[b][a]);
        let dark = 0;
        for (let y = 0; y < size; y++) {
          for (let x = 0; x < size; x++) {
            if (mods[y][x]) dark++;
            if (x < size - 1 && y < size - 1) {
              const c = mods[y][x];
              if (c === mods[y][x + 1] && c === mods[y + 1][x] && c === mods[y + 1][x + 1]) p += 3;
            }
          }
        }
        p += Math.floor(Math.abs(dark * 20 - size * size * 10) / (size * size)) * 10;
        return p;
      };

      let best = 0;
      let bestScore = Infinity;
      for (let m = 0; m < 8; m++) {
        applyMask(m);
        drawFormat(m);
        const sc = penalty();
        if (sc < bestScore) { bestScore = sc; best = m; }
        applyMask(m);                              // XOR again: back to unmasked
      }
      applyMask(best);
      drawFormat(best);
      return mods;
    }

    // SVG with the mandatory 4-module quiet zone, one path for every dark module.
    function svg(text) {
      const m = encode(text);
      if (!m) return '';
      const n = m.length;
      const q = 4;
      let d = '';
      for (let y = 0; y < n; y++) {
        for (let x = 0; x < n; x++) if (m[y][x]) d += `M${x + q} ${y + q}h1v1h-1z`;
      }
      return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n + 2 * q} ${n + 2 * q}" shape-rendering="crispEdges" aria-hidden="true"><rect width="100%" height="100%" fill="#fff"/><path d="${d}" fill="#0b0e1c"/></svg>`;
    }

    return { encode, svg };
  })();

  /* ------------------------------------------------------------------
     Configuration clean-up. Mirrors the server whitelist (showcase.php):
     every field is typed, bounded, and links can only be http(s), so a
     configuration can never inject a script or a style.
     ------------------------------------------------------------------ */
  // Same rules as the PHP side: control characters become a space, only
  // spaces are trimmed (as PHP's trim), and the length counts characters.
  const str = (v, max) => Array.from((typeof v === 'string' || typeof v === 'number' ? String(v) : '')
    .replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/^ +| +$/g, '')).slice(0, max).join('');
  const oneOf = (v, list, dflt) => (list.includes(v) ? v : dflt);
  const bool = (v, dflt) => (typeof v === 'boolean' ? v : dflt);
  // A number, or a numeric string as PHP's is_numeric reads it ("0x10" is
  // not one); halves round away from zero, as PHP's round. Else: the default.
  const NUMERIC = /^[ \t\n\r\v\f]*[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?[ \t\n\r\v\f]*$/;
  const int = (v, lo, hi, dflt) => {
    if (typeof v === 'string' && NUMERIC.test(v)) v = Number(v);
    if (typeof v !== 'number' || !Number.isFinite(v)) return dflt;
    return Math.min(hi, Math.max(lo, Math.sign(v) * Math.round(Math.abs(v))));
  };
  const link = (v) => { const s = str(v, 500); return /^https?:\/\/[^ \t\n\r\v\f"'<>\\]+$/i.test(s) ? s : ''; };
  const mediaPath = (v) => {
    const s = str(v, 500);
    if (/^https?:\/\/[^ \t\n\r\v\f"'<>\\()]+$/i.test(s)) return s;
    return /^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(s) && !s.includes('..') ? s : '';
  };
  const color = (v) => (/^#[0-9a-f]{6}$/i.test(v || '') ? v.toLowerCase() : '#1001ef');
  const obj = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});

  // One value by its rule, or an object by its spec (only the keys of the spec).
  function cleanBy(rule, v) {
    if (!Array.isArray(rule)) {
      const o = obj(v);
      const out = {};
      for (const k of Object.keys(rule)) out[k] = cleanBy(rule[k], o[k]);
      return out;
    }
    switch (rule[0]) {
      case 'bool': return bool(v, rule[1]);
      case 'int': return int(v, rule[1], rule[2], rule[3]);
      case 'enum': return oneOf(v, rule[1], rule[2]);
      case 'str': return typeof v === 'string' ? str(v, rule[1]) : rule[2];
      case 'link': return typeof v === 'string' ? link(v) : rule[1];
      case 'media': return typeof v === 'string' ? mediaPath(v) : rule[1];
      case 'color': return typeof v === 'string' && /^#[0-9a-f]{6}$/i.test(v) ? v.toLowerCase() : rule[1];
      default: return null;
    }
  }

  function cleanConfig(raw) {
    const r = obj(raw);
    const seen = new Set();
    const projects = [];
    for (const item of (Array.isArray(r.projects) ? r.projects : []).slice(0, 60)) {
      const p = obj(item);
      const title = str(p.title, 80);
      if (!title) continue;
      let id = slugify(str(p.id, 40)) || slugify(title) || 'carte';
      for (let k = 2, base = id; seen.has(id); k++) id = `${base}-${k}`;
      seen.add(id);
      const m = obj(p.media);
      const type = oneOf(m.type, ['image', 'gif', 'video'], '');
      const src = mediaPath(m.src);
      const legacy = typeof p.badge !== 'string';
      let kind = str(p.kind, 80);
      // Same correction: the badge now says "Exposition augmentée", the line
      // above the title no longer repeats it.
      if (legacy && id === 'les-moliaires' && kind === 'Exposition augmentée') kind = 'Réalité augmentée · IA';
      projects.push({
        id,
        title,
        kind,
        badge: legacy ? (LEGACY_BADGES[id] || '') : str(p.badge, 40),
        description: str(p.description, 600),
        tags: (Array.isArray(p.tags) ? p.tags : []).slice(0, 10).map((t) => str(t, 40)).filter(Boolean),
        icon: mediaPath(p.icon),
        media: type && src ? { type, src, poster: mediaPath(m.poster) } : null,
        appUrl: link(p.appUrl),
        articleUrl: link(p.articleUrl),
        cta: str(p.cta, 40),
        accent: color(p.accent),
        hidden: bool(p.hidden, false),
      });
    }
    return {
      schema: 2,
      rev: Number.isInteger(r.rev) && r.rev >= 0 ? r.rev : 0,
      updatedAt: str(r.updatedAt, 40) || null,
      ...cleanBy(SPEC, r),
      projects,
    };
  }
  // Content only (without revision and date), to compare two versions.
  const contentKey = (c) => JSON.stringify({ header: c.header, settings: c.settings, layout: c.layout, els: c.els, footer: c.footer, projects: c.projects });

  // Elementor may run this script again when the widget is edited: only
  // blocks that are not yet initialised get wired.
  document.querySelectorAll('.pcdc:not([data-pcdc-ready])').forEach(init);

  function init(root) {
    root.setAttribute('data-pcdc-ready', '');

    const mediaBase = root.dataset.mediaBase || '';
    /* One block can show only a part of the showcase, whatever the settings:
       data-part="footer"   the footer banner alone (a second widget, a video overlay)
       data-part="showcase" header + cards + controls, without the footer
       data-transparent     no background of its own (the page shows through) */
    const part = oneOf((root.dataset.part || '').toLowerCase(), ['footer', 'showcase'], '');
    const forcedTransparent = root.hasAttribute('data-transparent');
    const apiUrl = root.dataset.api || '';
    const KEY = {
      view: 'pcdc-view',
      cache: 'pcdc-published:' + (apiUrl || 'local'),
      draft: 'pcdc-draft:' + (apiUrl || 'local'),
      token: 'pcdc-token:' + (apiUrl || 'local'),
      local: 'pcdc-local-unlock',
    };
    /* Relative media paths are resolved against data-media-base. */
    const resolve = (p) => (!p ? '' : /^https?:\/\//i.test(p) ? p : mediaBase + p);

    const $ = (sel) => root.querySelector(sel);
    const stage = $('.pcdc-stage');
    const track = $('.pcdc-track');
    const views = $('.pcdc-views');
    const switchBtns = [...root.querySelectorAll('.pcdc-switch-btn')];
    const dotsBox = $('.pcdc-dots');
    const capCount = $('.pcdc-count');
    const capTitle = $('.pcdc-cap-title');
    const capKind = $('.pcdc-cap-kind');
    const head = $('.pcdc-head');
    const kickerEl = $('.pcdc-kicker');
    const titleEl = $('.pcdc-h');
    const nav = $('.pcdc-nav');
    const arrowBtns = [...root.querySelectorAll('.pcdc-arrow')];
    const caption = $('.pcdc-caption');
    const hintEl = $('.pcdc-hint');
    const foot = $('.pcdc-foot');
    const footLeft = $('.pcdc-foot-left');
    const footMid = $('.pcdc-foot-mid');
    const footRight = $('.pcdc-foot-right');
    const brand = $('.pcdc-brand');
    const brandSvg = brand.querySelector('svg');
    const brandImg = $('.pcdc-brand-img');
    const copyEl = $('.pcdc-copy');
    const siteEl = $('.pcdc-site');
    const sloganEl = $('.pcdc-slogan');
    const subEl = $('.pcdc-sub');
    const qrLink = $('.pcdc-qr');
    const qrCode = $('.pcdc-qr-code');
    const qrCap = $('.pcdc-qr-cap');
    const qrHint = $('.pcdc-qr-hint');
    const qrHintText = $('.pcdc-qr-hint-text');
    const devCorner = $('.pcdc-dev-corner');
    const draftBadge = $('.pcdc-draft');
    const dlg = $('.pcdc-dlg');

    /* ------------------------- content state ------------------------- */
    let embedded;
    try { embedded = cleanConfig(JSON.parse($('script.pcdc-data').textContent)); } catch (e) { embedded = cleanConfig({}); }
    let pub = embedded;                  // what visitors see
    let serverState = apiUrl ? 'unknown' : 'none';
    /* Version of showcase.php. Version 1 (the v3 file) accepts a publication
       but silently drops every setting it does not know: publishing to it is
       refused, so nothing is ever lost again. */
    const API_NEEDED = 2;
    let serverApi = 0;
    const serverOutdated = () => serverState === 'online' && serverApi > 0 && serverApi < API_NEEDED;
    let work = null;                     // developer working copy
    let workBase = 0;                    // revision the working copy started from
    let conf = pub;                      // what is on screen

    try {
      const cached = JSON.parse(store.get(KEY.cache) || 'null');
      if (apiUrl && cached) pub = cleanConfig(cached);
    } catch (e) { /* no cache */ }

    let token = sess.get(KEY.token) || '';
    let localMode = sess.get(KEY.local) === '1';
    const unlocked = () => Boolean(token) || localMode;

    /* --------------------------- card building --------------------------- */
    let cards = [];
    let dots = [];
    let n = 0;
    let step = TAU;
    let cardsKey = '';
    let cardIO = null;

    function mediaEl(m, cls, role) {
      if (m.type === 'video') {
        // No src yet: a video is only downloaded the first time it has to play.
        const v = h('video', {
          class: cls, muted: true, loop: true, playsinline: true, preload: 'none',
          poster: resolve(m.poster) || null, 'aria-hidden': 'true', disablepictureinpicture: true,
        });
        v.muted = true;
        v.dataset.src = resolve(m.src);
        v.dataset.role = role;
        return v;
      }
      return h('img', { class: cls, src: resolve(m.src), alt: '', decoding: 'async', draggable: 'false' });
    }

    function buildCard(p, i) {
      const rgb = hexRgb(p.accent);
      const primaryUrl = p.appUrl || p.articleUrl || '#';
      const still = p.media ? resolve(p.media.type === 'image' ? p.media.src : (p.media.poster || p.media.src)) : '';
      const num = String(i + 1).padStart(2, '0');

      const el = h('article', { class: 'pcdc-card', 'data-index': i });
      el.style.setProperty('--pcdc-accent', p.accent);
      el.style.setProperty('--pcdc-accent-soft', rgba(rgb, .55));
      el.style.setProperty('--pcdc-accent-glow', rgba(rgb, .65));
      // Accent text: lightened on dark cards, darkened on light cards (see the theme rules).
      el.style.setProperty('--pcdc-accent-light', tint(rgb, .5));
      el.style.setProperty('--pcdc-accent-deep', deepen(rgb, .45));

      // Featured media behind everything.
      const bg = h('div', { class: 'pcdc-bg' });
      if (p.icon && p.media) {
        bg.append(mediaEl(p.media, 'pcdc-bg-media', 'bg'));
      } else if (!p.icon && still) {
        bg.classList.add('is-blurred');
        bg.append(h('img', { class: 'pcdc-bg-media', src: still, alt: '', decoding: 'async', draggable: 'false' }));
      } else if (p.icon) {
        bg.classList.add('is-icon');
        bg.append(h('img', { class: 'pcdc-bg-media', src: resolve(p.icon), alt: '', decoding: 'async', draggable: 'false' }));
      }

      // Glass tile: the icon, or the media itself when there is no icon.
      const tileBlur = h('div', { class: 'pcdc-tile-blur' });
      if (still) tileBlur.style.setProperty('--pcdc-still', `url("${still}")`);
      let content;
      if (p.icon) {
        content = h('div', { class: 'pcdc-tile-content is-icon' },
          h('img', { src: resolve(p.icon), alt: '', decoding: 'async', draggable: 'false' }));
      } else if (p.media) {
        content = h('div', { class: 'pcdc-tile-content is-media' }, mediaEl(p.media, 'pcdc-tile-media', 'tile'));
      } else {
        content = h('div', { class: 'pcdc-tile-content is-letter' }, p.title.charAt(0));
      }
      const tile = h('div', { class: 'pcdc-tile' }, tileBlur, content);

      // Badge: its own text ("Expérience immersive"...), else Application / Projet.
      const top = h('div', { class: 'pcdc-top' },
        h('span', { class: 'pcdc-pill is-num' }, num),
        h('span', { class: 'pcdc-pill is-badge' + (p.appUrl || p.badge ? ' is-app' : '') }, p.badge || (p.appUrl ? UI.badgeApp : UI.badgeProject)));

      // CTAs: the app first, the article as a second choice.
      const ctaText = p.cta || (p.appUrl ? UI.openApp : UI.discover);
      const actions = h('div', { class: 'pcdc-actions' });
      const primary = h('a', { class: 'pcdc-cta is-primary', href: primaryUrl, 'aria-label': `${ctaText} : ${p.title}` });
      primary.append(h('span', null, ctaText));
      primary.insertAdjacentHTML('beforeend', ARROW_SVG);
      actions.append(primary);
      if (p.appUrl && p.articleUrl) {
        actions.append(h('a', { class: 'pcdc-cta is-secondary', href: p.articleUrl,
          'aria-label': `${UI.readArticle} : ${p.title}` }, UI.readArticle));
      }

      const tags = h('ul', { class: 'pcdc-tags' }, ...p.tags.map((t) => h('li', null, t)));
      const bottom = h('div', { class: 'pcdc-bottom' },
        h('div', { class: 'pcdc-info' },
          h('span', { class: 'pcdc-kind' }, p.kind),
          h('h3', { class: 'pcdc-title' }, p.title),
          h('div', { class: 'pcdc-more' }, h('div', null, h('p', { class: 'pcdc-desc' }, p.description), tags))),
        h('div', { class: 'pcdc-actions-wrap' }, h('div', null, actions)));

      // The card body itself is a link (kept out of the tab order: the CTA is
      // the focus target). Its href and target are set by applyLinks().
      const hit = h('a', { class: 'pcdc-hit', href: primaryUrl, tabindex: '-1', 'aria-hidden': 'true' });

      const front = h('div', { class: 'pcdc-face pcdc-front' },
        h('div', { class: 'pcdc-in' }, bg, h('div', { class: 'pcdc-shade' }), top, tile, hit, bottom));

      // Back face: in a tourniquet some cards are always seen from behind.
      const backMark = p.icon
        ? h('img', { class: 'pcdc-back-mark', src: resolve(p.icon), alt: '', decoding: 'async', draggable: 'false' })
        : still ? h('img', { class: 'pcdc-back-mark is-photo', src: still, alt: '', decoding: 'async', draggable: 'false' }) : null;
      const back = h('div', { class: 'pcdc-face pcdc-back', 'aria-hidden': 'true' },
        h('div', { class: 'pcdc-in' }, backMark,
          h('div', { class: 'pcdc-back-text' }, h('strong', null, p.title), h('span', null, 'PCdlabs · ' + num))));

      el.append(front, back,
        h('i', { class: 'pcdc-edge pcdc-edge-l' }), h('i', { class: 'pcdc-edge pcdc-edge-r' }), h('i', { class: 'pcdc-edge pcdc-edge-t' }));

      return { el, p, hit, primaryUrl, videos: [...el.querySelectorAll('video')], links: [...el.querySelectorAll('a.pcdc-cta')], visible: false };
    }

    function buildCards(list) {
      const keepId = cards[frontIdx] ? cards[frontIdx].p.id : null;
      if (cardIO) cardIO.disconnect();
      cards.forEach((c) => c.videos.forEach((v) => { v.pause(); v.removeAttribute('src'); v.load(); }));
      track.textContent = '';
      dotsBox.textContent = '';
      cards = list.map(buildCard);
      n = cards.length;
      step = TAU / Math.max(1, n);
      if (!n) {
        track.append(h('p', { class: 'pcdc-empty' }, UI.empty));
        track.style.transform = '';
        capCount.textContent = capTitle.textContent = capKind.textContent = '';
      }
      cards.forEach((c) => track.append(c.el));
      dots = cards.map((c, i) => {
        const b = h('button', { type: 'button', class: 'pcdc-dot', 'aria-label': `${UI.goTo} ${c.p.title}` });
        b.addEventListener('click', () => { userActed(); goTo(i); });
        dotsBox.append(b);
        return b;
      });
      if (cardIO) cards.forEach((c) => cardIO.observe(c.el));
      else cards.forEach((c) => { c.visible = true; });
      // Keep the same card in front after a rebuild (an edit, a new version).
      const keep = keepId ? cards.findIndex((c) => c.p.id === keepId) : -1;
      const idx = keep >= 0 ? keep : Math.min(Math.max(0, frontIdx), Math.max(0, n - 1));
      target = current = -idx * step;
      frontIdx = -1;
      hovered = -1;
      if (view === 'carousel') { layout(); kick(); }
      syncTabindex();
      syncMedia();
    }

    // Card link and tab behaviour, from the settings.
    function applyLinks() {
      const s = conf.settings;
      cards.forEach((c) => {
        c.hit.href = s.cardLink === 'article' && c.p.articleUrl ? c.p.articleUrl : c.primaryUrl;
        c.el.querySelectorAll('a').forEach((a) => {
          if (s.newTab) { a.target = '_blank'; a.rel = 'noopener'; } else { a.removeAttribute('target'); a.removeAttribute('rel'); }
        });
      });
      [qrLink, brand, siteEl].forEach((a) => {
        if (s.newTab) { a.target = '_blank'; a.rel = 'noopener'; } else { a.removeAttribute('target'); a.removeAttribute('rel'); }
      });
    }

    /* ------------------------ carousel engine ------------------------
       Same logic as the BirdSignal favourites tourniquet: cards stand on a
       ring, EDGE-ON, and only turn to face you once they reach the front
       (turn = cos^6, so exactly one card faces at a time). The front card
       grows and rises, the others dim with depth. Drawn with CSS 3D, so
       GIFs, videos, links and hover panels stay native HTML. */
    let S = 320;           // card size in px
    let R = 880;           // ring radius in px
    let target = 0;        // ring angle we are heading to
    let current = 0;       // ring angle on screen
    let hovered = -1;
    let frontIdx = -1;
    let tiltX = 0, tiltZ = 0, tiltTX = 0, tiltTZ = 0;
    let raf = 0;
    let view = 'carousel';

    function layout() {
      const W = stage.clientWidth || root.clientWidth || 800;
      const fill = root.classList.contains('is-fill');
      const H = fill ? stage.clientHeight : 0;
      let s;
      // Wide screens show the front card plus neighbours, phones one card. A
      // tall stage (portrait tablet in full-screen mode) gets one big card.
      if (fill && H > W * 0.62) s = W * (W < 600 ? 0.72 : 0.6);
      else s = W >= 900 ? W / 3.1 : W >= 600 ? W / 2.45 : W * 0.66;
      // The front card must fit the height: the free height of the stage in
      // full-screen mode, part of the window otherwise. No fixed maximum: a
      // 4K screen in F11 gets 4K-sized cards.
      s = Math.min(s, fill && H > 0 ? H / 1.14 : (window.innerHeight || 800) * 0.56);
      s *= (conf.layout ? conf.layout.cardSize : 100) / 100;
      S = Math.round(Math.max(140, Math.min(s, 1600)));
      // The arc between two cards must exceed a card width, or they interpenetrate.
      R = Math.max(1.6 * S, (1.55 * S) / (2 * Math.sin(Math.PI / Math.max(3, n))));
      root.style.setProperty('--pcdc-size', S + 'px');
      root.style.setProperty('--pcdc-persp', Math.round(S * 2.5) + 'px');
    }

    const frontOf = (angle) => (n ? ((Math.round(-angle / step) % n) + n) % n : -1);

    function frame() {
      raf = 0;
      if (view !== 'carousel' || !n) return;
      const k = reduceMotion.matches ? 0.4 : 0.12;
      current += (target - current) * k;
      if (Math.abs(target - current) < 1e-4) current = target;
      tiltX += (tiltTX - tiltX) * 0.08;
      tiltZ += (tiltTZ - tiltZ) * 0.08;

      // Slight high-angle view: gives the ring its depth without drawing a stand.
      // Tilting the ring about its centre would push the front card down by
      // R.sin(pitch); the leading translateY puts it back in the middle.
      const pitch = ((-6 + tiltX) * Math.PI) / 180;
      track.style.transform =
        `translateY(${(R * Math.sin(pitch)).toFixed(1)}px) translateZ(${(-R).toFixed(1)}px) rotateX(${pitch.toFixed(4)}rad) rotateZ(${tiltZ.toFixed(2)}deg) rotateY(${current.toFixed(4)}rad)`;

      for (let i = 0; i < n; i++) {
        const a = i * step + current;
        const front = Math.cos(a);                 // 1 = right in front, -1 = behind
        const turn = Math.pow(Math.max(0, front), 6);
        // Neighbours stand edge-on on BOTH sides, like cards in an index box:
        // the quarter turn follows the side of the ring (smoothly, so a card
        // passing behind does not flip).
        const side = Math.max(-1, Math.min(1, Math.sin(a) * 4));
        const edge = (Math.PI / 2) * (1 - turn) * side;
        const hot = i === hovered ? 1 : 0;
        const scale = (0.82 + 0.32 * turn + 0.09 * hot) / 1.14;
        const lift = -(0.07 * turn + 0.04 * hot) * S;
        const c = cards[i].el;
        c.style.transform =
          `rotateY(${(i * step).toFixed(4)}rad) translateZ(${R.toFixed(1)}px) rotateY(${edge.toFixed(4)}rad) translateY(${lift.toFixed(1)}px) scale(${scale.toFixed(4)})`;
        c.style.setProperty('--pcdc-dim', ((1 - (front * 0.5 + 0.5)) * 0.62).toFixed(3));
        c.style.setProperty('--pcdc-sheen', (50 + Math.sin(a) * 90).toFixed(1));
      }

      const f = frontOf(target);
      if (f !== frontIdx) setFront(f);

      const settling = current !== target || Math.abs(tiltTX - tiltX) > 0.01 || Math.abs(tiltTZ - tiltZ) > 0.01;
      if (settling) kick();                        // the loop sleeps once everything is still
    }
    const kick = () => { if (!raf && view === 'carousel' && n && !views.hidden) raf = requestAnimationFrame(frame); };

    function setFront(f) {
      frontIdx = f;
      const c = cards[f];
      if (!c) return;
      capCount.textContent = `${String(f + 1).padStart(2, '0')} / ${String(n).padStart(2, '0')}`;
      capTitle.textContent = c.p.title;
      capKind.textContent = c.p.kind;
      dots.forEach((d, i) => d.setAttribute('aria-current', String(i === f)));
      cards.forEach((cc, i) => {
        cc.el.classList.toggle('is-front', i === f);
        // No hover on a tablet: the front card shows its details straight away,
        // so one tap on it opens the app.
        cc.el.classList.toggle('is-open', i === f && noHover.matches);
        if (i !== f) cc.el.classList.remove('is-hover');
        else if (i === hovered) cc.el.classList.add('is-hover');
      });
      syncTabindex();
      syncMedia();
    }

    function goTo(i) {                             // shortest way round
      if (!n) return;
      const aim = -i * step;
      target = aim + Math.round((target - aim) / TAU) * TAU;
      kick();
    }
    const nudge = (d) => { if (!n) return; target = Math.round(target / step) * step - d * step; kick(); };
    const snap = () => { if (!n) return; target = Math.round(target / step) * step; kick(); };

    /* ------------------------------ views ------------------------------ */
    function setView(v, persist) {
      if (!VIEWS.includes(v)) v = 'carousel';
      view = v;
      root.dataset.view = v;
      root.style.setProperty('--pcdc-view-i', VIEWS.indexOf(v));
      switchBtns.forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.view === v)));
      cards.forEach((c) => c.el.classList.remove('is-open', 'is-hover'));
      if (v === 'carousel') {
        layout();
        frontIdx = -1;                             // force caption / classes refresh
        kick();
      } else {
        cancelAnimationFrame(raf); raf = 0;
        track.style.transform = '';
        cards.forEach((c) => {
          c.el.style.transform = '';
          c.el.style.removeProperty('--pcdc-dim');
          c.el.style.removeProperty('--pcdc-sheen');
          c.el.classList.remove('is-front');
        });
      }
      syncTabindex();
      syncMedia();
      if (persist) store.set(KEY.view, v);
      views.classList.remove('is-entering');
      void views.offsetWidth;
      views.classList.add('is-entering');
    }
    switchBtns.forEach((b) => b.addEventListener('click', () => { userActed(); setView(b.dataset.view, true); }));

    // Only the front card's buttons are reachable with Tab in the carousel.
    function syncTabindex() {
      cards.forEach((c, i) => {
        const reachable = view !== 'carousel' || i === frontIdx;
        c.links.forEach((a) => (reachable ? a.removeAttribute('tabindex') : a.setAttribute('tabindex', '-1')));
      });
    }

    /* ------------------------------ media ------------------------------
       Videos play only where they are seen: the front card of the carousel,
       or cards on screen in grid / list. Everything pauses when the block is
       scrolled away or the tab is hidden: a tablet on a stand runs all day. */
    let rootVisible = true;
    function syncMedia() {
      const on = rootVisible && !document.hidden && !reduceMotion.matches && !views.hidden;
      cards.forEach((c, i) => {
        const seen = view === 'carousel' ? i === frontIdx : c.visible;
        c.videos.forEach((v) => {
          const want = on && seen && !(view === 'list' && v.dataset.role === 'bg');
          if (want) {
            if (!v.getAttribute('src')) v.src = v.dataset.src;
            if (v.paused) { const pr = v.play(); if (pr && pr.catch) pr.catch(() => { /* poster stays */ }); }
          } else if (!v.paused) {
            v.pause();
          }
        });
      });
    }
    if ('IntersectionObserver' in window) {
      new IntersectionObserver((entries) => {
        rootVisible = entries[0].isIntersecting;
        syncMedia();
      }).observe(root);
      cardIO = new IntersectionObserver((entries) => {
        entries.forEach((e) => {
          const c = cards.find((cc) => cc.el === e.target);
          if (c) c.visible = e.isIntersecting;
        });
        syncMedia();
      }, { threshold: 0.2 });
    }
    document.addEventListener('visibilitychange', syncMedia);

    /* --------------------------- interaction --------------------------- */
    let lastAct = performance.now();
    let lastAttract = 0;
    function userActed() {
      lastAct = performance.now();
      root.classList.add('has-interacted');
    }

    // Drag / swipe. The pointer is only captured once the gesture is clearly
    // horizontal, so a plain tap still reaches the links, and a vertical swipe
    // still scrolls the page on a tablet.
    let drag = null;
    let suppressClick = false;
    stage.addEventListener('pointerdown', (e) => {
      if (view !== 'carousel' || e.button > 0) return;
      suppressClick = false;
      drag = { id: e.pointerId, x0: e.clientX, y0: e.clientY, x: e.clientX, active: false };
      userActed();
    });
    stage.addEventListener('pointermove', (e) => {
      if (view !== 'carousel') return;
      if (e.pointerType === 'mouse' && !drag) {    // subtle parallax, as the gyroscope did on phones
        const r = stage.getBoundingClientRect();
        tiltTZ = ((e.clientX - r.left) / r.width - 0.5) * 3;
        tiltTX = ((e.clientY - r.top) / r.height - 0.5) * -4;
        userActed();
        kick();
      }
      if (!drag || e.pointerId !== drag.id) return;
      const dx = e.clientX - drag.x0;
      const dy = e.clientY - drag.y0;
      if (!drag.active) {
        if (Math.hypot(dx, dy) < 8) return;
        if (Math.abs(dy) > Math.abs(dx)) { drag = null; return; }
        drag.active = true;
        try { stage.setPointerCapture(e.pointerId); } catch (err) { /* already released */ }
        root.classList.add('is-dragging');
      }
      target += (e.clientX - drag.x) / (R * 0.8);  // the front card follows the finger
      drag.x = e.clientX;
      kick();
    });
    const endDrag = (e) => {
      if (!drag || e.pointerId !== drag.id) return;
      if (drag.active) {
        suppressClick = true;                      // the release of a drag is not a click
        setTimeout(() => { suppressClick = false; }, 350);
        snap();
      }
      drag = null;
      root.classList.remove('is-dragging');
    };
    stage.addEventListener('pointerup', endDrag);
    stage.addEventListener('pointercancel', endDrag);
    // Links and images are natively draggable: that would cancel the swipe.
    stage.addEventListener('dragstart', (e) => e.preventDefault());
    stage.addEventListener('pointerleave', () => { tiltTX = 0; tiltTZ = 0; kick(); });

    // Horizontal wheel / trackpad (or Shift + wheel) always turns the ring. The
    // vertical wheel is left to the page, unless "lateral scrolling" is on in
    // the settings: then it turns the ring too while the pointer is over it.
    let wheelTimer = 0;
    stage.addEventListener('wheel', (e) => {
      if (view !== 'carousel' || !n) return;
      const unit = e.deltaMode === 1 ? 16 : 1;
      const d = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : (e.shiftKey || conf.settings.wheel) ? e.deltaY : 0;
      if (!d) return;
      e.preventDefault();
      userActed();
      target -= d * unit * 0.0022;
      clearTimeout(wheelTimer);
      wheelTimer = setTimeout(snap, 140);
      kick();
    }, { passive: false });

    stage.addEventListener('keydown', (e) => {
      if (view !== 'carousel' || !n) return;
      if (e.key === 'ArrowRight') nudge(1);
      else if (e.key === 'ArrowLeft') nudge(-1);
      else if (e.key === 'Home') goTo(0);
      else if (e.key === 'End') goTo(n - 1);
      else if ((e.key === 'Enter' || e.key === ' ') && e.target === stage && cards[frontIdx]) cards[frontIdx].hit.click();
      else return;
      e.preventDefault();
      userActed();
    });
    root.querySelectorAll('.pcdc-arrow').forEach((b) =>
      b.addEventListener('click', () => { userActed(); nudge(Number(b.dataset.dir)); }));

    // Hover (mouse only): lifts the card and shows its details.
    track.addEventListener('pointerover', (e) => {
      if (e.pointerType !== 'mouse') return;
      const card = e.target.closest('.pcdc-card');
      setHover(card ? cards.findIndex((c) => c.el === card) : -1);
    });
    track.addEventListener('pointerleave', () => setHover(-1));
    function setHover(i) {
      if (i === hovered) return;
      hovered = i;
      cards.forEach((c, j) => c.el.classList.toggle('is-hover', j === i && (view !== 'carousel' || j === frontIdx)));
      kick();
    }

    // Clicks are decided here, in the capture phase, before any link reacts.
    root.addEventListener('click', (e) => {
      if (suppressClick) { e.preventDefault(); e.stopPropagation(); suppressClick = false; return; }
      if (e.target.closest('.pcdc-dlg')) return;
      const card = e.target.closest('.pcdc-card');
      if (!card) {
        if (!e.target.closest('button')) cards.forEach((c) => view !== 'carousel' && c.el.classList.remove('is-open'));
        return;
      }
      const i = cards.findIndex((c) => c.el === card);
      userActed();
      if (view === 'carousel') {
        // A card on the side first comes round to the front; the front card opens.
        if (i !== frontIdx) { e.preventDefault(); goTo(i); }
        return;
      }
      // Grid on a touch screen: first tap shows the details, second tap opens.
      if (view === 'grid' && noHover.matches && !card.classList.contains('is-open') && !e.target.closest('.pcdc-cta')) {
        e.preventDefault();
        cards.forEach((c) => c.el.classList.toggle('is-open', c.el === card));
      }
    }, true);

    // Attract mode: after a while without anyone touching the tablet, the
    // carousel turns on its own; the first touch stops it.
    setInterval(() => {
      const wait = conf.settings.attract;
      if (!wait || view !== 'carousel' || n < 2 || drag || dlg.open || document.hidden || !rootVisible || reduceMotion.matches || views.hidden) return;
      const now = performance.now();
      if (now - lastAct > wait * 1000 && now - lastAttract > 4500) { lastAttract = now; nudge(1); }
    }, 500);

    // Any change of size (window, F11, rotation, a section switched on or
    // off, a font arriving) re-sizes the ring and re-fits the footer, once per frame.
    let resizeRaf = 0;
    function onResize() {
      resizeRaf = 0;
      fitWrap();
      if (view === 'carousel' && !views.hidden) { layout(); kick(); }
      fitFooter();
      centerMid();
    }
    const queueResize = () => { if (!resizeRaf) resizeRaf = requestAnimationFrame(onResize); };
    if ('ResizeObserver' in window) {
      const ro = new ResizeObserver(queueResize);
      [root, stage, foot].forEach((el) => ro.observe(el));
    }
    window.addEventListener('resize', queueResize);
    if (document.fonts && document.fonts.ready) {
      document.fonts.ready.then(queueResize);
      if (document.fonts.addEventListener) document.fonts.addEventListener('loadingdone', queueResize);
    }

    /* ------------------------------ render ------------------------------ */
    const pageUrl = () => {
      try { const u = new URL(window.location.href); u.hash = ''; u.searchParams.delete('vue'); return u.href; } catch (e) { return window.location.href; }
    };
    let qrText = null;

    const setVar = (k, v) => (v === null || v === undefined || v === '' ? root.style.removeProperty(k) : root.style.setProperty(k, String(v)));

    // Theme, fonts, sizes and spacing: CSS variables on the block.
    function applyLook(c) {
      const s = c.settings;
      const L = c.layout;
      root.dataset.theme = s.theme;
      root.dataset.switch = String(s.switch);
      root.dataset.align = c.header.align;
      setVar('--pcdc-tile-w', s.tile / 100);
      setVar('--pcdc-font', s.font === 'inter-tight' ? null : fontStack(s.font));
      if (s.font !== 'inter-tight') loadFont(s.font);
      setVar('--pcdc-bg', s.bg || null);
      const factor = (v) => (v === 100 ? null : v / 100);
      setVar('--pcdc-ts', factor(L.text));
      setVar('--pcdc-kp', factor(L.pad));
      setVar('--pcdc-kh', factor(L.gapHead));
      setVar('--pcdc-kn', factor(L.gapNav));
      setVar('--pcdc-kf', factor(L.gapFoot));
      setVar('--pcdc-ct', factor(L.cardText));
      setVar('--pcdc-off', L.offset ? L.offset / 100 : null);
      setVar('--pcdc-hf', L.height / 100);
      root.classList.toggle('is-fill', L.height > 0 && sectionsOf(c).showcase);
      root.classList.toggle('is-transparent', L.bgNone || forcedTransparent);
      root.classList.toggle('no-foot-line', !L.footLine);
      [['num', L.cardNum], ['badge', L.cardBadge], ['kind', L.cardKind], ['desc', L.cardDesc], ['tags', L.cardTags]]
        .forEach(([k, on]) => root.classList.toggle('no-card-' + k, !on));
      // One set of variables per element: --pcdc-s-title, --pcdc-f-title...
      for (const [k, e] of Object.entries(c.els)) {
        setVar(`--pcdc-s-${k}`, factor(e.size));
        if ('font' in e) { setVar(`--pcdc-f-${k}`, e.font ? fontStack(e.font) : null); if (e.font) loadFont(e.font); }
        if ('weight' in e) setVar(`--pcdc-w-${k}`, e.weight || null);
        if ('color' in e) setVar(`--pcdc-c-${k}`, e.color || null);
      }
    }

    // Sections really shown by THIS block: the settings, then data-part.
    function sectionsOf(c) {
      const L = c.layout;
      return {
        header: part === 'footer' ? false : L.header,
        showcase: part === 'footer' ? false : L.showcase,
        footer: part === 'showcase' ? false : part === 'footer' ? true : L.footer,
      };
    }

    // Sections and elements on / off.
    function applySections(c) {
      const L = c.layout;
      const E = c.els;
      const sec = sectionsOf(c);
      kickerEl.hidden = !E.kicker.show || !c.header.kicker;
      titleEl.hidden = !E.title.show || !c.header.title;
      head.hidden = !sec.header || (kickerEl.hidden && titleEl.hidden && !c.settings.switch);
      views.hidden = !sec.showcase;
      const ctl = L.controls;
      arrowBtns.forEach((b) => { b.hidden = !ctl || !E.arrows.show; });
      caption.hidden = !ctl || !E.caption.show;
      nav.hidden = !ctl || (!E.arrows.show && !E.caption.show);
      nav.classList.toggle('no-arrows', !E.arrows.show);
      nav.classList.toggle('no-caption', !E.caption.show);
      dotsBox.hidden = !ctl || !E.dots.show;
      hintEl.textContent = c.header.hint;
      hintEl.hidden = !ctl || !E.hint.show || !c.header.hint;
      foot.hidden = !sec.footer;
      devCorner.hidden = sec.footer;               // ".dev" must stay reachable
    }

    function renderFooter(c) {
      const f = c.footer;
      const E = c.els;
      brand.hidden = !f.logo;
      brand.href = f.siteUrl || 'https://presentcomposedesign.fr/';
      if (f.logo && f.logoUrl) {
        brandImg.src = resolve(f.logoUrl);
        brandImg.hidden = false;
        if (brandSvg) brandSvg.style.display = 'none';
      } else {
        brandImg.hidden = true;
        brandImg.removeAttribute('src');
        if (brandSvg) brandSvg.style.display = '';
      }
      copyEl.textContent = f.copyText;
      copyEl.hidden = !E.copy.show || !f.copyText;
      siteEl.textContent = f.siteText || f.siteUrl.replace(/^https?:\/\//i, '').replace(/\/$/, '');
      siteEl.hidden = !E.site.show || !siteEl.textContent;
      if (f.siteUrl) siteEl.href = f.siteUrl; else siteEl.removeAttribute('href');
      sloganEl.textContent = f.sloganText;
      sloganEl.hidden = !(f.slogan && f.sloganText);
      subEl.textContent = f.subText;
      subEl.hidden = !(E.sub.show && f.subText);
      footMid.hidden = sloganEl.hidden && subEl.hidden;

      const text = f.qrUrl || pageUrl();
      const svg = f.qr ? QR.svg(text) : '';
      const showQr = Boolean(svg);
      root.classList.toggle('has-qr', showQr);
      qrLink.hidden = !showQr;
      if (showQr && text !== qrText) {
        qrText = text;
        qrCode.innerHTML = svg;                    // generated here, never from user HTML
        qrLink.href = text;
        qrLink.setAttribute('aria-label', `${f.qrCaption || 'QR code'} : ${text}`);
      }
      qrCap.textContent = f.qrCaption || 'Scannez-moi';
      qrHintText.textContent = f.qrHintText;
      qrHint.hidden = !(showQr && E.qrHint.show && f.qrHintText);
    }

    /* Room above the footer line for the QR card, which sticks out of the
       footer on purpose (and for its note). On a wide screen, when the
       centred controls leave enough room on their right, the card rises
       beside them instead of pushing the carousel up. */
    function fitFooter() {
      if (!root.classList.contains('has-qr') || foot.hidden) { root.style.removeProperty('--pcdc-qr-band'); return; }
      const hintOn = !qrHint.hidden;
      let band = -Math.min(qrLink.offsetTop, hintOn ? qrHint.offsetTop : 0) + 14;   // + tilt and float
      if (!views.hidden && view === 'carousel') {
        const parts = [nav, dotsBox, hintEl].filter((e) => !e.hidden && e.offsetHeight);
        if (parts.length) {
          const rects = parts.map((e) => e.getBoundingClientRect());
          const left = Math.min(qrLink.getBoundingClientRect().left, hintOn ? qrHint.getBoundingClientRect().left : Infinity);
          if (rects.every((r) => r.right + 24 <= left)) {
            band -= Math.max(...rects.map((r) => r.bottom)) - Math.min(...rects.map((r) => r.top));
          }
        }
      }
      root.style.setProperty('--pcdc-qr-band', Math.max(0, Math.round(band)) + 'px');
    }

    /* The slogan is centred on the SCREEN (setting "Centré sur"): when the
       block is not centred in the window, the middle column moves, within
       the room the logo on the left and the links on the right leave. */
    function centerMid() {
      let shift = 0;
      const wide = getComputedStyle(foot).gridTemplateColumns.split(' ').length === 3;
      if (conf.layout.sloganAnchor === 'screen' && wide && !foot.hidden && !footMid.hidden) {
        const current = parseFloat(root.style.getPropertyValue('--pcdc-mid-shift')) || 0;
        const fr = foot.getBoundingClientRect();
        const mr = footMid.getBoundingClientRect();
        const midL = mr.left - current;
        const midR = mr.right - current;
        const vw = document.documentElement.clientWidth || window.innerWidth;
        const want = vw / 2 - (midL + midR) / 2;
        const rects = (box) => [...box.children].filter((e) => !e.hidden && e.offsetWidth).map((e) => e.getBoundingClientRect());
        const leftEdge = rects(footLeft).reduce((m, r) => Math.max(m, r.right), fr.left);
        const rightEdge = rects(footRight).reduce((m, r) => Math.min(m, r.left), fr.right);
        const lo = leftEdge + 16 - midL;
        const hi = rightEdge - 16 - midR;
        shift = lo > hi ? 0 : Math.max(lo, Math.min(hi, want));
        if (Math.abs(shift) < 0.5) shift = 0;
      }
      setVar('--pcdc-mid-shift', shift ? shift.toFixed(1) + 'px' : null);
    }

    /* Full-screen mode: "100 %" means that the whole section fills the
       screen, not the block alone with the page's margins on top of it.
       The space the page puts around the block is measured, never guessed:
       padding and borders of the wrappers that hold nothing but the block
       (an Elementor container...), their margins, and whatever sits just
       above it near the top of the page (a header, the admin bar). */
    function fitWrap() {
      if (!root.classList.contains('is-fill')) { setVar('--pcdc-wrap', null); return; }
      const sum = (el, keys) => { const cs = getComputedStyle(el); return keys.reduce((s, k) => s + (parseFloat(cs[k]) || 0), 0); };
      const holdsOnly = (el, me) => ![...el.children].some((c) => {
        if (c === me) return false;
        const cs = getComputedStyle(c);
        return cs.display !== 'none' && cs.position !== 'absolute' && cs.position !== 'fixed' && c.getBoundingClientRect().height > 1;
      });
      let extra = 0;
      let outer = root;
      for (let el = root.parentElement; el && el !== document.body && el !== document.documentElement; el = el.parentElement) {
        if (!holdsOnly(el, outer)) break;
        extra += sum(outer, ['marginTop', 'marginBottom']) + sum(el, ['paddingTop', 'paddingBottom', 'borderTopWidth', 'borderBottomWidth']);
        outer = el;
      }
      const top = outer.getBoundingClientRect().top + window.scrollY;
      if (top > 0 && top < (window.innerHeight || 800) * 0.3) extra += top;
      setVar('--pcdc-wrap', extra > 0.5 ? Math.round(extra) + 'px' : null);
    }

    function render(c) {
      conf = c;
      applyLook(c);
      applySections(c);
      kickerEl.textContent = c.header.kicker;
      titleEl.textContent = c.header.title;
      renderFooter(c);
      const visible = c.projects.filter((p) => !p.hidden);
      const key = mediaBase + JSON.stringify(visible);
      if (key !== cardsKey) { cardsKey = key; buildCards(visible); }
      applyLinks();
      draftBadge.hidden = !(work && isDirty());
      fitWrap();
      if (view === 'carousel' && !views.hidden) { layout(); kick(); }
      syncMedia();
      fitFooter();
      centerMid();
    }
    const shown = () => (work && unlocked() ? work : pub);
    const isDirty = () => Boolean(work) && contentKey(work) !== contentKey(pub);

    /* ------------------------- server connection ------------------------- */
    const endpoint = (action, extra) => {
      const u = new URL(apiUrl, window.location.href);
      u.searchParams.set('action', action);
      Object.entries(extra || {}).forEach(([k, v]) => u.searchParams.set(k, v));
      return u.href;
    };
    async function api(action, { method = 'GET', body, extra } = {}) {
      if (!apiUrl) return { status: 0, data: null };
      const headers = {};
      if (token) headers['X-PCDC-Token'] = token;
      if (body) headers['Content-Type'] = 'application/json';
      try {
        const r = await fetch(endpoint(action, extra), {
          method, headers, body: body ? JSON.stringify(body) : undefined,
          cache: action === 'config' ? 'no-cache' : 'no-store', credentials: 'omit',
        });
        let data = null;
        try { data = await r.json(); } catch (e) { /* not JSON */ }
        return { status: r.status, data };
      } catch (e) {
        return { status: 0, data: null };        // network error, CORS refusal, no server
      }
    }
    async function loadPublished() {
      const res = await api('config');
      if (res.status !== 200 || !res.data || !res.data.ok) {
        serverState = res.status === 0 ? 'offline' : 'error';
        return false;
      }
      serverState = 'online';
      serverApi = Number(res.data.api) || 1;
      if (!res.data.config) return true;           // nothing published yet: the block's JSON stays
      const fresh = cleanConfig(res.data.config);
      const differs = contentKey(fresh) !== contentKey(pub) || fresh.rev !== pub.rev;
      const draftWasClean = work ? contentKey(work) === contentKey(pub) : true;
      pub = fresh;
      store.set(KEY.cache, JSON.stringify(pub));
      // An untouched working copy follows the new version; a real draft is kept
      // (publishing it will then report the newer version).
      if (work && draftWasClean) { work = clone(pub); workBase = pub.rev; }
      if (differs) render(shown());
      return true;
    }

    /* ------------------------------ start ------------------------------ */
    if (unlocked()) {
      try {
        const d = JSON.parse(store.get(KEY.draft) || 'null');
        if (d && d.config) { work = cleanConfig(d.config); workBase = Number(d.baseRev) || 0; }
      } catch (e) { /* no draft */ }
    }
    render(shown());
    let startView = conf.settings.view;
    const savedView = store.get(KEY.view);
    if (savedView && conf.settings.switch) startView = savedView;   // the visitor's own choice
    try {
      const q = new URLSearchParams(window.location.search).get('vue');
      if (q && VIEW_PARAM[q.toLowerCase()]) startView = VIEW_PARAM[q.toLowerCase()];
    } catch (e) { /* old browser */ }
    layout();
    setView(startView, false);
    views.classList.remove('is-entering');

    if (apiUrl) {
      // First visit: wait (briefly) for the published version rather than
      // flashing the block's own content and then swapping it.
      const hasCache = Boolean(store.get(KEY.cache));
      if (!hasCache) root.classList.add('is-loading');
      const reveal = setTimeout(() => root.classList.remove('is-loading'), 1500);
      loadPublished().then(() => {
        clearTimeout(reveal);
        root.classList.remove('is-loading');
        if (adm) refreshAdmin();
      });
    }

    /* ==================================================================
       DEVELOPER MODE
       ".dev" -> access code -> administration drawer. With a server, the
       code is checked by the server, which returns a signed session token
       (8 hours); publishing and uploads require it. Without a server, the
       code is checked here and changes stay in this browser (export them
       to make them permanent).
       ================================================================== */
    const lockForm = dlg.querySelector('.pcdc-lock');
    const lockNote = dlg.querySelector('.pcdc-lock-note');
    const codeInput = lockForm.elements.code;
    const codeErr = lockForm.querySelector('.pcdc-err');
    const admBox = dlg.querySelector('.pcdc-adm');
    let adm = null;                                // admin UI, built on first unlock

    root.querySelectorAll('[data-open-dev]').forEach((b) => b.addEventListener('click', openDev));
    dlg.addEventListener('click', (e) => {
      if (e.target === dlg) dlg.close();           // click on the backdrop
      else if (e.target.closest('[data-close]')) dlg.close();
    });
    dlg.addEventListener('close', () => { dlg.classList.remove('is-admin'); dlg.classList.add('is-lock'); });

    // "#dev" at the end of the page address opens the developer mode too.
    const devHash = () => { if (/^#(pcdc-)?dev$/i.test(window.location.hash)) openDev(); };
    window.addEventListener('hashchange', devHash);
    setTimeout(devHash, 0);

    function openDev() {
      if (typeof dlg.showModal === 'function') { if (!dlg.open) dlg.showModal(); } else dlg.setAttribute('open', '');
      if (unlocked()) showAdmin(); else showLock('');
    }
    function showLock(note) {
      dlg.classList.add('is-lock');
      dlg.classList.remove('is-admin');
      lockForm.hidden = false;
      admBox.hidden = true;
      lockNote.hidden = !note;
      lockNote.textContent = note;
      codeInput.value = '';
      codeErr.textContent = '';
      codeInput.focus();
    }

    async function pbkdf2Hex(code) {
      const enc = new TextEncoder();
      const key = await crypto.subtle.importKey('raw', enc.encode(code), 'PBKDF2', false, ['deriveBits']);
      const salt = new Uint8Array(DEV_KDF.salt.match(/../g).map((x) => parseInt(x, 16)));
      const bits = await crypto.subtle.deriveBits({ name: 'PBKDF2', hash: 'SHA-256', salt, iterations: DEV_KDF.iter }, key, 256);
      return [...new Uint8Array(bits)].map((b) => b.toString(16).padStart(2, '0')).join('');
    }

    lockForm.addEventListener('submit', async (e) => {
      e.preventDefault();
      const code = codeInput.value;
      const submit = lockForm.querySelector('[type="submit"]');
      submit.disabled = true;
      codeErr.textContent = 'Vérification...';
      let msg = 'Code incorrect.';
      let ok = false;
      if (apiUrl) {
        const res = await api('login', { method: 'POST', body: { code } });
        if (res.status === 200 && res.data && res.data.ok && res.data.token) {
          token = res.data.token;
          serverApi = Number(res.data.api) || 1;
          sess.set(KEY.token, token);
          localMode = false;
          sess.del(KEY.local);
          serverState = 'online';
          ok = true;
        } else if (res.status === 429) {
          msg = `Trop d'essais. Réessayez dans ${Math.ceil(((res.data && res.data.retryAfter) || 900) / 60)} min.`;
        } else if (res.status === 401) {
          msg = 'Code incorrect.';
        } else {
          serverState = res.status === 0 ? 'offline' : 'error';
        }
        // Server unreachable or not configured: offline check, changes stay here.
        if (!ok && res.status !== 401 && res.status !== 429 && await offlineCheck(code)) ok = true;
      } else if (await offlineCheck(code)) {
        ok = true;
      }
      submit.disabled = false;
      if (ok) {
        codeErr.textContent = '';
        startEditing();
        showAdmin();
      } else {
        codeErr.textContent = msg;
        codeInput.value = '';
        lockForm.classList.remove('is-wrong'); void lockForm.offsetWidth; lockForm.classList.add('is-wrong');
        codeInput.focus();
      }
    });
    async function offlineCheck(code) {
      if (!window.crypto || !window.crypto.subtle) return false;
      try {
        if ((await pbkdf2Hex(code)) !== DEV_KDF.hash) return false;
      } catch (e) { return false; }
      if (!token) { localMode = true; sess.set(KEY.local, '1'); }
      return true;
    }

    function startEditing() {
      if (!work) {
        try {
          const d = JSON.parse(store.get(KEY.draft) || 'null');
          if (d && d.config) { work = cleanConfig(d.config); workBase = Number(d.baseRev) || 0; }
        } catch (e) { /* no draft */ }
      }
      if (!work) { work = clone(pub); workBase = pub.rev; }
      render(work);
    }

    /* Every edit goes through here: the working copy is saved as a draft in
       this browser, and the showcase behind the drawer is redrawn. */
    let renderTimer = 0;
    function changed(opts) {
      if (isDirty()) store.set(KEY.draft, JSON.stringify({ baseRev: workBase, config: work }));
      else store.del(KEY.draft);
      clearTimeout(renderTimer);
      renderTimer = setTimeout(() => {
        render(work);
        exportPreviewSoon();
        if (opts && opts.view) setView(work.settings.view, false);
        if (opts && opts.focusId) {
          const i = cards.findIndex((c) => c.p.id === opts.focusId);
          if (i >= 0 && view === 'carousel') goTo(i);
        }
      }, 120);
      updateState();
    }

    function showAdmin() {
      if (!adm) buildAdmin();
      dlg.classList.remove('is-lock');
      dlg.classList.add('is-admin');
      lockForm.hidden = true;
      admBox.hidden = false;
      refreshAdmin();
      adm.head.querySelector('.pcdc-x').focus();
    }

    /* --------------------------- admin UI --------------------------- */
    function buildAdmin() {
      const tpl = root.querySelector('.pcdc-adm-tpl');
      admBox.append(tpl.content.cloneNode(true));
      adm = {
        head: admBox.querySelector('.pcdc-adm-head'),
        server: admBox.querySelector('.pcdc-server'),
        tabs: [...admBox.querySelectorAll('.pcdc-tab')],
        panels: [...admBox.querySelectorAll('.pcdc-panel')],
        cardsView: admBox.querySelector('.pcdc-cards-view'),
        cardsList: admBox.querySelector('.pcdc-adm-cards'),
        editor: admBox.querySelector('.pcdc-editor'),
        setPanels: [...admBox.querySelectorAll('.pcdc-set')],
        qrPrev: null,                              // QR preview of the footer tab
        pubInfo: admBox.querySelector('.pcdc-pub-info'),
        versions: admBox.querySelector('.pcdc-versions'),
        ioState: admBox.querySelector('.pcdc-io-state'),
        state: admBox.querySelector('.pcdc-state'),
        publishBtn: admBox.querySelector('[data-publish]'),
        discardBtn: admBox.querySelector('[data-discard]'),
        editing: null,                             // id of the card in the editor
        force: false,                              // next publish overwrites a newer version
      };

      adm.tabs.forEach((t) => t.addEventListener('click', () => selectTab(t.dataset.tab)));
      admBox.querySelector('[data-add]').addEventListener('click', addCard);
      adm.publishBtn.addEventListener('click', publish);
      adm.discardBtn.addEventListener('click', discard);
      admBox.querySelector('[data-logout]').addEventListener('click', logout);
      admBox.querySelector('[data-export]').addEventListener('click', exportJson);
      admBox.querySelector('[data-copy-block]').addEventListener('click', copyForBlock);
      admBox.querySelector('[data-import]').addEventListener('click', importJson);
      admBox.querySelector('[data-from-block]').addEventListener('click', () => {
        work = clone(embedded);
        workBase = pub.rev;
        changed({ view: true });
        refreshAdmin();
        adm.ioState.textContent = 'Contenu du bloc Elementor chargé dans le brouillon : vérifiez, puis « Publier ».';
      });
      admBox.querySelector('[data-versions]').addEventListener('click', listVersions);
    }

    function selectTab(name) {
      adm.tabs.forEach((t) => t.setAttribute('aria-selected', String(t.dataset.tab === name)));
      adm.panels.forEach((p) => { p.hidden = p.dataset.panel !== name; });
      if (name === 'cards') { adm.editor.hidden = true; adm.cardsView.hidden = false; adm.editing = null; renderCardList(); }
      if (name === 'footer') exportPreviewSoon();
    }

    function refreshAdmin() {
      if (!adm) return;
      const labels = {
        online: 'Serveur connecté : les publications sont vues par tous',
        offline: 'Serveur injoignable : brouillon gardé sur cet appareil',
        error: 'Le serveur répond mal : brouillon gardé sur cet appareil',
        none: 'Pas de serveur (data-api vide) : exportez le JSON pour publier',
        unknown: 'Connexion au serveur...',
      };
      let state = serverState;
      let label = labels[serverState] || labels.unknown;
      if (!token && localMode) {
        state = 'local';
        label = !apiUrl ? labels.none
          : serverState === 'online' ? 'Mode local : reconnectez-vous pour publier'
            : 'Mode local : serveur injoignable, brouillon gardé sur cet appareil';
      }
      if (state === 'online' && serverOutdated()) {
        state = 'outdated';
        label = 'showcase.php sur le serveur est l’ancienne version : envoyez showcase/api/showcase.php (FileZilla), sinon la publication perdrait les réglages';
      }
      adm.server.dataset.state = state;
      adm.server.textContent = label;
      renderCardList();
      buildSettings();
      fillPublishInfo();
      updateState();
    }

    function updateState(message, kind) {
      if (!adm) return;
      const dirty = isDirty();
      adm.state.className = 'pcdc-state' + (kind ? ' is-' + kind : dirty ? ' is-dirty' : '');
      adm.state.textContent = '';
      if (message) {
        adm.state.append(h('strong', null, message));
      } else if (dirty) {
        adm.state.append(h('strong', null, 'Modifications non publiées'), ' · visibles sur cet appareil seulement');
      } else {
        adm.state.append(h('strong', null, 'À jour'), pub.rev ? ` · version en ligne n° ${pub.rev}` : ' · contenu du bloc');
      }
      adm.discardBtn.disabled = !dirty;
      adm.publishBtn.disabled = !dirty || !token || serverOutdated();
      adm.publishBtn.title = !token ? 'Connexion au serveur nécessaire'
        : serverOutdated() ? 'Mettez d’abord à jour showcase.php sur le serveur' : '';
      adm.publishBtn.textContent = adm.force ? 'Publier quand même' : 'Publier';
    }

    /* ---- Cards tab: list ---- */
    function thumbFor(p) {
      const t = h('span', { class: 'pcdc-thumb' + (p.icon ? ' is-icon' : '') });
      t.style.setProperty('--pcdc-accent', p.accent);
      const src = p.icon || (p.media ? (p.media.type === 'video' ? p.media.poster : p.media.src) : '');
      if (src) t.append(h('img', { src: resolve(src), alt: '' }));
      else t.textContent = p.title.charAt(0) || '?';
      return t;
    }
    function renderCardList() {
      if (!adm || adm.editing) return;
      const list = adm.cardsList;
      list.textContent = '';
      work.projects.forEach((p, i) => {
        const up = h('button', { type: 'button', class: 'pcdc-ico', 'aria-label': `Monter ${p.title}`, disabled: i === 0 });
        up.innerHTML = ICONS.up;
        up.addEventListener('click', () => moveCard(i, -1));
        const down = h('button', { type: 'button', class: 'pcdc-ico', 'aria-label': `Descendre ${p.title}`, disabled: i === work.projects.length - 1 });
        down.innerHTML = ICONS.down;
        down.addEventListener('click', () => moveCard(i, 1));
        const eye = h('button', { type: 'button', class: 'pcdc-ico', 'aria-label': p.hidden ? `Afficher ${p.title}` : `Masquer ${p.title}`, 'aria-pressed': String(!p.hidden) });
        eye.innerHTML = p.hidden ? ICONS.eyeOff : ICONS.eye;
        eye.addEventListener('click', () => { p.hidden = !p.hidden; changed(); renderCardList(); });
        const edit = h('button', { type: 'button', class: 'pcdc-ico', 'aria-label': `Modifier ${p.title}` });
        edit.innerHTML = ICONS.edit;
        edit.addEventListener('click', () => openEditor(p.id));
        const open = h('button', { type: 'button', class: 'pcdc-adm-open' },
          h('strong', null, p.title, p.hidden ? h('em', null, 'Masquée') : null),
          h('small', null, p.kind || (p.appUrl || p.articleUrl || 'Sans lien')));
        open.addEventListener('click', () => openEditor(p.id));
        list.append(h('div', { class: 'pcdc-adm-card' + (p.hidden ? ' is-hidden' : '') },
          thumbFor(p), open, h('div', { class: 'pcdc-adm-tools' }, up, down, eye, edit)));
      });
      if (!work.projects.length) list.append(h('p', { class: 'pcdc-dlg-note' }, 'Aucune carte. Ajoutez-en une.'));
    }
    function moveCard(i, d) {
      const j = i + d;
      if (j < 0 || j >= work.projects.length) return;
      const arr = work.projects;
      [arr[i], arr[j]] = [arr[j], arr[i]];
      changed();
      renderCardList();
    }
    function addCard() {
      const ids = new Set(work.projects.map((p) => p.id));
      let id = 'nouvelle-carte';
      for (let k = 2; ids.has(id); k++) id = `nouvelle-carte-${k}`;
      work.projects.push({
        id, title: 'Nouvelle carte', kind: '', badge: '', description: '', tags: [], icon: '', media: null,
        appUrl: '', articleUrl: '', cta: '', accent: '#1001ef', hidden: false,
      });
      changed({ focusId: id });
      openEditor(id);
    }

    /* ---- Cards tab: editor ---- */
    function field(label, input, help) {
      const id = 'pcdc-f-' + Math.random().toString(36).slice(2, 8);
      input.id = id;
      return [h('label', { class: 'pcdc-lbl', for: id }, ...label), input, help].filter(Boolean);
    }
    function openEditor(id) {
      const p = work.projects.find((x) => x.id === id);
      if (!p) return;
      adm.editing = id;
      adm.cardsView.hidden = true;
      const ed = adm.editor;
      ed.hidden = false;
      ed.textContent = '';

      const back = h('button', { type: 'button', class: 'pcdc-btn is-small' }, '← Toutes les cartes');
      back.addEventListener('click', () => { adm.editing = null; ed.hidden = true; adm.cardsView.hidden = false; renderCardList(); });
      const visible = h('input', { type: 'checkbox', class: 'pcdc-toggle', 'aria-label': 'Carte visible' });
      visible.checked = !p.hidden;
      visible.addEventListener('change', () => { p.hidden = !visible.checked; changed(); });
      ed.append(h('div', { class: 'pcdc-edit-head' }, back,
        h('label', { class: 'pcdc-inline' }, h('span', { class: 'pcdc-help' }, 'Visible'), visible)));

      const text = (name, value, max, attrs) => {
        const el = h('input', { class: 'pcdc-field', value, maxlength: max, ...(attrs || {}) });
        el.addEventListener('input', () => { p[name] = str(el.value, max); changed({ focusId: p.id }); });
        return el;
      };
      const urlInput = (name) => {
        const el = h('input', { class: 'pcdc-field', value: p[name], placeholder: 'https://...', inputmode: 'url' });
        const warn = h('p', { class: 'pcdc-warn', hidden: true }, 'Adresse ignorée : elle doit commencer par https:// ou http://');
        el.addEventListener('input', () => {
          p[name] = link(el.value);
          warn.hidden = !(el.value.trim() && !p[name]);
          changed({ focusId: p.id });
        });
        return [el, warn];
      };

      ed.append(...field(['Titre'], text('title', p.title, 80, { required: true })));
      ed.append(...field(['Catégorie ', h('small', null, '(ligne au-dessus du titre)')], text('kind', p.kind, 80)));
      // Badge: suggestions, but any short text is accepted.
      const badgeList = 'pcdc-badges-' + Math.random().toString(36).slice(2, 8);
      ed.append(...field(['Badge ', h('small', null, '(en haut à droite ; vide : Application ou Projet selon les liens)')],
        text('badge', p.badge, 40, { list: badgeList, placeholder: p.appUrl ? UI.badgeApp : UI.badgeProject })));
      ed.append(h('datalist', { id: badgeList },
        ...['Application', 'Expérience immersive', 'Exposition augmentée', 'Installation', 'Prototype', 'Projet'].map((v) => h('option', { value: v }))));
      const desc = h('textarea', { class: 'pcdc-field', maxlength: 600, rows: 3 });
      desc.value = p.description;
      desc.addEventListener('input', () => { p.description = str(desc.value, 600); changed({ focusId: p.id }); });
      ed.append(...field(['Description ', h('small', null, '(visible au survol)')], desc));
      const tagsIn = h('input', { class: 'pcdc-field', value: p.tags.join(', '), placeholder: '3D, WebXR, IA' });
      tagsIn.addEventListener('input', () => {
        p.tags = tagsIn.value.split(',').map((t) => str(t, 40)).filter(Boolean).slice(0, 10);
        changed({ focusId: p.id });
      });
      ed.append(...field(['Mots-clés ', h('small', null, '(séparés par des virgules)')], tagsIn));

      const [appIn, appWarn] = urlInput('appUrl');
      ed.append(...field(['Lien de l’application'], appIn, appWarn));
      const [artIn, artWarn] = urlInput('articleUrl');
      ed.append(...field(['Lien de l’article de présentation'], artIn, artWarn));
      ed.append(...field(['Texte du bouton ', h('small', null, '(vide : « Ouvrir l’application », ou « Découvrir le projet » sans application)')],
        text('cta', p.cta, 40, { placeholder: p.appUrl ? UI.openApp : UI.discover })));

      // Icon.
      ed.append(h('p', { class: 'pcdc-sec-title' }, 'Icône'));
      ed.append(mediaBox({
        kind: 'icon',
        accept: 'image/*',
        get: () => p.icon,
        set: (v) => { p.icon = v; },
        thumb: () => (p.icon ? { src: p.icon, contain: true } : null),
        help: 'Carré, 512 px conseillé (PNG ou WebP transparent). Sans icône, le média remplit le carré de verre.',
        onUploaded: (info) => { if (info.accent) p.accent = info.accent; },
        refresh: () => openEditor(p.id),
        focusId: p.id,
      }));

      // Featured media.
      ed.append(h('p', { class: 'pcdc-sec-title' }, 'Média à la une'));
      const typeSel = h('select', { class: 'pcdc-field', 'aria-label': 'Type de média' },
        h('option', { value: '' }, 'Aucun'), h('option', { value: 'image' }, 'Image'),
        h('option', { value: 'gif' }, 'GIF animé'), h('option', { value: 'video' }, 'Vidéo (MP4, WebM)'));
      typeSel.value = p.media ? p.media.type : '';
      typeSel.addEventListener('change', () => {
        if (!typeSel.value) p.media = null;
        else p.media = { type: typeSel.value, src: p.media ? p.media.src : '', poster: p.media ? p.media.poster : '' };
        changed({ focusId: p.id });
        openEditor(p.id);
      });
      ed.append(typeSel);
      if (p.media) {
        ed.append(mediaBox({
          kind: 'media',
          accept: p.media.type === 'video' ? 'video/mp4,video/webm' : p.media.type === 'gif' ? 'image/gif' : 'image/*',
          get: () => p.media.src,
          set: (v) => { p.media.src = v; },
          thumb: () => (p.media.type === 'video' ? (p.media.poster ? { src: p.media.poster } : null) : p.media.src ? { src: p.media.src } : null),
          help: p.media.type === 'video'
            ? 'MP4 court et léger, carré 1024 px conseillé. L’image d’aperçu est créée automatiquement au téléversement.'
            : p.media.type === 'gif' ? 'Un GIF lourd ralentit la tablette : préférez une vidéo MP4 quand c’est possible.'
              : 'Carré 1024 × 1024 conseillé. Les grandes images sont réduites automatiquement.',
          onUploaded: (info) => {
            if (info.poster) p.media.poster = info.poster;
            if (!p.icon && info.accent) p.accent = info.accent;
          },
          refresh: () => openEditor(p.id),
          focusId: p.id,
        }));
        if (p.media.type === 'video') {
          ed.append(mediaBox({
            kind: 'poster',
            accept: 'image/*',
            label: 'Image d’aperçu de la vidéo',
            get: () => p.media.poster,
            set: (v) => { p.media.poster = v; },
            thumb: () => (p.media.poster ? { src: p.media.poster } : null),
            help: 'Affichée avant la lecture et au dos de la carte.',
            refresh: () => openEditor(p.id),
            focusId: p.id,
          }));
        }
      }

      // Accent colour.
      ed.append(h('p', { class: 'pcdc-sec-title' }, 'Couleur d’accent'));
      const col = h('input', { type: 'color', class: 'pcdc-color', value: p.accent, 'aria-label': 'Couleur d’accent' });
      col.addEventListener('input', () => { p.accent = color(col.value); changed({ focusId: p.id }); });
      ed.append(h('div', { class: 'pcdc-inline' }, col, h('span', { class: 'pcdc-help' }, 'Teinte de la carte, reprise automatiquement de l’icône téléversée.')));

      // Delete: two steps, so a slip of the finger on the tablet costs nothing.
      const del = h('button', { type: 'button', class: 'pcdc-btn is-small' }, 'Supprimer cette carte');
      let armed = 0;
      del.addEventListener('click', () => {
        if (!armed) {
          del.textContent = 'Confirmer la suppression';
          del.classList.add('is-danger');
          armed = setTimeout(() => { armed = 0; del.textContent = 'Supprimer cette carte'; del.classList.remove('is-danger'); }, 4000);
          return;
        }
        clearTimeout(armed);
        work.projects = work.projects.filter((x) => x.id !== p.id);
        adm.editing = null;
        ed.hidden = true;
        adm.cardsView.hidden = false;
        changed();
        renderCardList();
        updateState(`« ${p.title} » supprimée du brouillon`, 'dirty');
      });
      ed.append(h('div', { class: 'pcdc-danger-zone' },
        h('span', { class: 'pcdc-help' }, 'La suppression reste annulable tant que rien n’est publié.'), del));

      changed({ focusId: p.id });
    }

    // One media slot: preview, path or URL, upload and remove buttons.
    function mediaBox(o) {
      const box = h('div', { class: 'pcdc-media-box' });
      const th = o.thumb();
      const t = h('span', { class: 'pcdc-thumb' + (th && th.contain ? ' is-icon' : '') });
      if (th) t.append(h('img', { src: resolve(th.src), alt: '' }));
      else t.textContent = '·';
      const input = h('input', { class: 'pcdc-field', value: o.get(), placeholder: 'uploads/fichier.webp ou https://...' });
      const warn = h('p', { class: 'pcdc-warn', hidden: true }, 'Chemin ignoré : un chemin du dossier média ou une adresse https://');
      input.addEventListener('input', () => {
        o.set(mediaPath(input.value));
        warn.hidden = !(input.value.trim() && !mediaPath(input.value));
        changed({ focusId: o.focusId });
      });
      input.addEventListener('change', () => o.refresh());
      const bar = h('div', { class: 'pcdc-progress', hidden: true }, h('i'));
      const up = h('button', { type: 'button', class: 'pcdc-btn is-small', disabled: !token }, 'Téléverser');
      if (!token) up.title = 'Connexion au serveur nécessaire';
      up.addEventListener('click', () => pickFile(o.accept, async (file) => {
        const info = await uploadMedia(file, o.kind, bar);
        if (!info) return;
        o.set(info.path);
        if (o.onUploaded) o.onUploaded(info);
        changed({ focusId: o.focusId });
        o.refresh();
      }));
      const rm = h('button', { type: 'button', class: 'pcdc-btn is-small is-quiet' }, 'Retirer');
      rm.addEventListener('click', () => { o.set(''); changed({ focusId: o.focusId }); o.refresh(); });
      box.append(t, h('div', null,
        o.label ? h('p', { class: 'pcdc-help' }, o.label) : null,
        input, warn,
        h('div', { class: 'pcdc-media-btns' }, up, rm),
        bar,
        h('p', { class: 'pcdc-help' }, o.help)));
      return box;
    }

    /* ---- Uploads ---- */
    // The file input and the download link are created INSIDE the dialog:
    // while a modal dialog is open, the rest of the page is inert.
    function pickFile(accept, cb) {
      const inp = h('input', { type: 'file', accept, hidden: true });
      inp.addEventListener('change', () => {
        const file = inp.files && inp.files[0];
        inp.remove();
        if (file) cb(file);
      });
      admBox.append(inp);
      inp.click();
    }
    function loadImage(file) {
      return new Promise((ok, ko) => {
        const url = URL.createObjectURL(file);
        const img = new Image();
        img.onload = () => ok({ img, url });
        img.onerror = () => { URL.revokeObjectURL(url); ko(new Error('image')); };
        img.src = url;
      });
    }
    const toBlob = (canvas, type, q) => new Promise((ok) => canvas.toBlob(ok, type, q));
    // Dominant saturated colour of a picture: the card accent.
    function accentOf(source) {
      const c = document.createElement('canvas');
      c.width = c.height = 48;
      const x = c.getContext('2d', { willReadFrequently: true });
      x.drawImage(source, 0, 0, 48, 48);
      const d = x.getImageData(0, 0, 48, 48).data;
      const buckets = new Map();
      for (let i = 0; i < d.length; i += 4) {
        const [r, g, b, a] = [d[i], d[i + 1], d[i + 2], d[i + 3]];
        if (a < 200) continue;
        const mx = Math.max(r, g, b), mn = Math.min(r, g, b);
        const sat = mx ? (mx - mn) / mx : 0;
        if (sat < 0.35 || mx < 77) continue;
        let hue = 0;
        if (mx !== mn) {
          if (mx === r) hue = ((g - b) / (mx - mn)) % 6; else if (mx === g) hue = (b - r) / (mx - mn) + 2; else hue = (r - g) / (mx - mn) + 4;
        }
        const k = Math.floor((((hue * 60) + 360) % 360) / 15);
        const e = buckets.get(k) || { w: 0, r: 0, g: 0, b: 0, n: 0 };
        e.w += sat * (mx / 255); e.r += r; e.g += g; e.b += b; e.n++;
        buckets.set(k, e);
      }
      let best = null;
      buckets.forEach((e) => { if (!best || e.w > best.w) best = e; });
      if (!best) return null;
      return '#' + [best.r, best.g, best.b].map((v) => Math.round(v / best.n).toString(16).padStart(2, '0')).join('');
    }
    // Images are reduced before upload (never enlarged): 512 px for an icon,
    // 1024 px for a featured image. GIFs and videos are sent untouched.
    async function prepareImage(file, kind) {
      const max = kind === 'icon' ? 512 : kind === 'logo' ? 480 : 1024;
      const { img, url } = await loadImage(file);
      try {
        const w0 = img.naturalWidth || 1024;
        const h0 = img.naturalHeight || 1024;
        const scale = Math.min(1, max / Math.max(w0, h0));
        const c = document.createElement('canvas');
        c.width = Math.max(1, Math.round(w0 * scale));
        c.height = Math.max(1, Math.round(h0 * scale));
        c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
        let accent = null;
        try { accent = accentOf(c); } catch (e) { /* unreadable canvas */ }
        const isSvg = file.type === 'image/svg+xml';
        const blob = await toBlob(c, 'image/webp', 0.86);
        const base = file.name.replace(/\.[^.]+$/, '') || 'image';
        if (!blob || (!isSvg && scale === 1 && blob.size >= file.size)) return { blob: file, name: file.name, accent };
        return { blob, name: base + (blob.type === 'image/webp' ? '.webp' : '.png'), accent };
      } finally {
        URL.revokeObjectURL(url);
      }
    }
    // First frame of a video, as its poster image.
    async function posterOf(file) {
      const url = URL.createObjectURL(file);
      try {
        const v = document.createElement('video');
        v.muted = true; v.playsInline = true; v.preload = 'auto'; v.src = url;
        await new Promise((ok, ko) => { v.onloadeddata = ok; v.onerror = ko; setTimeout(ko, 10000); });
        await new Promise((ok) => { v.onseeked = ok; v.currentTime = Math.min(1, (v.duration || 3) / 3); setTimeout(ok, 4000); });
        const scale = Math.min(1, 1024 / Math.max(v.videoWidth || 1, v.videoHeight || 1));
        const c = document.createElement('canvas');
        c.width = Math.max(1, Math.round((v.videoWidth || 640) * scale));
        c.height = Math.max(1, Math.round((v.videoHeight || 640) * scale));
        c.getContext('2d').drawImage(v, 0, 0, c.width, c.height);
        let accent = null;
        try { accent = accentOf(c); } catch (e) { /* ignore */ }
        const blob = await toBlob(c, 'image/webp', 0.82);
        return blob ? { blob, name: file.name.replace(/\.[^.]+$/, '') + '-apercu.webp', accent } : null;
      } catch (e) {
        return null;
      } finally {
        URL.revokeObjectURL(url);
      }
    }
    function sendFile(blob, name, onProgress) {
      return new Promise((resolveUp) => {
        const xhr = new XMLHttpRequest();
        xhr.open('POST', endpoint('upload'));
        xhr.setRequestHeader('X-PCDC-Token', token);
        xhr.responseType = 'json';
        xhr.upload.onprogress = (e) => { if (e.lengthComputable && onProgress) onProgress(e.loaded / e.total); };
        xhr.onload = () => resolveUp({ status: xhr.status, data: xhr.response });
        xhr.onerror = () => resolveUp({ status: 0, data: null });
        const fd = new FormData();
        fd.append('file', blob, name);
        xhr.send(fd);
      });
    }
    const UPLOAD_ERRORS = {
      0: 'Serveur injoignable.',
      401: 'Session expirée : saisissez à nouveau le code.',
      413: 'Fichier trop lourd pour le serveur.',
      415: 'Format refusé : PNG, JPEG, WebP, AVIF, GIF, MP4 ou WebM.',
    };
    async function uploadWithBar(file, kind, bar, onProgress) {
      if (!token) { updateState('Téléversement impossible sans connexion au serveur', 'error'); return null; }
      bar.hidden = false;
      const fill = bar.firstElementChild;
      fill.style.width = '0%';
      updateState('Téléversement de « ' + file.name + ' »...');
      let prepared = { blob: file, name: file.name, accent: null };
      if (/^image\/(png|jpeg|webp|avif|svg\+xml)$/.test(file.type)) {
        try { prepared = await prepareImage(file, kind); } catch (e) { /* send as is */ }
      }
      const res = await sendFile(prepared.blob, prepared.name, (r) => { fill.style.width = Math.round(r * 100) + '%'; if (onProgress) onProgress(r); });
      bar.hidden = true;
      if (res.status === 200 && res.data && res.data.ok) {
        updateState('Fichier téléversé : ' + res.data.path, 'ok');
        return { path: res.data.path, accent: prepared.accent };
      }
      if (res.status === 401) expireSession();
      updateState(UPLOAD_ERRORS[res.status] || `Échec du téléversement (erreur ${res.status}).`, 'error');
      return null;
    }
    async function uploadMedia(file, kind, bar) {
      const main = await uploadWithBar(file, kind, bar);
      if (!main) return null;
      const info = { path: main.path, accent: main.accent, poster: '' };
      if (kind === 'media' && file.type.startsWith('video/')) {
        const poster = await posterOf(file);
        if (poster) {
          const res = await sendFile(poster.blob, poster.name);
          if (res.status === 200 && res.data && res.data.ok) { info.poster = res.data.path; info.accent = poster.accent; }
        }
      }
      return info;
    }

    /* ---- Settings tabs (Affichage, En-tête, Pied de page) ----
       Built from descriptors: each control reads and writes ONE path of the
       working copy, and every value goes through the SPEC rule of that path
       before it is kept, exactly as the server will clean it. */
    const ruleAt = (path) => path.split('.').reduce((a, k) => (a ? a[k] : undefined), SPEC);
    const getAt = (path) => path.split('.').reduce((a, k) => (a ? a[k] : undefined), work);
    function setAt(path, raw, opts) {
      const keys = path.split('.');
      const last = keys.pop();
      keys.reduce((a, k) => a[k], work)[last] = cleanBy(ruleAt(path), raw);
      changed(opts);
    }
    const uid = () => 'pcdc-s-' + Math.random().toString(36).slice(2, 8);
    const labelled = (label, help) => h('span', null, label, help ? h('small', null, help) : null);

    function cToggle(path, label, help, opts) {
      const inp = h('input', { type: 'checkbox', class: 'pcdc-toggle' });
      inp.checked = getAt(path);
      inp.addEventListener('change', () => setAt(path, inp.checked, opts));
      return h('label', { class: 'pcdc-row' }, labelled(label, help), inp);
    }
    function cRange(path, label, o = {}) {
      const r = ruleAt(path);
      const inp = h('input', { type: 'range', min: r[1], max: o.max !== undefined ? o.max : r[2], step: o.step || 1, 'aria-label': label });
      inp.value = getAt(path);
      const out = h('output');
      const show = () => { const v = Number(inp.value); out.textContent = o.fmt ? o.fmt(v) : v + ' %'; };
      show();
      inp.addEventListener('input', () => { show(); setAt(path, Number(inp.value)); });
      return h('div', { class: 'pcdc-row is-wide' }, labelled(label, o.help), h('div', { class: 'pcdc-range' }, inp, out));
    }
    function cSeg(path, label, options, o = {}) {
      const name = uid();
      const seg = h('div', { class: 'pcdc-seg', role: 'radiogroup', 'aria-label': label });
      options.forEach(([v, text]) => {
        const inp = h('input', { type: 'radio', name, value: v });
        inp.checked = getAt(path) === v;
        inp.addEventListener('change', () => { if (!inp.checked) return; if (o.before) o.before(v); setAt(path, v, o.opts); });
        seg.append(h('label', null, inp, h('span', null, text)));
      });
      return h('div', { class: 'pcdc-row' }, labelled(label, o.help), seg);
    }
    function cText(path, label, o = {}) {
      const id = uid();
      const inp = h('input', { class: 'pcdc-field', id, maxlength: ruleAt(path)[1], placeholder: o.placeholder || null });
      inp.value = getAt(path);
      inp.addEventListener('input', () => setAt(path, inp.value));
      return [h('label', { class: 'pcdc-lbl', for: id }, label), inp];
    }
    function cUrl(path, label, o = {}) {
      const id = uid();
      const inp = h('input', { class: 'pcdc-field', id, placeholder: 'https://...', inputmode: 'url' });
      inp.value = getAt(path);
      const warn = h('p', { class: 'pcdc-warn', hidden: true }, 'Adresse ignorée : elle doit commencer par https:// ou http://');
      inp.addEventListener('input', () => {
        setAt(path, inp.value);
        warn.hidden = !(inp.value.trim() && !getAt(path));
        if (o.after) o.after();
      });
      return [h('label', { class: 'pcdc-lbl', for: id }, label, o.small ? h('small', null, ' ' + o.small) : null), inp, warn];
    }
    // Colour: "Auto" follows the theme (value ""), otherwise the colour picked.
    function cColor(path, label) {
      const cur = getAt(path);
      const auto = h('input', { type: 'checkbox' });
      auto.checked = !cur;
      const col = h('input', { type: 'color', class: 'pcdc-color', 'aria-label': label, disabled: !cur });
      col.value = cur || '#1001ef';
      auto.addEventListener('change', () => { col.disabled = auto.checked; setAt(path, auto.checked ? '' : col.value); });
      col.addEventListener('input', () => { if (!auto.checked) setAt(path, col.value); });
      return h('div', null, h('span', { class: 'pcdc-lbl' }, label),
        h('div', { class: 'pcdc-colorctl' }, h('label', null, auto, 'Auto (thème)'), col));
    }
    function cSelect(path, label, options) {
      const id = uid();
      const sel = h('select', { class: 'pcdc-field', id });
      options.forEach(([v, text]) => sel.append(h('option', { value: v }, text)));
      sel.value = getAt(path);
      sel.addEventListener('change', () => setAt(path, sel.value));
      return [h('label', { class: 'pcdc-lbl', for: id }, label), sel];
    }
    const fontOptions = (path, inherit) => ruleAt(path)[1].map((k) => [k, k ? fontLabel(k) : inherit]);
    const WEIGHT_NAMES = { '': 'Par défaut', 300: 'Light', 400: 'Regular', 500: 'Medium', 600: 'Semi-bold', 700: 'Bold', 800: 'Extra-bold', 900: 'Black' };
    // Size, font, weight and colour of one text element.
    const sizeOf = (k, label) => cRange(`els.${k}.size`, label || 'Taille', { step: 5 });
    const styleOf = (k) => [
      h('div', { class: 'pcdc-duo' },
        h('div', null, ...cSelect(`els.${k}.font`, 'Police', fontOptions(`els.${k}.font`, 'Police générale'))),
        h('div', null, ...cSelect(`els.${k}.weight`, 'Graisse', ruleAt(`els.${k}.weight`)[1].map((w) => [w, WEIGHT_NAMES[w]])))),
      cColor(`els.${k}.color`, 'Couleur'),
    ];

    // One folding box per element: name, on / off switch, details underneath.
    const openBoxes = new Set();                   // unfolded boxes survive a rebuild
    function box(key, title, o) {
      const wrap = h('div', { class: 'pcdc-el' });
      const bodyId = uid();
      const isOpen = openBoxes.has(key);
      const name = h('button', { type: 'button', class: 'pcdc-el-name', 'aria-expanded': String(isOpen), 'aria-controls': bodyId });
      name.innerHTML = CHEVRON;
      name.append(h('span', null, title, o.sub ? h('small', null, o.sub) : null));
      const body = h('div', { class: 'pcdc-el-body', id: bodyId, hidden: !isOpen });
      name.addEventListener('click', () => {
        const open = body.hidden;
        body.hidden = !open;
        name.setAttribute('aria-expanded', String(open));
        if (open) openBoxes.add(key); else openBoxes.delete(key);
        if (open && o.onOpen) o.onOpen();
      });
      const top = h('div', { class: 'pcdc-el-head' }, name);
      if (o.show) {
        const sw = h('input', { type: 'checkbox', class: 'pcdc-toggle', 'aria-label': 'Afficher : ' + title });
        sw.checked = getAt(o.show);
        wrap.classList.toggle('is-off', !sw.checked);
        sw.addEventListener('change', () => { wrap.classList.toggle('is-off', !sw.checked); setAt(o.show, sw.checked, o.opts); });
        top.append(sw);
      }
      body.append(...o.body().flat());
      wrap.append(top, body);
      return wrap;
    }
    const sec = (text) => h('p', { class: 'pcdc-sec-title' }, text);
    const intro = (text) => h('p', { class: 'pcdc-set-intro' }, text);

    const PANELS = {
      display: () => [
        sec('Sections'),
        intro('Chaque section s’allume et s’éteint séparément. Le détail de chaque élément se règle dans les onglets En-tête et Pied de page.'),
        part ? h('p', { class: 'pcdc-warn' }, part === 'footer'
          ? 'Ce bloc est le bandeau seul (data-part="footer") : il n’affiche que le pied de page, quels que soient ces réglages.'
          : 'Ce bloc affiche la vitrine sans pied de page (data-part="showcase").') : null,
        cToggle('layout.header', 'En-tête', 'Sur-titre, titre et sélecteur d’affichage'),
        cToggle('layout.showcase', 'Vitrine', 'Les cartes : carrousel, grille ou liste'),
        cToggle('layout.controls', 'Commandes du carrousel', 'Flèches, légende, points et aide'),
        cToggle('layout.footer', 'Pied de page', 'Éteint, « .dev » reste accessible, discret, en bas à gauche'),

        sec('Hauteur et espacements'),
        cRange('layout.height', 'Hauteur du bloc', { step: 5, fmt: (v) => (v ? v + ' % de l’écran' : 'selon le contenu'),
          help: 'À 100 %, la section remplit exactement l’écran, marges de la page comprises (tablette, plein écran F11), et les cartes prennent toute la hauteur libre' }),
        cRange('layout.offset', 'Position verticale du carrousel', { fmt: (v) => (v > 0 ? '+' : '') + v + ' %', help: 'Descend (+) ou remonte (−) l’anneau de cartes' }),
        cRange('layout.pad', 'Marges du bloc', { step: 5 }),
        cRange('layout.gapHead', 'Espace sous l’en-tête', { step: 5 }),
        cRange('layout.gapNav', 'Espace entre les cartes et les commandes', { step: 5 }),
        cRange('layout.gapFoot', 'Espace au-dessus du pied de page', { step: 5 }),

        sec('Apparence'),
        cSeg('settings.theme', 'Thème', [['auto', 'Auto'], ['dark', 'Sombre'], ['light', 'Clair']], { help: 'Auto suit le réglage du navigateur' }),
        cColor('settings.bg', 'Couleur de fond'),
        cToggle('layout.bgNone', 'Fond transparent', 'Le bloc laisse voir la page derrière lui (bandeau posé sur une image, une vidéo)'),
        ...cSelect('settings.font', 'Police générale', fontOptions('settings.font')),
        cRange('layout.text', 'Taille générale des textes', { step: 5, help: 'Tous les textes hors cartes. Chaque élément a en plus sa propre taille' }),

        sec('Cartes'),
        cSeg('settings.view', 'Affichage par défaut', [['carousel', 'Carrousel'], ['grid', 'Grille'], ['list', 'Liste']],
          { before: () => store.del(KEY.view), opts: { view: true } }),
        cRange('layout.cardSize', 'Taille des cartes du carrousel', { step: 5 }),
        cRange('layout.cardText', 'Taille des textes des cartes', { step: 5 }),
        cRange('settings.tile', 'Taille du carré', { fmt: (v) => v + ' % de la carte' }),
        cToggle('layout.cardNum', 'Numéro'),
        cToggle('layout.cardBadge', 'Badge', 'Application, Expérience immersive...'),
        cToggle('layout.cardKind', 'Catégorie', 'Ligne au-dessus du titre'),
        cToggle('layout.cardDesc', 'Description', 'Au survol ou au toucher'),
        cToggle('layout.cardTags', 'Mots-clés'),

        sec('Comportement'),
        cRange('settings.attract', 'Rotation automatique', { step: 5, max: 60, fmt: (v) => (v ? v + ' s' : 'désactivée'),
          help: 'Délai sans interaction avant que le carrousel tourne seul (salon)' }),
        cToggle('settings.wheel', 'Défilement latéral à la molette', 'Ordinateur : la molette fait tourner le carrousel au lieu de faire défiler la page'),
        cSeg('settings.cardLink', 'Clic sur la carte', [['app', 'Application'], ['article', 'Article']]),
        cToggle('settings.newTab', 'Liens dans un nouvel onglet'),
      ],

      header: () => [
        sec('En-tête'),
        cSeg('header.align', 'Alignement', [['left', 'À gauche'], ['center', 'Centré']]),
        box('kicker', 'Sur-titre', { show: 'els.kicker.show', body: () => [cText('header.kicker', 'Texte'), sizeOf('kicker'), styleOf('kicker')] }),
        box('title', 'Titre', { show: 'els.title.show', body: () => [cText('header.title', 'Texte'), sizeOf('title'), styleOf('title')] }),
        box('switch', 'Sélecteur d’affichage', { sub: 'Carrousel, Grille, Liste', show: 'settings.switch', opts: { view: true }, body: () => [sizeOf('switch')] }),
        sec('Commandes du carrousel'),
        box('arrows', 'Flèches', { show: 'els.arrows.show', body: () => [sizeOf('arrows')] }),
        box('caption', 'Légende', { sub: 'Numéro, titre et catégorie de la carte de face', show: 'els.caption.show', body: () => [sizeOf('caption'), styleOf('caption')] }),
        box('dots', 'Points', { show: 'els.dots.show', body: () => [sizeOf('dots')] }),
        box('hint', 'Aide', { sub: 'S’efface au premier geste', show: 'els.hint.show', body: () => [cText('header.hint', 'Texte'), sizeOf('hint'), styleOf('hint')] }),
      ],

      footer: () => [
        sec('Pied de page'),
        cToggle('layout.footLine', 'Filet de séparation', 'Ligne fine au-dessus du pied de page'),
        box('logo', 'Logo de marque', { sub: 'En bas à gauche', show: 'footer.logo', body: () => [sizeOf('logo', 'Hauteur'), logoCtl()] }),
        box('copy', 'Mention', { sub: '© PCdlabs...', show: 'els.copy.show', body: () => [cText('footer.copyText', 'Texte'), sizeOf('copy'), styleOf('copy')] }),
        box('site', 'Lien du site', { show: 'els.site.show', body: () => [cText('footer.siteText', 'Texte'), cUrl('footer.siteUrl', 'Adresse', { small: '(aussi celle du logo)' }), sizeOf('site'), styleOf('site')] }),
        box('slogan', 'Slogan', { sub: 'Au centre', show: 'footer.slogan', body: () => [
          cText('footer.sloganText', 'Texte'),
          cSeg('layout.sloganAnchor', 'Centré sur', [['screen', 'L’écran'], ['block', 'Le bloc']]),
          sizeOf('slogan'), styleOf('slogan')] }),
        box('sub', 'Sous-titre', { sub: 'Sous le slogan', show: 'els.sub.show', body: () => [cText('footer.subText', 'Texte'), sizeOf('sub'), styleOf('sub')] }),
        box('qr', 'QR code', { sub: 'À droite, dépasse du pied de page', show: 'footer.qr', body: () => [
          sizeOf('qr'),
          cUrl('footer.qrUrl', 'Adresse encodée', { small: '(vide : l’adresse de cette page)', after: qrPreview }),
          cText('footer.qrCaption', 'Légende'),
          (adm.qrPrev = h('div', { class: 'pcdc-qr-preview' }, h('div'), h('p')))] }),
        box('qrHint', 'Texte flottant avec flèche', { sub: 'Invite à flasher le QR code', show: 'els.qrHint.show', body: () => [
          cText('footer.qrHintText', 'Texte'), sizeOf('qrHint'), styleOf('qrHint')] }),
        sec('Bandeau pour la vidéo'),
        box('export', 'Exporter le bandeau', { sub: 'Image ou vidéo transparente, à poser sur une vidéo (Premiere Pro...)', onOpen: exportPreviewSoon, body: exportCtl }),
      ],
    };

    function buildSettings() {
      adm.setPanels.forEach((panel) => {
        panel.textContent = '';
        panel.append(...PANELS[panel.dataset.panel]().flat().filter(Boolean));
      });
      qrPreview();
    }

    // Logo: a path or an address, or a file sent to the server.
    function logoCtl() {
      const id = uid();
      const inp = h('input', { class: 'pcdc-field', id, placeholder: 'uploads/mon-logo.webp ou https://...' });
      inp.value = work.footer.logoUrl;
      const warn = h('p', { class: 'pcdc-warn', hidden: true }, 'Chemin ignoré : un chemin du dossier média ou une adresse https://');
      inp.addEventListener('input', () => {
        setAt('footer.logoUrl', inp.value);
        warn.hidden = !(inp.value.trim() && !work.footer.logoUrl);
      });
      const bar = h('div', { class: 'pcdc-progress', hidden: true }, h('i'));
      const up = h('button', { type: 'button', class: 'pcdc-btn is-small', disabled: !token }, 'Téléverser');
      if (!token) up.title = 'Connexion au serveur nécessaire';
      up.addEventListener('click', () => pickFile('image/*', async (file) => {
        const res = await uploadWithBar(file, 'logo', bar);
        if (!res) return;
        work.footer.logo = true;
        setAt('footer.logoUrl', res.path);
        buildSettings();
      }));
      return [h('label', { class: 'pcdc-lbl', for: id }, 'Logo personnalisé ', h('small', null, '(vide : logo Présent Composé design officiel)')),
        h('div', { class: 'pcdc-inline' }, inp, up), bar, warn];
    }

    function qrPreview() {
      const prev = adm.qrPrev;
      if (!prev) return;
      const text = work.footer.qrUrl || pageUrl();
      prev.firstElementChild.innerHTML = QR.svg(text);
      prev.lastElementChild.textContent = (work.footer.qrUrl ? '' : 'Cette page : ') + text;
    }

    /* ==================================================================
       BANNER EXPORT (footer tab)
       The footer is laid out again, off screen, at the width of the video
       frame; then it is painted on a canvas element by element: every word
       where the browser placed it, the logo, the QR code and the arrow as
       SVG images. The QR card and its note are painted at the exact point
       of their CSS animation (same keyframes, same easing), so an export of
       4.8 s, one float of the card, loops without a jump. Nothing is cut:
       the frame is checked against the card at every point of its float,
       shadow included.
       Output: transparent PNG, PNG sequence (ZIP), or a QuickTime MOV with
       the PNG codec and an alpha channel, read by Premiere Pro.
       ================================================================== */
    const LOOP_S = 4.8;                            // period of pcdc-qr-float (CSS)
    const EXPORT_FRAMES = {
      fhd: { w: 1920, h: 1080, layout: 1920 },
      uhd: { w: 3840, h: 2160, layout: 1920 },    // laid out as 1920, painted at twice the size
      vertical: { w: 1080, h: 1920, layout: 900 },
      strip: { w: 1920, h: 0, layout: 1920 },     // height of the banner only
    };
    const EXPORT_CHOICES = { format: ['mov', 'zip', 'png'], frame: Object.keys(EXPORT_FRAMES), fps: ['25', '30', '50'], loops: ['1', '2', '3'], theme: ['dark', 'light', 'page'], bg: ['none', 'theme'] };
    const exportOpts = { format: 'mov', frame: 'fhd', fps: '25', loops: '1', theme: 'dark', bg: 'none', text: 120, shadow: true };
    try {
      const saved = obj(JSON.parse(store.get('pcdc-export') || '{}'));
      for (const [k, list] of Object.entries(EXPORT_CHOICES)) exportOpts[k] = oneOf(saved[k], list, exportOpts[k]);
      exportOpts.text = int(saved.text, 60, 200, exportOpts.text);
      exportOpts.shadow = bool(saved.shadow, exportOpts.shadow);
    } catch (e) { /* defaults */ }
    const saveExport = () => store.set('pcdc-export', JSON.stringify(exportOpts));

    // CSS "ease-in-out", that is cubic-bezier(.42, 0, .58, 1), solved for x.
    function easeInOut(x) {
      const bx = (s) => 3 * 0.42 * s * (1 - s) * (1 - s) + 3 * 0.58 * s * s * (1 - s) + s * s * s;
      let lo = 0;
      let hi = 1;
      for (let i = 0; i < 40; i++) { const m = (lo + hi) / 2; if (bx(m) < x) lo = m; else hi = m; }
      const s = (lo + hi) / 2;
      return 3 * s * s * (1 - s) + s * s * s;
    }
    // pcdc-qr-float and pcdc-hint-float at a point p of the loop (0 to 1).
    function floatAt(p) {
      const q = ((p % 1) + 1) % 1;
      const e = easeInOut(q < 0.5 ? q * 2 : (q - 0.5) * 2);
      const k = q < 0.5 ? e : 1 - e;               // 0 at rest, 1 at the top of the float
      return { rot: -4 + 2 * k, ty: -7 * k, hx: -4 * k, hy: -6 * k };
    }

    const scratch = document.createElement('canvas').getContext('2d');
    const loadImg = (src, cors) => new Promise((ok, ko) => {
      const i = new Image();
      if (cors) i.crossOrigin = 'anonymous';
      i.onload = () => ok(i);
      i.onerror = () => ko(new Error('image'));
      i.src = src;
    });
    // An inline SVG of the page as an image, at the size it will be painted.
    function svgImage(svg, color, w, h) {
      const c = svg.cloneNode(true);
      c.setAttribute('width', String(Math.max(1, Math.round(w))));
      c.setAttribute('height', String(Math.max(1, Math.round(h))));
      c.setAttribute('color', color);              // "currentColor" inside the drawing
      c.removeAttribute('class');
      c.removeAttribute('style');
      return loadImg('data:image/svg+xml;charset=utf-8,' + encodeURIComponent(new XMLSerializer().serializeToString(c)));
    }
    // Every word of an element, where the browser placed it, with its font.
    function wordsOf(el, ref) {
      const out = [];
      const walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
      for (let node = walk.nextNode(); node; node = walk.nextNode()) {
        const parent = node.parentElement;
        if (!parent || !parent.getClientRects().length) continue;
        const cs = getComputedStyle(parent);
        const font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
        scratch.font = font;
        const re = /\S+/g;
        for (let m = re.exec(node.data); m; m = re.exec(node.data)) {
          const r = document.createRange();
          r.setStart(node, m.index);
          r.setEnd(node, m.index + m[0].length);
          const b = r.getClientRects()[0];
          if (!b) continue;
          out.push({
            text: m[0], font, color: cs.color,
            spacing: cs.letterSpacing === 'normal' ? '0px' : cs.letterSpacing,
            x: b.left - ref.left, y: b.top - ref.top + scratch.measureText(m[0]).fontBoundingBoxAscent,
          });
        }
      }
      return out;
    }

    // Lays the footer out again at the width of the frame and measures it.
    async function exportStage() {
      const F = EXPORT_FRAMES[exportOpts.frame];
      const W = F.layout;
      const scale = F.w / W;
      const pad = Math.max(72, Math.round(W * 0.045));   // video "title safe" margin
      const u = Math.min(26, Math.max(13.5, 6 + 0.007 * (W - 2 * pad))) * (conf.layout.text / 100) * (exportOpts.text / 100);
      const box = document.createElement('section');
      box.className = root.className;
      box.classList.remove('is-fill', 'is-loading', 'is-dragging');
      box.dataset.theme = exportOpts.theme === 'page' ? root.dataset.theme : exportOpts.theme;
      box.setAttribute('aria-hidden', 'true');
      box.style.cssText = root.style.cssText;
      box.style.removeProperty('--pcdc-mid-shift');
      box.style.removeProperty('--pcdc-qr-band');
      box.style.setProperty('--pcdc-u', u.toFixed(3) + 'px');
      box.style.setProperty('--pcdc-pad', pad + 'px');
      Object.assign(box.style, { position: 'fixed', left: '-40000px', top: '0', width: W + 'px', minHeight: '0', background: 'none', overflow: 'visible', pointerEvents: 'none' });
      const f = foot.cloneNode(true);
      f.hidden = false;
      f.querySelectorAll('.pcdc-dev-link').forEach((e) => e.remove());
      const q = f.querySelector('.pcdc-qr');
      const hn = f.querySelector('.pcdc-qr-hint');
      [q, hn].forEach((e) => { e.style.animation = 'none'; e.style.transform = 'none'; });
      box.append(f);
      document.body.append(box);
      try {
        if (document.fonts && document.fonts.ready) await document.fonts.ready;
        const shown = (e) => Boolean(e) && e.getClientRects().length > 0;
        const ref = f.getBoundingClientRect();
        const rel = (r, o) => ({ x: r.left - o.left, y: r.top - o.top, w: r.width, h: r.height });
        const st = { scale, W: F.w, texts: [], images: [], warn: '' };
        const vars = getComputedStyle(box);
        st.bgColor = vars.getPropertyValue('--pcdc-bg').trim();
        const fcs = getComputedStyle(f);
        const lineW = parseFloat(fcs.borderTopWidth) || 0;
        if (lineW && !/,\s*0\)$/.test(fcs.borderTopColor) && fcs.borderTopColor !== 'transparent') st.line = { w: ref.width, h: lineW, color: fcs.borderTopColor };

        const brandBox = f.querySelector('.pcdc-brand');
        if (shown(brandBox)) {
          const img = brandBox.querySelector('.pcdc-brand-img');
          const svg = brandBox.querySelector('svg');
          if (shown(img) && img.getAttribute('src')) {
            try { st.images.push({ ...rel(img.getBoundingClientRect(), ref), img: await loadImg(img.src, true) }); } catch (e) {
              st.warn = 'Logo personnalisé non exporté : son serveur n’autorise pas la copie d’image (CORS).';
            }
          } else if (shown(svg)) {
            const r = svg.getBoundingClientRect();
            st.images.push({ ...rel(r, ref), img: await svgImage(svg, getComputedStyle(svg).color, r.width * scale * 2, r.height * scale * 2) });
          }
        }
        ['.pcdc-copy', '.pcdc-site', '.pcdc-slogan', '.pcdc-sub'].forEach((sel) => {
          const el = f.querySelector(sel);
          if (shown(el)) st.texts.push(...wordsOf(el, ref));
        });

        if (shown(q)) {
          const qRef = q.getBoundingClientRect();
          const qcs = getComputedStyle(q);
          const [ox, oy] = qcs.transformOrigin.split(' ').map(parseFloat);
          const Q = { ...rel(qRef, ref), ox, oy, radius: parseFloat(qcs.borderTopLeftRadius) || 0, bg: qcs.backgroundColor };
          const code = q.querySelector('.pcdc-qr-code svg');
          if (shown(code)) {
            const r = code.getBoundingClientRect();
            Q.code = { ...rel(r, qRef), img: await svgImage(code, '#000', r.width * scale * 3, r.height * scale * 3) };
          }
          const cap = q.querySelector('.pcdc-qr-cap');
          if (shown(cap) && cap.textContent.trim()) {
            Q.cap = { ...rel(cap.getBoundingClientRect(), qRef), radius: parseFloat(getComputedStyle(cap).borderBottomLeftRadius) || 0,
              c1: vars.getPropertyValue('--pcdc-blue').trim() || '#1001ef', c2: vars.getPropertyValue('--pcdc-blue-2').trim() || '#4a5bff', words: wordsOf(cap, qRef) };
          }
          st.qr = Q;
        }
        if (shown(hn)) {
          const hRef = hn.getBoundingClientRect();
          const H = { ...rel(hRef, ref), words: wordsOf(hn, hRef) };
          const arrow = hn.querySelector('svg');
          if (shown(arrow)) {
            const r = arrow.getBoundingClientRect();
            H.arrow = { ...rel(r, hRef), img: await svgImage(arrow, getComputedStyle(arrow).color, r.width * scale * 2, r.height * scale * 2) };
          }
          st.hint = H;
        }

        // Everything the banner can cover during the loop, shadows included.
        const ext = { top: 0, bottom: ref.height, left: 0, right: ref.width };
        // Margins: sideways, above, below (m alone: the same everywhere).
        const grow = (x, y, m, up = m, down = m) => {
          ext.top = Math.min(ext.top, y - up); ext.bottom = Math.max(ext.bottom, y + down);
          ext.left = Math.min(ext.left, x - m); ext.right = Math.max(ext.right, x + m);
        };
        // Margins cover the whole blur of the shadows (a canvas blur fades out
        // over about three times half its radius), so not even a shadow is cut.
        st.images.forEach((i) => { grow(i.x, i.y, 24); grow(i.x + i.w, i.y + i.h, 24); });
        st.texts.forEach((w) => grow(w.x, w.y, 40));
        if (st.hint) { grow(st.hint.x - 4, st.hint.y - 6, 24); grow(st.hint.x + st.hint.w, st.hint.y + st.hint.h, 24); }
        if (st.qr) {
          const Q = st.qr;
          for (let i = 0; i <= 48; i++) {
            const fa = floatAt(i / 48);
            const a = (fa.rot * Math.PI) / 180;
            const c = Math.cos(a);
            const s = Math.sin(a);
            [[0, 0], [Q.w, 0], [0, Q.h], [Q.w, Q.h]].forEach(([px, py]) => {
              const X = px - Q.ox;
              const Y = py - Q.oy + fa.ty;
              // Card shadows: drop shadow 22 px down with a 44 px blur, glow 40 px blur.
              grow(Q.x + Q.ox + X * c - Y * s, Q.y + Q.oy + X * s + Y * c, 64, 56, 76);
            });
          }
        }
        st.ext = ext;

        // Where the banner sits in the frame: at the bottom, with the video margin.
        const boxRect = box.getBoundingClientRect();
        const frameW = F.w / scale;
        st.ox = Math.min(ref.left - boxRect.left, frameW - ext.right);
        if (st.ox + ext.left < 0) throw new Error('too-wide');
        if (F.h) {
          const frameH = F.h / scale;
          st.oy = Math.min(frameH - pad - ref.height, frameH - ext.bottom);
          if (st.oy + ext.top < 0) throw new Error('too-tall');
          st.H = F.h;
        } else {
          st.oy = -ext.top;
          st.H = Math.ceil(((ext.bottom - ext.top) * scale) / 2) * 2;   // even: video tools prefer it
        }
        return st;
      } finally {
        box.remove();
      }
    }

    function roundedPath(ctx, x, y, w, h, rt, rb) {
      ctx.beginPath();
      ctx.moveTo(x + rt, y);
      ctx.arcTo(x + w, y, x + w, y + h, rt);
      ctx.arcTo(x + w, y + h, x, y + h, rb);
      ctx.arcTo(x, y + h, x, y, rb);
      ctx.arcTo(x, y, x + w, y, rt);
      ctx.closePath();
    }

    // One frame of the banner at the point p of the loop; k shrinks it (preview).
    function paintExport(ctx, st, p, k = 1) {
      const s = st.scale * k;
      ctx.setTransform(1, 0, 0, 1, 0, 0);
      ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
      if (exportOpts.bg === 'theme' && st.bgColor) { ctx.fillStyle = st.bgColor; ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); }
      ctx.setTransform(s, 0, 0, s, st.ox * s, st.oy * s);
      ctx.textBaseline = 'alphabetic';
      ctx.textAlign = 'left';
      // Shadows are in canvas pixels, whatever the transform: scaled by hand.
      const shade = (on) => {
        ctx.shadowColor = on ? 'rgba(0, 0, 0, .5)' : 'rgba(0, 0, 0, 0)';
        ctx.shadowBlur = on ? 12 * s : 0;
        ctx.shadowOffsetX = 0;
        ctx.shadowOffsetY = on ? 2 * s : 0;
      };
      const words = (list) => list.forEach((w) => {
        ctx.font = w.font;
        ctx.fillStyle = w.color;
        if ('letterSpacing' in ctx) ctx.letterSpacing = w.spacing;
        ctx.fillText(w.text, w.x, w.y);
      });
      shade(false);
      if (st.line) { ctx.fillStyle = st.line.color; ctx.fillRect(0, 0, st.line.w, st.line.h); }
      shade(exportOpts.shadow);
      st.images.forEach((i) => ctx.drawImage(i.img, i.x, i.y, i.w, i.h));
      words(st.texts);
      const fa = floatAt(p);
      if (st.hint) {
        const H = st.hint;
        ctx.save();
        ctx.translate(H.x + fa.hx, H.y + fa.hy);
        words(H.words);
        if (H.arrow) ctx.drawImage(H.arrow.img, H.arrow.x, H.arrow.y, H.arrow.w, H.arrow.h);
        ctx.restore();
      }
      shade(false);
      if (st.qr) {
        const Q = st.qr;
        const r = Q.radius;
        ctx.save();
        // Same transform as the CSS: rotate() then translateY(), around the bottom centre.
        ctx.translate(Q.x + Q.ox, Q.y + Q.oy);
        ctx.rotate((fa.rot * Math.PI) / 180);
        ctx.translate(0, fa.ty);
        ctx.translate(-Q.ox, -Q.oy);
        // Box shadows of the card (dark drop shadow, blue glow).
        ctx.fillStyle = '#fff';
        ctx.save();
        ctx.shadowColor = 'rgba(0, 0, 0, .7)'; ctx.shadowBlur = 44 * s; ctx.shadowOffsetY = 22 * s;
        roundedPath(ctx, 18, 18, Q.w - 36, Q.h - 36, Math.max(0, r - 18), Math.max(0, r - 18)); ctx.fill();
        ctx.shadowColor = 'rgba(74, 91, 255, .55)'; ctx.shadowBlur = 40 * s; ctx.shadowOffsetY = 0;
        roundedPath(ctx, 8, 8, Q.w - 16, Q.h - 16, Math.max(0, r - 8), Math.max(0, r - 8)); ctx.fill();
        ctx.restore();
        ctx.fillStyle = Q.bg;
        roundedPath(ctx, 0, 0, Q.w, Q.h, r, r); ctx.fill();
        ctx.strokeStyle = 'rgba(255, 255, 255, .5)'; ctx.lineWidth = 1;
        roundedPath(ctx, -0.5, -0.5, Q.w + 1, Q.h + 1, r + 0.5, r + 0.5); ctx.stroke();
        if (Q.code) ctx.drawImage(Q.code.img, Q.code.x, Q.code.y, Q.code.w, Q.code.h);
        if (Q.cap) {
          const C = Q.cap;
          const a = (135 * Math.PI) / 180;         // linear-gradient(135deg, ...)
          const dx = Math.sin(a);
          const dy = -Math.cos(a);
          const len = Math.abs(C.w * dx) + Math.abs(C.h * dy);
          const cx = C.x + C.w / 2;
          const cy = C.y + C.h / 2;
          const g = ctx.createLinearGradient(cx - (dx * len) / 2, cy - (dy * len) / 2, cx + (dx * len) / 2, cy + (dy * len) / 2);
          g.addColorStop(0, C.c1);
          g.addColorStop(1, C.c2);
          ctx.fillStyle = g;
          roundedPath(ctx, C.x, C.y, C.w, C.h, 0, C.radius); ctx.fill();
          words(C.words);
        }
        ctx.restore();
      }
    }

    /* ---- file writers ---- */
    const CRC_TABLE = (() => {
      const tb = new Uint32Array(256);
      for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; tb[n] = c >>> 0; }
      return tb;
    })();
    function crc32(bytes) {
      let c = 0xffffffff;
      for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
      return (c ^ 0xffffffff) >>> 0;
    }
    // ZIP archive, files stored as they are (PNG is already compressed).
    async function zipStore(entries) {
      const enc = new TextEncoder();
      const now = new Date();
      const time = (now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1);
      const date = ((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate();
      const parts = [];
      const central = [];
      let offset = 0;
      for (const [name, blob] of entries) {
        const crc = crc32(new Uint8Array(await blob.arrayBuffer()));
        const nm = enc.encode(name);
        const lh = new DataView(new ArrayBuffer(30));
        lh.setUint32(0, 0x04034b50, true); lh.setUint16(4, 20, true); lh.setUint16(6, 0x0800, true);
        lh.setUint16(8, 0, true); lh.setUint16(10, time, true); lh.setUint16(12, date, true);
        lh.setUint32(14, crc, true); lh.setUint32(18, blob.size, true); lh.setUint32(22, blob.size, true);
        lh.setUint16(26, nm.length, true); lh.setUint16(28, 0, true);
        parts.push(lh.buffer, nm, blob);
        const ch = new DataView(new ArrayBuffer(46));
        ch.setUint32(0, 0x02014b50, true); ch.setUint16(4, 20, true); ch.setUint16(6, 20, true); ch.setUint16(8, 0x0800, true);
        ch.setUint16(10, 0, true); ch.setUint16(12, time, true); ch.setUint16(14, date, true);
        ch.setUint32(16, crc, true); ch.setUint32(20, blob.size, true); ch.setUint32(24, blob.size, true);
        ch.setUint16(28, nm.length, true); ch.setUint32(42, offset, true);
        central.push(ch.buffer, nm);
        offset += 30 + nm.length + blob.size;
      }
      const cdSize = central.reduce((n, b) => n + b.byteLength, 0);
      const end = new DataView(new ArrayBuffer(22));
      end.setUint32(0, 0x06054b50, true); end.setUint16(8, entries.length, true); end.setUint16(10, entries.length, true);
      end.setUint32(12, cdSize, true); end.setUint32(16, offset, true);
      return new Blob([...parts, ...central, end.buffer], { type: 'application/zip' });
    }
    // QuickTime movie, one PNG per frame ("PNG" codec, 32 bits: with alpha).
    function movPng(frames, w, h, fps) {
      const enc = new TextEncoder();
      const cat = (arrs) => {
        const out = new Uint8Array(arrs.reduce((n, a) => n + a.length, 0));
        let at = 0;
        arrs.forEach((a) => { out.set(a, at); at += a.length; });
        return out;
      };
      const u8 = (...v) => new Uint8Array(v);
      const u16 = (v) => u8((v >>> 8) & 255, v & 255);
      const u32 = (v) => u8((v >>> 24) & 255, (v >>> 16) & 255, (v >>> 8) & 255, v & 255);
      const cc = (s) => enc.encode(s);
      const zeros = (n) => new Uint8Array(n);
      const atom = (type, ...body) => { const b = cat(body); return cat([u32(8 + b.length), cc(type), b]); };
      const fullAtom = (type, flags, ...body) => atom(type, u32(flags & 0xffffff), ...body);   // version 0
      const pascal = (s, size) => { const b = zeros(size); const x = enc.encode(s).slice(0, size - 1); b[0] = x.length; b.set(x, 1); return b; };
      const matrix = cat([u32(0x10000), u32(0), u32(0), u32(0), u32(0x10000), u32(0), u32(0), u32(0), u32(0x40000000)]);
      const n = frames.length;
      const sizes = frames.map((b) => b.size);
      const total = sizes.reduce((a, b) => a + b, 0);
      if (total + 8 > 0xffffffff) throw new Error('Vidéo trop lourde : choisissez moins d’images ou un cadre plus petit.');
      const ftyp = atom('ftyp', cc('qt  '), u32(0x20050300), cc('qt  '));
      let at = ftyp.length + 8;
      const offsets = sizes.map((sz) => { const o = at; at += sz; return o; });
      const movieDur = Math.round((n * 1000) / fps);
      const mvhd = fullAtom('mvhd', 0, u32(0), u32(0), u32(1000), u32(movieDur), u32(0x10000), u16(0x100), zeros(10), matrix,
        u32(0), u32(0), u32(0), u32(0), u32(0), u32(0), u32(2));
      const tkhd = fullAtom('tkhd', 3, u32(0), u32(0), u32(1), u32(0), u32(movieDur), zeros(8), u16(0), u16(0), u16(0), u16(0), matrix,
        u32(w * 65536), u32(h * 65536));
      const mdhd = fullAtom('mdhd', 0, u32(0), u32(0), u32(fps * 100), u32(n * 100), u16(0), u16(0));
      const hdlrM = fullAtom('hdlr', 0, cc('mhlr'), cc('vide'), u32(0), u32(0), u32(0), pascal('VideoHandler', 13));
      const vmhd = fullAtom('vmhd', 1, u16(0x40), u16(0x8000), u16(0x8000), u16(0x8000));
      const hdlrD = fullAtom('hdlr', 0, cc('dhlr'), cc('alis'), u32(0), u32(0), u32(0), pascal('DataHandler', 12));
      const dinf = atom('dinf', fullAtom('dref', 0, u32(1), fullAtom('alis', 1)));
      const stsd = fullAtom('stsd', 0, u32(1), atom('png ', zeros(6), u16(1), u16(0), u16(0), u32(0), u32(0), u32(0x400),
        u16(w), u16(h), u32(0x480000), u32(0x480000), u32(0), u16(1), pascal('PNG', 32), u16(32), u16(0xffff)));
      const stts = fullAtom('stts', 0, u32(1), u32(n), u32(100));
      const stsc = fullAtom('stsc', 0, u32(1), u32(1), u32(1), u32(1));
      const stsz = fullAtom('stsz', 0, u32(0), u32(n), ...sizes.map(u32));
      const stco = fullAtom('stco', 0, u32(n), ...offsets.map(u32));
      const moov = atom('moov', mvhd, atom('trak', tkhd, atom('mdia', mdhd, hdlrM,
        atom('minf', vmhd, hdlrD, dinf, atom('stbl', stsd, stts, stsc, stsz, stco)))));
      return new Blob([ftyp, u32(total + 8), cc('mdat'), ...frames, moov], { type: 'video/quicktime' });
    }
    function exportReadme(st, fps, n) {
      return [
        'Bandeau PCdlabs : séquence PNG transparente',
        `${n} images, ${fps} images par seconde, ${(n / fps).toFixed(1).replace('.', ',')} s (boucle sans raccord de 4,8 s)`,
        `Cadre : ${st.W} x ${st.H} pixels`,
        '',
        'Premiere Pro : Fichier > Importer, choisir bandeau_0001.png et cocher « Séquence d’images ».',
        `Si la cadence n’est pas de ${fps} images/s : clic droit sur le média > Modifier > Interpréter le métrage.`,
        'Poser le média sur une piste au-dessus de la vidéo : la transparence est conservée.',
        '',
      ].join('\r\n');
    }

    /* ---- the export box ---- */
    let exportBusy = false;
    let exportCancel = false;
    let exportPrev = null;                         // { cv, ctx, st, k, raf }
    let exportPrevTimer = 0;
    function exportPreviewSoon() {
      clearTimeout(exportPrevTimer);
      exportPrevTimer = setTimeout(async () => {
        const P = exportPrev;
        if (!P || !P.cv.isConnected || !P.cv.offsetParent) return;
        try {
          const st = await exportStage();
          P.st = st;
          P.k = Math.min(1, 520 / st.W);
          P.cv.width = Math.round(st.W * P.k);
          P.cv.height = Math.max(1, Math.round(st.H * P.k));
          P.cv.parentElement.classList.toggle('is-light', (exportOpts.theme === 'page' ? root.dataset.theme : exportOpts.theme) === 'light');
          P.note.textContent = `${st.W} × ${st.H} px` + (st.warn ? ' · ' + st.warn : '');
          if (!P.raf) P.raf = requestAnimationFrame(previewLoop);
        } catch (e) {
          P.note.textContent = e.message === 'too-tall' ? 'Le bandeau est plus haut que ce cadre : réduisez les tailles ou choisissez « Bandeau seul ».'
            : e.message === 'too-wide' ? 'Le bandeau est plus large que ce cadre : réduisez la taille des textes ou du QR code.' : 'Aperçu impossible.';
        }
      }, 250);
    }
    function previewLoop() {
      const P = exportPrev;
      if (!P || !P.st || !P.cv.isConnected || !P.cv.offsetParent) { if (P) P.raf = 0; return; }
      const moving = exportOpts.format !== 'png' && !reduceMotion.matches;
      paintExport(P.ctx, P.st, moving ? (performance.now() / 1000 / LOOP_S) % 1 : 0, P.k);
      P.raf = requestAnimationFrame(previewLoop);
    }

    function exportCtl() {
      const choice = (key, label, options, help) => {
        const name = uid();
        const seg = h('div', { class: 'pcdc-seg', role: 'radiogroup', 'aria-label': label });
        options.forEach(([v, text]) => {
          const inp = h('input', { type: 'radio', name, value: v });
          inp.checked = exportOpts[key] === v;
          inp.addEventListener('change', () => { if (!inp.checked) return; exportOpts[key] = v; saveExport(); exportPreviewSoon(); });
          seg.append(h('label', null, inp, h('span', null, text)));
        });
        return h('div', { class: 'pcdc-row is-wide is-choice' }, labelled(label, help), seg);
      };
      const size = h('input', { type: 'range', min: 60, max: 200, step: 5, value: exportOpts.text, 'aria-label': 'Taille des textes' });
      const sizeOut = h('output', null, exportOpts.text + ' %');
      size.addEventListener('input', () => { exportOpts.text = int(size.value, 60, 200, 120); sizeOut.textContent = exportOpts.text + ' %'; saveExport(); exportPreviewSoon(); });
      const shadow = h('input', { type: 'checkbox', class: 'pcdc-toggle' });
      shadow.checked = exportOpts.shadow;
      shadow.addEventListener('change', () => { exportOpts.shadow = shadow.checked; saveExport(); exportPreviewSoon(); });
      const cv = h('canvas', { class: 'pcdc-exp-canvas', width: 520, height: 80 });
      const note = h('p', { class: 'pcdc-help' });
      exportPrev = { cv, ctx: cv.getContext('2d'), st: null, k: 1, raf: 0, note };
      const go = h('button', { type: 'button', class: 'pcdc-btn is-primary' }, 'Exporter');
      const bar = h('div', { class: 'pcdc-progress', hidden: true }, h('i'));
      const state = h('p', { class: 'pcdc-help', 'aria-live': 'polite' });
      go.addEventListener('click', () => runExport({ go, bar, fill: bar.firstElementChild, state }));
      return [
        h('p', { class: 'pcdc-help' }, 'Le pied de page seul, sur fond transparent, à poser sur une vidéo. Le QR code flotte comme sur la page ; le cadre le contient toujours en entier, ombre comprise.'),
        choice('format', 'Format', [['mov', 'Vidéo MOV'], ['zip', 'Séquence PNG'], ['png', 'Image PNG']],
          'MOV : codec PNG avec transparence, à glisser dans Premiere Pro. Séquence PNG : lue par tous les logiciels. Image : le bandeau fixe'),
        choice('frame', 'Cadre', [['fhd', '1920 × 1080'], ['uhd', '4K'], ['vertical', '1080 × 1920'], ['strip', 'Bandeau seul']],
          'Plein cadre : le bandeau est déjà en bas, il suffit de le poser sur la piste au-dessus'),
        choice('fps', 'Images par seconde', [['25', '25'], ['30', '30'], ['50', '50']], 'La cadence de votre séquence Premiere'),
        choice('loops', 'Durée', [['1', '4,8 s'], ['2', '9,6 s'], ['3', '14,4 s']], 'Boucle sans raccord : dupliquez-la bout à bout pour une vidéo plus longue'),
        choice('theme', 'Couleurs', [['dark', 'Textes clairs'], ['light', 'Textes foncés'], ['page', 'Comme la page']]),
        choice('bg', 'Fond', [['none', 'Transparent'], ['theme', 'Couleur du thème']]),
        h('div', { class: 'pcdc-row is-wide' }, labelled('Taille des textes', 'En plus des tailles réglées pour la page'), h('div', { class: 'pcdc-range' }, size, sizeOut)),
        h('label', { class: 'pcdc-row' }, labelled('Ombre sous les textes', 'Lisibles sur une image claire comme sombre'), shadow),
        h('div', { class: 'pcdc-exp-prev' }, cv),
        note,
        h('div', { class: 'pcdc-dlg-actions is-start' }, go),
        bar,
        state,
      ];
    }

    async function runExport(ui) {
      if (exportBusy) { exportCancel = true; return; }
      exportBusy = true;
      exportCancel = false;
      ui.go.textContent = 'Annuler';
      ui.bar.hidden = false;
      ui.fill.style.width = '0%';
      ui.state.textContent = 'Préparation...';
      try {
        const st = await exportStage();
        const cv = document.createElement('canvas');
        cv.width = st.W;
        cv.height = st.H;
        const ctx = cv.getContext('2d');
        const base = `bandeau-${st.W}x${st.H}`;
        if (exportOpts.format === 'png') {
          paintExport(ctx, st, 0);
          const png = await toBlob(cv, 'image/png');
          download(base + '.png', png);
          ui.fill.style.width = '100%';
          ui.state.textContent = `Image PNG prête : ${st.W} × ${st.H} px.` + (st.warn ? ' ' + st.warn : '');
          return;
        }
        const fps = Number(exportOpts.fps);
        const n = Math.round(LOOP_S * Number(exportOpts.loops) * fps);
        const frames = [];
        for (let i = 0; i < n; i++) {
          if (exportCancel) { ui.state.textContent = 'Export annulé.'; return; }
          paintExport(ctx, st, i / (LOOP_S * fps));
          const b = await toBlob(cv, 'image/png');
          if (!b) throw new Error('image');
          frames.push(b);
          ui.fill.style.width = Math.round(((i + 1) / n) * 92) + '%';
          ui.state.textContent = `Image ${i + 1} sur ${n}...`;
        }
        ui.state.textContent = 'Assemblage du fichier...';
        const name = `${base}-${fps}ips`;
        const out = exportOpts.format === 'zip'
          ? await zipStore(frames.map((b, i) => [`bandeau_${String(i + 1).padStart(4, '0')}.png`, b])
            .concat([['LISEZMOI.txt', new Blob([exportReadme(st, fps, n)], { type: 'text/plain' })]]))
          : movPng(frames, st.W, st.H, fps);
        download(name + (exportOpts.format === 'zip' ? '.zip' : '.mov'), out);
        ui.fill.style.width = '100%';
        ui.state.textContent = `${exportOpts.format === 'zip' ? 'Séquence PNG' : 'Vidéo MOV'} prête : ${n} images, ${(out.size / 1048576).toFixed(1).replace('.', ',')} Mo.` + (st.warn ? ' ' + st.warn : '');
      } catch (e) {
        ui.state.textContent = e && e.message === 'too-tall' ? 'Le bandeau est plus haut que ce cadre : réduisez les tailles ou choisissez « Bandeau seul ».'
          : e && e.message === 'too-wide' ? 'Le bandeau est plus large que ce cadre : réduisez la taille des textes ou du QR code.'
            : 'Export impossible : ' + ((e && e.message) || e);
      } finally {
        exportBusy = false;
        ui.go.textContent = 'Exporter';
        setTimeout(() => { ui.bar.hidden = true; }, 1500);
      }
    }

    /* ---- Publication tab ---- */
    function fillPublishInfo() {
      const when = (iso) => { try { return new Date(iso).toLocaleString('fr-FR', { dateStyle: 'medium', timeStyle: 'short' }); } catch (e) { return iso; } };
      if (!apiUrl) adm.pubInfo.textContent = 'Aucun serveur configuré : la vitrine affiche le contenu intégré au bloc.';
      else if (pub.rev) adm.pubInfo.textContent = `Version n° ${pub.rev}, publiée le ${when(pub.updatedAt)}.`;
      else adm.pubInfo.textContent = serverState === 'online'
        ? 'Rien n’est encore publié : les visiteurs voient le contenu intégré au bloc.'
        : 'Serveur non joint : version en ligne inconnue.';
    }
    async function listVersions() {
      const box = adm.versions;
      box.hidden = false;
      box.textContent = '';
      if (!token) { box.append(h('p', { class: 'pcdc-help' }, 'Connexion au serveur nécessaire.')); return; }
      box.append(h('p', { class: 'pcdc-help' }, 'Chargement...'));
      const res = await api('versions');
      box.textContent = '';
      if (res.status === 401) { expireSession(); return; }
      const list = res.data && res.data.ok ? res.data.versions : null;
      if (!list || !list.length) { box.append(h('p', { class: 'pcdc-help' }, 'Aucune version précédente.')); return; }
      list.forEach((v) => {
        const load = h('button', { type: 'button', class: 'pcdc-btn is-small' }, 'Charger');
        load.addEventListener('click', async () => {
          const r = await api('version', { extra: { name: v.name } });
          if (!(r.data && r.data.ok && r.data.config)) { updateState('Version illisible', 'error'); return; }
          work = cleanConfig(r.data.config);
          workBase = pub.rev;
          changed();
          refreshAdmin();
          updateState(`Version n° ${v.rev} chargée dans le brouillon : publiez pour la remettre en ligne`, 'dirty');
        });
        const d = new Date(v.updatedAt);
        box.append(h('div', { class: 'pcdc-version' },
          h('span', null, `n° ${v.rev} `, h('small', null, `${isNaN(d) ? '' : d.toLocaleString('fr-FR', { dateStyle: 'short', timeStyle: 'short' })} · ${v.cards} cartes`)),
          load));
      });
    }
    function download(name, data) {
      const blob = data instanceof Blob ? data : new Blob([data], { type: 'application/json' });
      const a = h('a', { href: URL.createObjectURL(blob), download: name, hidden: true });
      admBox.append(a);
      a.click();
      setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 60000);
    }
    const exportText = () => JSON.stringify({ ...work, rev: pub.rev, updatedAt: pub.updatedAt }, null, 2);
    function exportJson() {
      download(`tourniquet-pcdlabs-${new Date().toISOString().slice(0, 10)}.json`, exportText());
      adm.ioState.textContent = 'Fichier JSON téléchargé.';
    }
    async function copyForBlock() {
      const text = `<script type="application/json" class="pcdc-data">\n${exportText().replace(/<\//g, '<\\/')}\n<\/script>`;
      try {
        await navigator.clipboard.writeText(text);
        adm.ioState.textContent = 'Copié : remplacez la balise <script class="pcdc-data"> du bloc Elementor par ce texte.';
      } catch (e) {
        download('bloc-pcdc-data.txt', text);
        adm.ioState.textContent = 'Presse-papiers indisponible : le texte a été téléchargé.';
      }
    }
    function importJson() {
      pickFile('application/json,.json', async (file) => {
        try {
          const data = cleanConfig(JSON.parse(await file.text()));
          work = data;
          workBase = pub.rev;
          changed({ view: true });
          refreshAdmin();
          adm.ioState.textContent = `« ${file.name} » importé : ${data.projects.length} cartes dans le brouillon.`;
        } catch (e) {
          adm.ioState.textContent = 'Fichier illisible : ce n’est pas un JSON de la vitrine.';
        }
      });
    }

    /* ---- Publish, discard, leave ---- */
    async function publish() {
      if (!token || !isDirty()) return;
      if (serverOutdated()) { refreshAdmin(); updateState('Publication refusée : showcase.php doit d’abord être mis à jour sur le serveur', 'error'); return; }
      adm.publishBtn.disabled = true;
      updateState('Publication...');
      const res = await api('publish', { method: 'POST', body: { config: work, baseRev: workBase, force: adm.force } });
      if (res.status === 200 && res.data && res.data.ok && res.data.config && !(res.data.config.schema >= 2 && res.data.config.layout)) {
        // An old showcase.php took the publication but kept only what it knows:
        // visitors now see that version, the full draft stays here.
        serverApi = 1;
        pub = cleanConfig(res.data.config);
        store.set(KEY.cache, JSON.stringify(pub));
        workBase = pub.rev;
        changed();
        refreshAdmin();
        updateState('Le serveur a enregistré une version incomplète (ancien showcase.php). Vos réglages sont gardés ici : mettez showcase.php à jour, puis publiez à nouveau.', 'error');
        return;
      }
      if (res.status === 200 && res.data && res.data.ok && res.data.config) {
        serverApi = Number(res.data.api) || serverApi;
        pub = cleanConfig(res.data.config);
        store.set(KEY.cache, JSON.stringify(pub));
        work = clone(pub);
        workBase = pub.rev;
        adm.force = false;
        store.del(KEY.draft);
        render(work);
        refreshAdmin();
        updateState(`Publié : version n° ${pub.rev}, visible par tous les visiteurs`, 'ok');
        return;
      }
      if (res.status === 401) { expireSession(); return; }
      if (res.status === 409) {
        adm.force = true;
        updateState(`Une version plus récente (n° ${res.data && res.data.rev}) a été publiée ailleurs. « Publier quand même » la remplacera.`, 'error');
        adm.publishBtn.disabled = false;
        adm.publishBtn.textContent = 'Publier quand même';
        return;
      }
      updateState(res.status === 0 ? 'Serveur injoignable : rien n’a été publié, le brouillon est gardé.' : `Publication refusée (erreur ${res.status}).`, 'error');
      adm.publishBtn.disabled = false;
    }
    function discard() {
      work = clone(pub);
      workBase = pub.rev;
      adm.force = false;
      store.del(KEY.draft);
      adm.editing = null;
      render(work);
      setView(work.settings.view, false);
      refreshAdmin();
      selectTab(adm.tabs.find((t) => t.getAttribute('aria-selected') === 'true').dataset.tab);
      updateState('Modifications annulées : retour à la version en ligne', 'ok');
    }
    function expireSession() {
      token = '';
      sess.del(KEY.token);
      showLock('Session expirée : saisissez à nouveau le code. Vos modifications sont gardées.');
    }
    function logout() {
      token = '';
      localMode = false;
      sess.del(KEY.token);
      sess.del(KEY.local);
      work = null;                                 // the draft stays saved for the next session
      dlg.close();
      render(pub);
      setView(pub.settings.view, false);
    }
  }
})();
</script>
</section>
				</div>
				</div>
				</article>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/applications/">Applications</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>BirdSignal-infos</title>
		<link>https://presentcomposedesign.fr/birdsignal-infos/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Tue, 15 Sep 2026 11:23:50 +0000</pubDate>
				<category><![CDATA[Applications]]></category>
		<category><![CDATA[Design graphique]]></category>
		<category><![CDATA[Design produit]]></category>
		<category><![CDATA[PCdlabs]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=36789</guid>

					<description><![CDATA[<p>Activer le son Étude de cas &#183; Application web sur mesure BirdSignal Une application web qui analyse le son pour identifier l&#8217;oiseau qui chante, et transforme chaque écoute en carte à collectionner géolocalisée. Imaginée, nommée, dessinée, développée, mise en ligne et hébergée en France. Idéation Naming Identité graphique UX / UI Développement 3D temps réel IA embarquée Hébergement Voir l&#8217;application Demander un rendez-vous Sans magasin d&#8217;applications Elle s&#8217;installe sur tous les téléphones, gratuitement BirdSignal est une application web. Elle s&#8217;ouvre dans un navigateur, puis s&#8217;installe d&#8217;un geste sur l&#8217;écran d&#8217;accueil : son icône, son écran de lancement, son fonctionnement hors ligne. Aucun magasin d&#8217;applications, aucun compte, aucun téléchargement payant. Une seule adresse, et le même produit sur Android, sur iPhone, sur ordinateur. Gratuit Android iPhone et iPad Windows et macOS Installée depuis le navigateur, elle se range sur le bureau comme un logiciel. Ouvrir et installer 6&#8239;522espèces 2langues 137tests auto 0octet audio envoyé Points clés Ce que j&#8217;ai réalisé Idéation, positionnement et naming de l&#8217;application Création du logo, du système d&#8217;icônes et de l&#8217;identité graphique complète Conception de l&#8217;expérience, des parcours et de l&#8217;interface, en deux langues Développement intégral, front et back, et intégration d&#8217;un modèle d&#8217;intelligence artificielle Rendu 3D temps réel des cartes, avec gyroscope sur mobile Mise en ligne, sécurisation, hébergement en France et documentation Le cœur de l&#8217;application Tout part d&#8217;un son L&#8217;application enregistre quelques secondes de chant, en construit le spectrogramme, et soumet cette image de fréquences à un modèle d&#8217;intelligence artificielle de 6522 espèces. Le tout dans le navigateur, en moins de temps qu&#8217;il n&#8217;en faut pour ranger son téléphone. Ce qui défile ci-dessous n&#8217;est pas une illustration : c&#8217;est l&#8217;analyse d&#8217;un vrai enregistrement, une rue de Toulouse à six heures du matin. Enregistrement réel 8,5 kHz4 kHz0,4 Le temps en abscisse, la fréquence en ordonnée, l&#8217;intensité en couleur. Chaque trait vertical est un oiseau ; la bande claire du bas est la ville qui se réveille. Activez le son pour entendre ce que vous voyez. 1. CapterMicro ou fichier importé, rééchantillonné à 48 kHz. 2. Voir le sonTransformée de Fourier, spectrogramme, découpage en segments de 3 secondes. 3. ReconnaîtreModèle BirdNET exécuté en WebAssembly, filtré par le lieu et la saison. Design produit Le son devient un objet Chaque analyse conservée devient une carte numérotée, avec sa photo, son spectrogramme, sa date et son lieu. Elle se retourne en 3D et s&#8217;incline au gyroscope, de sorte que le reflet suit le poignet comme sur une vraie carte. BirdSignal Corvidés Pie bavarde Pica pica chant &#183; Défense de territoire Rareté &#183; LC8 % Confiance91 % 13 sept. 2026, 07:4312,8 s &#183; localisé BirdSignal Détails EspècePie bavarde MoteurBirdNET Filtre local157 espèces Durée12,8 s Position43.6080, 1.4812 Spectrogramme ÉMISE PAR BirdSignalR_001presentcomposedesign.fr Or Les quatre rangs Le rang change la matière de la carte, et il se mérite. Passez la souris sur les poinçons : ce sont exactement ceux frappés au verso des cartes. Uc Non classéeAucune espèce reconnue Sd StandardArgent Or AuthentiqueCaptée sur place, localisée Dd DiamantIrisé, le plus rare Le point fort Votre collection raconte vos sorties Chaque carte retient l&#8217;endroit où le chant a été capté. La collection devient une carte de vos promenades, et surtout une carte des espèces : on voit où elles sont, et où elles ne sont plus. Merle noir08:14 · LC Mésange charbonnière09:02 · LC Pie bavarde09:47 · LC Tourterelle turque10:21 · LC Représentation de principe. La position n&#8217;est enregistrée qu&#8217;avec l&#8217;accord de l&#8217;utilisateur, et elle est arrondie avant tout partage. Identité graphique Un signe qui se lit à 16 pixels Un oiseau et une lettre dans la même forme. Une seule couleur pour le signe : une identité qui a besoin d&#8217;un dégradé pour exister ne survit pas à une impression en noir. Le favicon, lui, existe en deux fichiers, parce qu&#8217;un gris clair disparaît sur une barre d&#8217;onglets blanche. Signe principal#1001ef sur blanc En réserveBlanc sur aplat de marque Icône d&#8217;applicationZone sûre du masque Android Thème clair Thème sombreFaviconDeux fichiers, un par thème d&#8217;onglet Logo de pied de pagePlaque blanche, signe bleu 48 32 16LisibilitéValidé à sa plus petite taille Méthode De l&#8217;idée à la mise en ligne 01 Idéation et positionnement Définir ce que l&#8217;application fait, ce qu&#8217;elle refuse de faire, et à qui elle s&#8217;adresse. 02 Naming Un nom qui tient dans une icône, se prononce en français comme en anglais, et dit quelque chose de vrai sur le produit. 03 Logo et identité graphique Du croquis au vectoriel, décliné en favicon, icônes d&#8217;application, thème clair et thème sombre. 04 Architecture et UX Parcours, écrans, arborescence, et une architecture choisie pour tenir sur un hébergement mutualisé français. 05 Développement Interface, API, traitement du signal audio, IA embarquée, rendu 3D temps réel et tests automatisés. 06 Mise en ligne et suivi Sécurisation, hébergement en France, application installable et hors ligne, documentation et mises à jour. Ce que cela veut dire pour vous Un seul interlocuteur, du premier croquis à la mise en ligne Application web sur mesure Outil métier, configurateur, application mobile installable. Conçue, développée et hébergée. Identité et design produit Naming, logo, système d&#8217;icônes, interface et déclinaisons, pensés pour tenir à toutes les tailles. 3D, AR et expériences immersives Configurateurs 3D temps réel, visualiseurs, réalité augmentée, directement dans le navigateur. Vous souhaitez faire appel à mes services ? Parlons de votre projet. J&#8217;interviens sur l&#8217;ensemble de la chaîne, ou sur la partie qui vous manque. Demander un rendez-vous Voir les autres réalisations BirdSignal est une application gratuite et non commerciale, conçue et hébergée en France par Present Composé Design, nom commercial de l&#8217;entreprise individuelle Alban DESBARAX, à Toulouse. Les hypothèses d&#8217;espèce produites sont probabilistes et ne constituent pas une détermination scientifique.</p>
<p>Cet article <a href="https://presentcomposedesign.fr/birdsignal-infos/">BirdSignal-infos</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="36789" class="elementor elementor-36789">
				<div class="elementor-element elementor-element-ba2361d 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="ba2361d" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-3260263 elementor-widget__width-inherit elementor-widget elementor-widget-html" data-id="3260263" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!--
// ║  Birdsignal, © All rights reserved                                       ║
// ║  2026 · PCdlabs ·  Present Composé Design                                ║
// ║  Alban DESBARAX                                                          ║
// ║  360° Design & Creative Technologist - Toulouse, FRANCE                  ║
// ║                                                                          ║
// ║  · Expertise & Support 2D, 3D, AR, VR, XR, AI                            ║
// ║  · Product Design & Industrial Design                                    ║
// ║  · Digital communication                                                 ║
// ║  · Immersive experiences  -ᯅ- ✔️                                        ║
//                                                                            
//  · 🡺 https://presentcomposedesign.fr/      
//  · 🡺 https://pcdlabs.presentcomposedesign.fr/tools/birdsignal 

// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@..........::::::::::::::::::::::::::::::@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.....::::::::::::::::::::::::::::::---@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@:.:::@@@@@@@@@@@@@@:..::::::::::::::::::::::::::::::-----@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.:::::::::::::::::.: @@@@@@@@@@::::::::::::::::::::::::::::::-------@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@:::::::::::::::::::::::::::::::@@@@@@@@::::::::::::::::::::::::::::--------
// @@@@@@@@@@@@@@@@@@@@@@@@@@@:.:::::::::::::::::::::::::::::::::..@@@@@@@::::::::::::::::::::::::::--------
// @@@@@@@@@@@@@@@@@@@@@@@:..::::::::::::::::::::::::::::::::::::::..@@@@@@@::::::::::::::::::::::::--------
// @@@@@@@@@@@@@@@@@@@@@@.::::::::::::::::::::::::::::::::::::::::::::@@@@@@::::::::::::::::::::::::--------
// @@@@@@@@@@@@@@@@@@@ .:::::::::::::::::::::::::::::@@@@@:::::::::::::@@@@@@:::::::::::::::::::::::--------
// @@@@@@@@@@@@@@@@@@:::::::::::::::::::::::::::::::.@@@@@.::::::::::::::@@@@@::::::::::::::::::::::--------
// @@@@@@@@@@@@@@@@::::::::::::::::::::::::::::::::::@@@@@.:::::::::::::::.:@@@@@:::::::::::::::::::--------
// @@@@@@@@@@@@@@::::::::::::::::::::::::::::::::::::::::::::::::::::::::@@@@@@@@@@@@@@:::::::::::::--------
// @@@@@@@@@@@@@@.:::::::::::::::::::::::::::::::::::::::::::::::::::@@@@@@@@@@@@@@@@@@@@@@:::::::::--------
// @@@@@@@@@@@@-:::::::::::::::::::::::::::::::::::::::::::::::::@@@@@@@@@@@@@@@@@@@@@-:::::::::::::--------
// @@@@@@@@@@@ .:::::::::::::::::::::::::::::::::::::::::::::::@@@@@@@@@@@@@@@@@@:::::::::::::::::::--------
// @@@@@@@@@@@:::::::: ::::::::::::::::::::::::::::::::::::::-@@@@@@@@@@@@@@::::::::::::::::::::::::--------
// @@@@@@@@@@:::::::::.@@.::::::::::::::::::::::::::::::::: @@@@@@@@@@@:::::::::::::::::::::::::::::--------
// @@@@@@@@@..:::::::::@@@.:::::::::::::::::::::::::::::::@@@@@@@@@@:.::::::::::::::::::::::::::::::--------
// @@@@@@@@@.:::::::::::@@@@@@::::::::::::::::::::::::::::@@@@@@@@....::::::::::::::::::::::::::::::--------
// @@@@@@@@@:::::::::::.:@@@@@@@.::::::::::::::::::::::..@@@@@@@......::::::::::::::::::::::::::::::--------
// @@@@@@@@ :::::::::::::.@@@@@@@@@@@::::::::::::::::::.@@@@@@@.......::::::::::::::::::::::::::::::--------
// @@@@@@@@::::::::::::::::@@@@@@@@@@@@@..::::::::::::::@@@@@@........::::::::::::::::::::::::::::::--------
// @@@@@@@@::::::::::::::::.@@@@@@@@@@@@@@@@-::::::::::@@@@@@@........::::::::::::::::::::::::::::::--------
// @@@@@@@@:::::::::::::::::::@@@@@@@@@@@@@@@@@@@@:.:::@@@@@@@........::::::::::::::::::::::::::::::--------
// @@@@@@@@::::::::::::::::::::: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@........::::::::::::::::::::::::::::::--------
// @@@@@@@@::::::::::::::::::::::::.@@@@@@@@@@@@@@@@@@@@@@@@@@........::::::::::::::::::::::::::::::--------
// @@@@@@@@::::::::::::::::::::::::::..@@@@@@@@@@@@@@@@@@@@@@@........::::::::::::::::::::::::::::::--------
// @@@@@@@@::::::::::::::::::::::::::::::.:@@@@@@@@@@@@@@@@@@@@:......::::::::::::::::::::::::::::::--------
// @@@@@@@@:::::::::::::::::::::::::::::::::::::@@@@@@@@@@@@@@@@@@@:..::::::::::::::::::::::::::::::--------
// @@@@@@@@::::::::::::::::::::::::::::::::::::::::.@@@@@@@@@@@@@@@@@@::::::::::::::::::::::::::::::--------
// @@@@@@@@:::::::::::::::::::::::::::::::::::::::::::::.@@@@@@@@@@@@@@@::::::::::::::::::::::::::::--------
// @@@@@@@@:::::::::::::::::::::::::::::::::::::::::::::::.:@@@@@@@@@@@@@@::::::::::::::::::::::::::--------
// @@@@@@@@:::::::::::::::::::::::::::::::::::::::::::::::::::@@@@@@@@@@@@@@::::::::::::::::::::::::--------
// @@@@@@@@::::::::::::::::::::::::::::::::::::::::::::::::::::.@@@@@@@@@@@@@:::::::::::::::::::::::--------
// @@@@@@@@:::::::::::::::::::::::::::::::::::::::::::::::::::::.:@@@@@@@@@@@@::::::::::::::::::::::--------
// @@@@@@@@:::::::::::::::::::::::::::::::::::::::::::::::::::::::.@@@@@@@@@@@@:::::::::::::::::::::--------
// @@@@@@@@::::::::::::::::::::::::::::::::::::::::::::::::::::::::@@@@@@@@@@@@@::::::::::::::::::::--------
// @@@@@@@@:::::::::::::::::::::::::::::::::::::::::::::::::::::::::@@@@@@@@@@@@::::::::::::::::::::--------
// @@@@@@@@:::::::::::::::::::::::::::::::::::::::::::::::::::::::::@@@@@@@@@@@@@:::::::::::::::::::--------
// @@@@@@@@::::::::::::::::::::::::::::::::::::::::::::::::::::::::::@@@@@@@@@@@@:::::::::::::::::::--------
// @@@@@@@@:::::::::::::::::::::::::::::::::::::::::::::::::::::::::@@@@@@@@@@@@@:::::::::::::::::::--------
// @@@@@@@@:::::::::::::::::::::::::::::::::::::::::::::::::::::::::@@@@@@@@@@@@@:::::::::::::::::::--------
// @@@@@@@@::::::::::::::::::::::::::::::::::::::::::::::::::::::::@@@@@@@@@@@@@::::::::::::::::::::--------
// @@@@@@@@:::::::::::::::::::::::::::::::::::::::::::::::::::::::.@@@@@@@@@@@@:::::::::::::::::::::--------
// @@@@@@@@:::::::::::::::::::::::::::::::::::::::::::::::::::::::@@@@@@@@@@@@@:::::::::::::::::::::--------
// @@@@@@@@:::::::::::::::::::::::::::::::::::::::::::::::::::::@@@@@@@@@@@@@:::::::::::::::::::::::--------
// @@@@@@@@:::::::::::::::::::::::::::::::::::::::::::::::::.:@@@@@@@@@@@@@@::::::::::::::::::::::::--------
// @@@@@@@@::::::::::::::::::::::::::::::::::::::::::::::.:@@@@@@@@@@@@@@@@:::::::::::::::::::::::::--------
// @@@@@@@@:::::::::::::::::::::::::::::::::::::::::.@@@@@@@@@@@@@@@@@@@@:::::::::::::::::::::::::::--------
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@:::::::::::::::::::::::::::::-------@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@:::::::::::::::::::::::::::::::-----@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@....::::::::::::::::::::::::::::::---@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ .........::::::::::::::::::::::::::::::@@@@@@@@


// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#*++==------===**%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#++-::::::::::::::::::::::-=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+-::::::::::::::::::::::::::::::::=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@#+::::::::::::::::::::::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@%+:::::::::::::::::=++***+=-::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@#=::::::::::::::::+%@@@@@@@@@@%+-:::::::::::::::::::=#@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@#-::::::::::::::::#@@@@@@@@@@@@@@@@%-:::::::::::::::::::=#@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@%=::::::::::::::::*@@@@@@**==-=+##@@@@@*-:::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@=:::::::::::::::::%@@@@@+::::::::::-*@@@@#-::::::::::::::::::::=@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@#::::::::::::::::::%@@@@=::::::::::::::-%@@@#-:::::::::::::::::::::#@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@=::::::::::::::::::*@@@@=::::::::::::::::-%@@@*::::::::::::::::::::::=@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@%-::::::::::::::::::-@@@@=::::::::::::::::::-%@@@-::::::::::::::::::::::-%@@@@@@@@@@@@@@
// @@@@@@@@@@@@#::::::::::::::::::::*@@@#::::::::::::::::::::=@@@*::::::::::::::::::::::::*@@@@@@@@@@@@@
// @@@@@@@@@@@#:::::::::::::::::::::%@@@+::::::::::::::::::::-@@@%:::::::::::::::::::::::::*@@@@@@@@@@@@
// @@@@@@@@@@+::::::::::::::::::::::@@@@-:::::::::::::::::::::%@@@::::::::::::::::::::::::::*@@@@@@@@@@@
// @@@@@@@@@*:::::::::::::::::::::::@@@@=:::::::::::::::::::::@@@@:::::::::::::::::::::::::::*@@@@@@@@@@
// @@@@@@@@@%::::::::::::::::::::::::@@@@*:::::::::::::::::::::@@@%::::::::::::::::::::::::::::*@@@@@@@@
// @@@@@@@%:::::::::::::::::::::::::@@@@%::::::::::::::::::::+@@@*:::::::::::::::::::::::::::::%@@@@@@@@
// @@@@@@@::::::::::::::::::::::::::@@@@@*::::::::::::::::::-@@@@::::::::::::::::::::::::::::::-@@@@@@@@
// @@@@@@=::::::::::::::::::::::::::@@@@@@*::::::::::::::::-@@@@+:::::::::::::::::::::::::::::::*@@@@@@@
// @@@@@%:::::::::::::::::::::::::::@@@@@@@*::::::::::::::-%@@@#:::::::::::::::::::::::::::::::::*@@@@@@
// @@@@@::::::::::::::::::::::::::::@@@@@@@@@*::::::::::+@@@@@*::::::::::::::::::::::::::::::::::-@@@@@@
// @@@@*::::::::::::::::::::::::::::@@@#=@@@@@@%#*+=**%@@@@@@=::::::::::::::::::::::::::::::::::::*@@@@@
// @@@@:::::::::::::::::::::::::::::@@@#::*@@@@@@@@@@@@@@@@*:::::::::::::::::::::::::::::::::::::::@@@@@
// @@@*:::::::::::::::::::::::::::::@@@#::::=*@@@@@@@@@%#=:::::::::::::::::::::::::::::::::::::::::#@@@@
// @@@=:::::::::::::::::::::::::::::@@@#::::::::-==+=-:::::::::::::::::::::::::::::::::::::::::::::=@@@@
// @@@::::::::::::::::::::::::::::::@@@#:::::::::::::::::::::::::::::::::::::::::::::::::--:::::::::@@@@
// @@*::::::::::::::::::::::::::::::@@@#:::::::::+######*:::::::::::::::::::::::::::::::+@=:::::::::#@@@
// @@*::::::::::::::::::::::::::::::@@@#:::--::::===+*+==:::::=-:::::::=-::::::-::::::::+@=:::::::::+@@@
// @@:::::::::::::::::::::::::::::::@@@#::+@#@@::-#@@%@%=::=@@@@+:::+@@@@@+-::@@@@%*-::#@@%#:::::::::@@@
// @@:::::::::::::::::+*##%%#*=-::::@@@#::+@@=::-%@:::-*@=:%@-:=:::*@*:::-@*::@@-:=@%::=*@*=:::::::::@@@
// @@::::::::::::::+%@@@@@@@@@@@%*-:@@@#::+@*:::*@@@@@@@@#:+@%*-:::@@@@@@@@@-:@%:::#@:::+@=::::::::::@@@
// @%::::::::::::*@@@@@@@@@@@@@@@@@#+@@#::+@+:::+@=-------:::=%@*::@%-------::@%:::#@:::+@=::::::::::%@@
// @%::::::::::-@@@@@@++-::::-=#@@@@@+*#::+@+:::-@%=-:+%%-:-=::#@::*@*-::+%=::@%:::#@:::+@=::::::::::#@@
// @%:::::::::=@@@@%+::::::::::::#@@@@**::+@+::::-#@@@@%=::#@%%@+:::+@@@@@*:::@%:::#@:::+@=::::::::::%@@
// @%::::::::-@@@@+:::::::::::::::-@@@@+:::-::::::::--:::::::=-:::::::-=-:::::--:::-=----=-::::::::::%@@
// @@:::::::-@@@@*:::::::::::::::::-%%%*:::::::::::::::::::::::::::::::::::::::::::+%%%%%%#::::::::::@@@
// @@:::::::*@@@#:::::::+#@%*-::::::=+*#*=+#*=-:::::+#%%#-::::::=#%%#=::::-#@@#-::::=##%#+-::::::::::@@@
// @@-::::::@@@@-:::::-%@#+*%@*:::::%@#+#@@+*@*::::%@+==#@*::::%@#+=%@*-::%@-=*=:::*@*+=+@%-::::::::-@@@
// @@+:::::-@@@@::::::#@=::::#@=::::%@::=@*::#@-::*@=::::-@+::*@*::::*@*::*@*-::::-@@####%@#::::::::*@@@
// @@#:::::-@@@*::::::@@:::::=@*::::%@::=@*::#@-::%@::::::@*::%@-::::-@%:::=%@%-::*@#******+::::::::*@@@
// @@@-::::-@@@%::::::#@+::::%@-::::%@::=@*::#@-::@@-::::=@-::*@*::::*@*:::-::@%::=@*::::-=-::::::::@@@@
// @@@+:::::@@@@-::::::%@%*#@@+:::::%@::=@*::#@-::@@@#=+#@+::::*@%*+@@*:::#@*+@*:::*@%*+%@%::::::::-@@@@
// @@@#:::::#@@@+:::::::=*##+-::::::+*::-*=::+*:::@%=*%#*-::::::=*%#*-:::::=*#+:::::-*##*=--=::::::*@@@@
// @@@@-::::=@@@@-::::::::::::::::::=**+::::::::::@%:::::::::::::::::::::::-==++**##%%@@@@@@@-::::-@@@@@
// @@@@*:::::*@@@@-::::::::::::::::+@@@#::::::::::::::::::::-==++**##=-#@@@@@@@@@@@@@@@@@@@@@=::::*@@@@@
// @@@@@-:::::#@@@@+-:::::::::::::*@@@@::::::-==++--##%@@@@@@@@@#*#@@%%@@@@@@@@@@@#@@@@@*#@@@+:::-@@@@@@
// @@@@@%::::::*@@@@@+-:::::::::+@@@@%:=@@@@@@@@@@==@@@**%@@@@*-#@*-@@*@@@*-+**@@@:%@@==@++@@*:::%@@@@@@
// @@@@@@*::::::=@@@@@@@%%**##@@@@@@=::=@@@@#-:--=-=@@=+@+=@@@:%@@@=+@-*@*-@@@+:@@=#@:*@@%-@@#::+@@@@@@@
// @@@@@@@-:::::::+%@@@@@@@@@@@@@@#-::::@@@+-%@@@*--@@:@@=*@@@%=*%@@@@*=@-*@@@*:+@=+=#@@@%:@@%:-@@@@@@@@
// @@@@@@@@-::::::::=*%@@@@@@@##-:::::::@@@:#@@@@@*:@@:%==@@@@@@%+-%@@#-@*+@@@:+:@*:*@@@@@:%@@-@@@@@@@@@
// @@@@@@@@#-:::::::::::::-:::::::::::::@@%:%@@@@@*-@@-#@@@#-@*%@@@-#@@:@@#=+:%@-#@:@@@@@@*=@@%@@@@@@@@@
// @@@@@@@@@*:::::::::::::::::::::::::::#@@--@@@@%:*@@%*+*=*@@=+@@@=+@@-#@@@@@@@=#@#@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@*-:::::::::::::::::::::::::#@@@+-==-:#@@@@@@@@@@@@#++*#@@@@@@@#@@@%:@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@*-::::::::::::::::::::::::+@@@@@%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@-=-:+**++==--::%@@@@@@@@@@@@
// @@@@@@@@@@@@#-:::::::::::::::::::::::=@@@@@@@@@@@@@@@@@@@@@%%##**++=---::::::::::::::::%@@@@@@@@@@@@@
// @@@@@@@@@@@@@@-::::::::::::::::::::::-@@@@@@%###**++=--::::::::::::::::::::::::::::::-%@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@+::::::::::::::::::::::--::::::::::::::::::::::::::::::::::::::::::::+@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@%-::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::-%@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@+-:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::+@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@%=::::::::::::::::::::::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@%=::::::::::::::::::::::::::::::::::::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@%=-:::::::::::::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@*-:::::::::::::::::::::::::::::::::::::::::::::#@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@%+-:::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%*=-:::::::::::::::::::::::::::::::+#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%**=-:::::::::::::::::::::=+#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%**++=====+++*##@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
-->

<!--
  ============================================================================
  BIRDSIGNAL · ETUDE DE CAS
  Bloc HTML personnalise Elementor, pour https://presentcomposedesign.fr/birdsignal-infos

  A COLLER TEL QUEL dans un widget "HTML" d'Elementor. Le bloc est autonome :
  styles et scripts vivent dedans et sont tous prefixes "bsm-", de sorte qu'ils
  ne peuvent ni deborder sur le theme ni en recevoir.

  RIEN N'EST A REMPLIR : le bloc est complet. Tout est dessine ici meme, en
  SVG et en canvas (logo et declinaisons, carte qui tourne, poincons de rang,
  carte des releves), sauf deux choses qui vivent sur le serveur et qui
  DOIVENT y rester :

    1. le visualiseur 3D enginR, appele en fond du bloc « Elle s'installe
       partout » ;
    2. l'enregistrement sonore
       /tools/birdsignal/Birds-in-town_6am_Toulouse_12b-r-l.plana.mp4
       que le bouton de son joue, et dont le spectrogramme de cette page a ete
       calcule.

  Si l'enregistrement disparaissait du serveur, le bouton resterait sans
  effet, mais la page, elle, continuerait de fonctionner : le spectrogramme
  est une image embarquee, il ne depend de rien.
  ============================================================================
-->

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">

<style>
/* ===========================================================================
   Tout est enferme sous #bsm. Aucune regle ne vise un selecteur global : un
   bloc insere dans un theme ne doit jamais pouvoir repeindre ce qui l'entoure.
   =========================================================================== */
#bsm {
  --bsm-blue: #1001ef;
  --bsm-soft: #eeecff;
  --bsm-ink: #0d1020;
  --bsm-mid: #5d6479;
  --bsm-line: #e4e6f0;
  --bsm-wash: #f7f8fc;
  --bsm-night: #10131f;

  box-sizing: border-box;
  margin: 0; padding: 0;
  font-family: "Inter Tight", "Helvetica Neue", Arial, system-ui, sans-serif;
  color: var(--bsm-ink); background: #fff;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
#bsm *, #bsm *::before, #bsm *::after { box-sizing: border-box; }
#bsm p, #bsm h1, #bsm h2, #bsm h3, #bsm ul, #bsm li, #bsm figure { margin: 0; }
#bsm ul { padding: 0; list-style: none; }
#bsm img, #bsm svg { max-width: 100%; display: block; }
/* [hidden] vient de la feuille du navigateur : la regle ci-dessus, qui est
   une regle d'auteur, la battait, et l'image source du spectrogramme
   s'affichait en clair au-dessus de la section. */
#bsm [hidden] { display: none; }
#bsm a { color: inherit; text-decoration: none; }

#bsm .bsm-wrap { width: 100%; max-width: 1060px; margin: 0 auto; padding: 0 20px; }
#bsm .bsm-sec { padding: 76px 0; position: relative; overflow: hidden; }
#bsm .bsm-sec--wash { background: var(--bsm-wash); }
#bsm .bsm-sec--night { background: var(--bsm-night); color: #eef0f8; }

/* --------------------------------------------------------------- typographie */
#bsm .bsm-kicker {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 11.5px; font-weight: 700; letter-spacing: .16em; text-transform: uppercase;
  color: var(--bsm-blue); margin-bottom: 14px;
}
#bsm .bsm-kicker::before { content: ""; width: 24px; height: 2px; background: currentColor; }
/* Sur fond sombre, le bleu de marque tombe a un contraste illisible : il passe
   a sa version claire. Le trait suit, il est en currentColor. */
#bsm .bsm-sec--night .bsm-kicker { color: #8f97ff; }
#bsm h1.bsm-h1 { font-size: clamp(2.6rem, 7vw, 4.6rem); font-weight: 800; line-height: .98; letter-spacing: -.04em; }
#bsm h2.bsm-h2 { font-size: clamp(1.55rem, 3.4vw, 2.3rem); font-weight: 800; line-height: 1.1; letter-spacing: -.03em; margin-bottom: 14px; }
#bsm .bsm-sec--night h2.bsm-h2 { color: #fff; }
#bsm h3.bsm-h3 { font-size: 1.05rem; font-weight: 700; letter-spacing: -.015em; margin-bottom: 6px; }
#bsm .bsm-lede { font-size: clamp(1.05rem, 2vw, 1.25rem); color: var(--bsm-mid); max-width: 58ch; }
#bsm .bsm-p { color: var(--bsm-mid); font-size: 15px; max-width: 62ch; }
#bsm .bsm-sec--night .bsm-p, #bsm .bsm-sec--night .bsm-lede { color: #aeb5cb; }
#bsm .bsm-p strong, #bsm .bsm-lede strong { color: var(--bsm-ink); font-weight: 700; }
#bsm .bsm-sec--night .bsm-p strong { color: #fff; }

/* ---------------------------------------------------------------------- hero */
#bsm .bsm-hero {
  position: relative; overflow: hidden; padding: 92px 0 68px;
  background:
    radial-gradient(1000px 480px at 78% -10%, rgba(16,1,239,.14), transparent 60%),
    radial-gradient(700px 380px at 4% 108%, rgba(122,92,255,.12), transparent 58%),
    #fff;
}
#bsm .bsm-hero-grid { display: grid; gap: 40px; align-items: center; grid-template-columns: minmax(0,1.2fr) minmax(0,.8fr); }
#bsm .bsm-mark { width: min(280px, 72%); margin-inline: auto; }
/* Le signe est pose par <use> : c'est le <svg> qui porte la couleur, car
   `fill` s'herite jusque dans l'arbre d'ombre, contrairement aux selecteurs
   CSS et a `opacity`. L'entree se joue donc sur le conteneur (.bsm-zoom). */
#bsm .bsm-mark svg { width: 100%; fill: var(--bsm-blue); filter: drop-shadow(0 24px 44px rgba(16,1,239,.22)); }

#bsm .bsm-tags { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 22px; }
#bsm .bsm-tag {
  font-size: 12px; font-weight: 600; padding: 6px 12px; border-radius: 999px;
  border: 1px solid var(--bsm-line); background: #fff; color: var(--bsm-mid);
}
#bsm .bsm-tag--on { background: var(--bsm-blue); border-color: var(--bsm-blue); color: #fff; }

#bsm .bsm-eq { display: flex; align-items: flex-end; gap: 3px; height: 46px; margin-top: 28px; opacity: .45; }
#bsm .bsm-eq i {
  flex: 1 1 0; min-width: 2px; border-radius: 3px 3px 0 0;
  background: linear-gradient(180deg, var(--bsm-blue), rgba(16,1,239,.22));
  transform-origin: bottom; animation: bsm-bar 1.6s ease-in-out infinite alternate;
}
@keyframes bsm-bar { from { transform: scaleY(.16); } to { transform: scaleY(1); } }

/* ------------------------------------------------------------------- boutons */
#bsm .bsm-cta { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 26px; }
#bsm .bsm-btn {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 13px 22px; border-radius: 13px; font-weight: 700; font-size: 14.5px;
  border: 1px solid var(--bsm-blue); background: var(--bsm-blue); color: #fff;
  transition: transform .16s ease, box-shadow .16s ease, background .16s ease;
}
#bsm .bsm-btn:hover { transform: translateY(-2px); box-shadow: 0 14px 28px rgba(16,1,239,.3); }
#bsm .bsm-btn--ghost { background: transparent; color: var(--bsm-blue); }
#bsm .bsm-btn--ghost:hover { background: var(--bsm-soft); box-shadow: none; }
#bsm .bsm-sec--night .bsm-btn--ghost { color: #bfc5ff; border-color: rgba(191,197,255,.45); }
#bsm .bsm-sec--night .bsm-btn--ghost:hover { background: rgba(143,151,255,.14); }

/* --------------------------------------------------------------- points cles */
#bsm .bsm-keys { display: grid; gap: 10px; margin-top: 8px; }
#bsm .bsm-keys li {
  display: flex; gap: 12px; align-items: flex-start;
  font-size: 15.5px; font-weight: 600; color: var(--bsm-ink);
  padding: 13px 16px; border: 1px solid var(--bsm-line); border-radius: 13px; background: #fff;
  transition: transform .2s ease, border-color .2s ease, box-shadow .2s ease;
}
#bsm .bsm-keys li:hover { transform: translateX(4px); border-color: rgba(16,1,239,.3); box-shadow: 0 10px 26px rgba(13,16,32,.07); }
#bsm .bsm-keys li::before {
  content: ""; flex: 0 0 auto; width: 8px; height: 8px; border-radius: 50%;
  background: var(--bsm-blue); margin-top: 8px;
}

/* ------------------------------------------------------------------- chiffres */
#bsm .bsm-stats { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 14px; margin-top: 30px; }
#bsm .bsm-stat { text-align: center; padding: 20px 10px; border-radius: 16px; background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.12); }
#bsm .bsm-stat b { display: block; font-size: clamp(1.6rem, 3.6vw, 2.3rem); font-weight: 800; letter-spacing: -.04em; color: #fff; }
#bsm .bsm-stat span { font-size: 11px; text-transform: uppercase; letter-spacing: .1em; color: #8f97ff; font-weight: 700; }

/* --------------------------------------------------------------------- etapes */
#bsm .bsm-steps { display: grid; gap: 14px; margin-top: 30px; grid-template-columns: repeat(2, minmax(0,1fr)); }
#bsm .bsm-step {
  border: 1px solid var(--bsm-line); border-radius: 16px; padding: 22px; background: #fff;
  transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease;
}
#bsm .bsm-step:hover { transform: translateY(-3px); border-color: rgba(16,1,239,.35); box-shadow: 0 16px 36px rgba(13,16,32,.08); }
#bsm .bsm-num {
  display: inline-grid; place-items: center; width: 30px; height: 30px; border-radius: 9px;
  background: var(--bsm-soft); color: var(--bsm-blue); font-weight: 800; font-size: 13px; margin-bottom: 12px;
}

/* -------------------------------------------------------------- emplacements */
#bsm .bsm-slot { margin-top: 26px; }
#bsm .bsm-ph {
  border: 2px dashed rgba(16,1,239,.28); border-radius: 16px; background: var(--bsm-soft);
  min-height: 200px; display: grid; place-items: center; text-align: center; padding: 24px;
  color: var(--bsm-blue); font-size: 13px; font-weight: 600;
}
#bsm .bsm-slot img { border-radius: 16px; border: 1px solid var(--bsm-line); width: 100%; }
#bsm .bsm-cap { font-size: 12.5px; color: var(--bsm-mid); margin-top: 9px; font-style: italic; }
#bsm .bsm-sec--night .bsm-cap { color: #8b92a8; }

/* --------------------------------------------------------------- visualiseur */

/* ---------------------------------------------------------------- services */
#bsm .bsm-serv { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 14px; margin-top: 30px; }
#bsm .bsm-serv .bsm-step { background: var(--bsm-wash); }

/* ---------------------------------------------------------------------- final */
#bsm .bsm-final { text-align: center; }
#bsm .bsm-final .bsm-lede { margin-inline: auto; }
#bsm .bsm-final .bsm-cta { justify-content: center; }
#bsm .bsm-legal { font-size: 12px; color: var(--bsm-mid); margin-top: 34px; max-width: 74ch; margin-inline: auto; }

/* ===========================================================================
   SPECTROGRAMME ANIME
   Le dessin est en canvas et non en CSS : un spectrogramme est une image de
   donnees, pas une decoration. Des barres CSS auraient l'air d'un egaliseur
   de chaine hi-fi, ce qui raconterait exactement le contraire de ce que fait
   l'application.
   =========================================================================== */
#bsm .bsm-spectro {
  position: relative; border-radius: 18px; overflow: hidden; margin-top: 26px;
  border: 1px solid rgba(255,255,255,.12); background: #070a16;
  aspect-ratio: 1000 / 260;
}
#bsm .bsm-spectro canvas { position: absolute; inset: 0; display: block; width: 100%; height: 100%; }
/* Image de repli, posee SOUS le canvas : elle montre tout l'extrait d'un coup.
   Sans script, c'est elle qu'on voit, et le propos reste entier. */
#bsm .bsm-spectro-fond { position: absolute; inset: 0; width: 100%; height: 100%; }
#bsm .bsm-spectro-legend {
  position: absolute; left: 14px; top: 12px; display: flex; gap: 8px; align-items: center;
  font-size: 11px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: #8f97ff;
  pointer-events: none;
}
#bsm .bsm-spectro-legend i {
  width: 7px; height: 7px; border-radius: 50%; background: #7ef0ff;
  animation: bsm-pulse 1.8s ease-out infinite;
}
@keyframes bsm-pulse {
  0% { box-shadow: 0 0 0 0 rgba(126,240,255,.55); }
  70% { box-shadow: 0 0 0 10px rgba(126,240,255,0); }
  100% { box-shadow: 0 0 0 0 rgba(126,240,255,0); }
}
#bsm .bsm-spectro-axis {
  position: absolute; right: 12px; top: 12px; bottom: 12px;
  display: flex; flex-direction: column; justify-content: space-between;
  font-size: 10px; font-weight: 600; color: rgba(174,181,203,.6); text-align: right; pointer-events: none;
  /* La graduation du bas tombe sur la bande claire de la ville : sans cette
     ombre portee, elle s'y dissout. */
  text-shadow: 0 1px 4px rgba(7,10,22,.95), 0 0 2px rgba(7,10,22,.95);
}
#bsm .bsm-chain { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 12px; margin-top: 18px; }
#bsm .bsm-chain div {
  border: 1px solid rgba(255,255,255,.12); border-radius: 14px; padding: 16px;
  background: rgba(255,255,255,.04);
}
#bsm .bsm-chain b { display: block; font-size: 13px; color: #fff; font-weight: 700; margin-bottom: 4px; }
#bsm .bsm-chain span { font-size: 12.5px; color: #aeb5cb; }

/* ===========================================================================
   POINCONS DE RANG
   Reproduits a l'identique de ceux imprimes au verso des cartes : meme encre,
   meme substrat metallise sombre, meme anneau de 3,5 px et meme filet
   interieur. Les valeurs sont copiees depuis le code de l'application
   (GRADE_INK et drawGradeLabel), pas approchees a l'oeil.
   =========================================================================== */
#bsm .bsm-badges {
  display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 16px; margin-top: 30px;
  perspective: 900px;
}
#bsm .bsm-badge-cell { text-align: center; }
#bsm .bsm-badge {
  --ink: linear-gradient(#c4cde4, #c4cde4);
  --ring-a: 1;
  --hair-a: .55;
  --disc: linear-gradient(135deg, rgba(26,31,58,.95), rgba(8,10,22,.95));
  position: relative; width: 104px; height: 104px; margin: 0 auto 12px;
  border-radius: 50%; background-image: var(--disc);
  transform-style: preserve-3d;
  transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transition: transform .2s ease-out;
  box-shadow: 0 14px 30px rgba(8,10,22,.3);
}
#bsm .bsm-badge i { position: absolute; border-radius: 50%; background-image: var(--ink); pointer-events: none; }
/* L'anneau et le filet sont des disques ajoures par un masque : c'est le seul
   moyen de leur donner une encre en degrade conique, qu'aucune couleur de
   bordure ne sait porter. */
#bsm .bsm-badge .bsm-ring {
  inset: 2px; opacity: var(--ring-a);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3.5px), #000 calc(100% - 3.5px));
          mask: radial-gradient(farthest-side, transparent calc(100% - 3.5px), #000 calc(100% - 3.5px));
}
#bsm .bsm-badge .bsm-hair {
  inset: 13px; opacity: var(--hair-a);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 1px), #000 calc(100% - 1px));
          mask: radial-gradient(farthest-side, transparent calc(100% - 1px), #000 calc(100% - 1px));
}
/* Reflet qui suit le pointeur : c'est lui qui donne la sensation du metal,
   bien plus que la couleur. */
#bsm .bsm-badge .bsm-gloss {
  inset: 0; background-image: none;
  background: radial-gradient(58px 58px at var(--gx, 50%) var(--gy, 28%), rgba(255,255,255,.32), transparent 62%);
  mix-blend-mode: screen;
}
/* La lettre est peinte avec l'encre du rang, donc avec un degrade decoupe sur
   le texte. Si le navigateur ne sait pas faire, `-webkit-text-fill-color:
   transparent` s'appliquerait quand meme et la lettre disparaitrait : d'ou la
   couleur pleine par defaut, et le degrade seulement sous condition. */
#bsm .bsm-badge b {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-size: 31px; font-weight: 800; letter-spacing: -.02em; line-height: 1;
  color: var(--plat, #c4cde4);
}
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  #bsm .bsm-badge b {
    background-image: var(--ink); -webkit-background-clip: text; background-clip: text;
    color: transparent; -webkit-text-fill-color: transparent;
  }
}
#bsm .bsm-badge--uc {
  --ink: linear-gradient(rgba(150,155,170,.75), rgba(150,155,170,.75));
  --plat: #8a8fa0;
  --ring-a: .7; --hair-a: .4;
  /* Rang Uc : aucun substrat metallise, exactement comme sur la carte, ou la
     mention doit se voir sans briller. */
  --disc: linear-gradient(#f1f2f7, #e6e8f1);
  box-shadow: 0 10px 22px rgba(13,16,32,.12);
}
#bsm .bsm-badge--uc .bsm-gloss { background: none; }
#bsm .bsm-badge--sd { --ink: linear-gradient(rgba(196,205,228,.9), rgba(196,205,228,.9)); --plat: #c4cde4; }
#bsm .bsm-badge--or { --ink: linear-gradient(#ffc95e, #ffc95e); --plat: #ffc95e; }
#bsm .bsm-badge--dd {
  --plat: #9be8ff;
  --ink: conic-gradient(from calc(-34deg + var(--sweep, 0deg)), #7ef0ff, #8f9dff, #e6a8ff, #ffd79a, #9dffdd, #7ef0ff);
}
#bsm .bsm-badge-name { display: block; font-size: 14.5px; font-weight: 700; }
#bsm .bsm-badge-cell span { display: block; font-size: 12.5px; color: var(--bsm-mid); margin-top: 2px; }

/* ===========================================================================
   CARTE QUI TOURNE
   Deux faces opaques dos a dos, en rotation continue. Chaque face porte
   `backface-visibility: hidden`, sans quoi on lirait le texte du verso par
   transparence a chaque demi-tour.
   =========================================================================== */
#bsm .bsm-stage { perspective: 1500px; display: grid; place-items: center; padding: 14px 0 4px; }
#bsm .bsm-card3d {
  position: relative; width: min(290px, 74vw); aspect-ratio: 63 / 88;
  transform-style: preserve-3d; animation: bsm-spin 18s linear infinite;
}
#bsm .bsm-stage:hover .bsm-card3d { animation-play-state: paused; }
@keyframes bsm-spin { from { transform: rotateY(0deg); } to { transform: rotateY(360deg); } }
#bsm .bsm-face {
  position: absolute; inset: 0; border-radius: 18px; overflow: hidden;
  -webkit-backface-visibility: hidden; backface-visibility: hidden;
  box-shadow: 0 26px 60px rgba(8,10,22,.45), inset 0 0 0 1px rgba(255,255,255,.1);
  padding: 12px; display: flex; flex-direction: column; gap: 8px;
  background: linear-gradient(158deg, #232b4f, #161c33 64%, #1d2440);
  color: #f5f6ff;
}
#bsm .bsm-face--back { transform: rotateY(180deg); }
#bsm .bsm-cardtop { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
#bsm .bsm-cardbrand { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 800; letter-spacing: -.01em; }
#bsm .bsm-cardbrand svg { width: 15px; height: 15px; }
#bsm .bsm-cardbrand em { font-style: normal; color: #8b96ff; }
#bsm .bsm-pill {
  font-size: 9px; font-weight: 700; padding: 3px 8px; border-radius: 999px;
  border: 1px solid rgba(245,246,255,.28); color: rgba(245,246,255,.86);
}
#bsm .bsm-cardphoto {
  border-radius: 12px; aspect-ratio: 4 / 3; position: relative; overflow: hidden;
  background: linear-gradient(150deg, #3c4f8a, #22305c 58%, #4a3b74);
  display: grid; place-items: center;
}
#bsm .bsm-cardphoto svg { width: 54%; opacity: .3; fill: #fff; }
#bsm .bsm-cardname { font-size: 17px; font-weight: 800; letter-spacing: -.02em; line-height: 1.1; }
#bsm .bsm-cardsci { font-size: 11.5px; font-style: italic; color: rgba(245,246,255,.62); }
#bsm .bsm-cardmeta { font-size: 9.5px; color: rgba(245,246,255,.62); line-height: 1.35; }
#bsm .bsm-gauge { margin-top: auto; }
#bsm .bsm-gauge-row { display: flex; justify-content: space-between; align-items: baseline; font-size: 8.5px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: rgba(245,246,255,.62); }
#bsm .bsm-gauge-row b { font-size: 12px; letter-spacing: 0; color: #f5f6ff; }
#bsm .bsm-gauge-track { height: 5px; border-radius: 999px; background: rgba(255,255,255,.14); margin: 3px 0 7px; overflow: hidden; }
#bsm .bsm-gauge-fill { height: 100%; border-radius: 999px; }
#bsm .bsm-gauge-fill--rar { background: linear-gradient(90deg, #4ade80, #86efac); width: 8%; }
#bsm .bsm-gauge-fill--conf { background: linear-gradient(90deg, #8b96ff, #c4b5fd); width: 91%; }
#bsm .bsm-cardfoot { display: flex; align-items: flex-end; justify-content: space-between; gap: 8px; }
/* Le scelle holographique du recto : meme film irise que le poincon Dd. */
#bsm .bsm-seal {
  width: 44px; height: 44px; border-radius: 50%; flex: 0 0 auto; position: relative;
  background-image: conic-gradient(from -34deg, #7ef0ff, #8f9dff, #e6a8ff, #ffd79a, #9dffdd, #7ef0ff);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px));
          mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px));
  animation: bsm-sealspin 9s linear infinite;
}
@keyframes bsm-sealspin { to { filter: hue-rotate(360deg); } }
#bsm .bsm-cardspectro { border-radius: 10px; overflow: hidden; height: 62px; background: #0a0f24; }
#bsm .bsm-cardspectro canvas { width: 100%; height: 100%; display: block; }
#bsm .bsm-cardrows { display: grid; gap: 5px; font-size: 9px; }
#bsm .bsm-cardrows div { display: flex; justify-content: space-between; gap: 8px; }
#bsm .bsm-cardrows span { color: rgba(245,246,255,.55); letter-spacing: .06em; text-transform: uppercase; font-weight: 700; }
#bsm .bsm-cardrows b { font-weight: 700; text-align: right; }
#bsm .bsm-cardback-mark { display: grid; place-items: center; padding: 6px 0; }
#bsm .bsm-cardback-mark svg { width: 44%; fill: url(#bsm-gold); }

/* ===========================================================================
   CARTE DES RELEVES
   Fond abstrait volontaire : ce n'est pas une carte geographique reelle, et
   en dessiner une fausse laisserait croire a des donnees qui n'existent pas.
   Ce qui est vrai, et qui est le propos, c'est le lien entre une prise de son
   et l'endroit ou elle a ete faite.
   =========================================================================== */
#bsm .bsm-map {
  position: relative; border-radius: 20px; overflow: hidden; margin-top: 26px;
  background: linear-gradient(150deg, #101a3a, #0a0f24);
  border: 1px solid rgba(255,255,255,.12); aspect-ratio: 16 / 9;
}
#bsm .bsm-map svg { position: absolute; inset: 0; width: 100%; height: 100%; }
#bsm .bsm-trail { fill: none; stroke: #8f97ff; stroke-width: 1.6; stroke-dasharray: 5 7; stroke-linecap: round; opacity: .75; }
#bsm.bsm-js .bsm-trail { stroke-dashoffset: 900; }
#bsm.bsm-js .bsm-in .bsm-trail { animation: bsm-draw 3.4s cubic-bezier(.35,.1,.25,1) forwards; }
@keyframes bsm-draw { to { stroke-dashoffset: 0; } }
/* Transition et non animation, comme partout ailleurs dans ce bloc : un seul
   mecanisme d'apparition, donc un seul endroit ou se tromper. Une animation
   `forwards` qui ne demarrerait pas laisserait l'epingle invisible pour
   toujours, alors qu'une transition non jouee laisse simplement l'etat final. */
#bsm .bsm-pin {
  transform-origin: center;
  transition: opacity .5s ease, transform .5s cubic-bezier(.2,1.5,.45,1);
  transition-delay: calc(var(--i, 0) * 520ms);
}
#bsm.bsm-js .bsm-pin { opacity: 0; transform: translateY(9px) scale(.55); }
#bsm.bsm-js .bsm-in .bsm-pin { opacity: 1; transform: none; }
#bsm .bsm-pin text { font-family: inherit; font-size: 11px; font-weight: 700; fill: #eef0f8; }
#bsm .bsm-pin tspan { font-size: 9px; font-weight: 600; fill: rgba(174,181,203,.85); }

/* ===========================================================================
   DECLINAISONS DU LOGO
   =========================================================================== */
#bsm .bsm-logos { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 14px; margin-top: 28px; }
#bsm .bsm-logo {
  border: 1px solid var(--bsm-line); border-radius: 16px; background: #fff; overflow: hidden;
  transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease;
}
#bsm .bsm-logo:hover { transform: translateY(-4px); border-color: rgba(16,1,239,.3); box-shadow: 0 16px 36px rgba(13,16,32,.09); }
#bsm .bsm-logo-art { aspect-ratio: 1; display: grid; place-items: center; padding: 22px; }
#bsm .bsm-logo-art svg { width: 100%; fill: var(--bsm-blue); }
#bsm .bsm-logo-art--blue { background: var(--bsm-blue); }
#bsm .bsm-logo-art--blue svg { fill: #fff; }
#bsm .bsm-logo-art--tile { padding: 0; }
#bsm .bsm-tile {
  width: 100%; height: 100%; display: grid; place-items: center;
  background: var(--bsm-blue); border-radius: 22%;
}
#bsm .bsm-tile svg { width: 72%; fill: #fff; }
#bsm .bsm-logo-lab { font-size: 11.5px; font-weight: 700; padding: 11px 14px; border-top: 1px solid var(--bsm-line); }
#bsm .bsm-logo-lab span { display: block; font-weight: 500; color: var(--bsm-mid); font-size: 11px; }
#bsm .bsm-favrow { display: flex; align-items: flex-end; gap: 16px; justify-content: center; }
#bsm .bsm-favrow figure { text-align: center; }
#bsm .bsm-favrow svg { fill: var(--bsm-blue); }
#bsm .bsm-favrow figcaption { font-size: 9px; color: var(--bsm-mid); margin-top: 6px; font-weight: 600; }

/* ===========================================================================
   APPARITIONS
   L'etat cache n'existe QUE si le script a pu poser .bsm-js. Sans cette
   condition, un script bloque laisserait la page entierement vide, sans
   message et sans erreur. L'animation est un supplement, jamais la condition
   d'affichage.
   Le decalage vient d'une variable --i posee dans le HTML, et non d'une serie
   de :nth-child : une liste de six elements reclamerait six regles, et la
   septieme serait oubliee.
   =========================================================================== */
#bsm .bsm-rise, #bsm .bsm-left, #bsm .bsm-zoom {
  transition: opacity .65s ease, transform .65s cubic-bezier(.2,.75,.25,1);
  transition-delay: calc(var(--i, 0) * 70ms);
}
#bsm.bsm-js .bsm-rise { opacity: 0; transform: translateY(24px); }
#bsm.bsm-js .bsm-left { opacity: 0; transform: translateX(-26px); }
#bsm.bsm-js .bsm-zoom { opacity: 0; transform: scale(.93); }
#bsm.bsm-js .bsm-in.bsm-rise, #bsm.bsm-js .bsm-in .bsm-rise,
#bsm.bsm-js .bsm-in.bsm-left, #bsm.bsm-js .bsm-in .bsm-left,
#bsm.bsm-js .bsm-in.bsm-zoom, #bsm.bsm-js .bsm-in .bsm-zoom { opacity: 1; transform: none; }

/* Sans pointeur, rien ne peut suivre la souris : le poincon respire de
   lui-meme, sinon les quatre rangs seraient quatre pastilles inertes. */
@media (hover: none) {
  #bsm .bsm-badge { animation: bsm-idle 6s ease-in-out infinite alternate; }
  #bsm .bsm-badge-cell:nth-child(2) .bsm-badge { animation-delay: -1.5s; }
  #bsm .bsm-badge-cell:nth-child(3) .bsm-badge { animation-delay: -3s; }
  #bsm .bsm-badge-cell:nth-child(4) .bsm-badge { animation-delay: -4.5s; }
  @keyframes bsm-idle {
    from { transform: rotateY(-11deg) rotateX(5deg); }
    to   { transform: rotateY(11deg) rotateX(-5deg); }
  }
}

/* ===========================================================================
   LE BLOC QUI PORTE LE VISUALISEUR EN FOND
   ---------------------------------------------------------------------------
   Le visualiseur n'est pas pose DANS une boite : il EST le fond de la section,
   et le texte se lit par-dessus. Trois consequences, toutes voulues :

   1. Le cadre du visualiseur est blanc, pas transparent. La section est donc
      blanche elle aussi : un bloc sombre aurait laisse une plaque blanche au
      milieu de la page. On epouse ce qu'on ne controle pas.
   2. Le texte est dans un calque « pointer-events: none ». Le lecteur fait
      donc tourner le telephone a travers le texte, ce qui est exactement ce
      qu'on lui demande de faire. Seuls les liens reprennent la souris.
   3. Un voile blanc, degrade, protege la colonne de texte. Sans lui, la
      lisibilite dependrait de l'angle du modele 3D, c'est-a-dire du hasard.
   =========================================================================== */
#bsm .bsm-hold {
  position: relative; overflow: hidden; background: #fff; padding: 0;
  display: flex; align-items: flex-start;
  min-height: clamp(640px, 92vh, 940px);
}
#bsm .bsm-hold-bg {
  position: absolute; inset: 0; width: 100%; height: 100%;
  border: 0; background: transparent; display: block;
}
#bsm .bsm-hold-fg {
  position: relative; z-index: 2; width: 100%; padding: 64px 0 40px;
  pointer-events: none;
  background: linear-gradient(180deg, #fff 0%, rgba(255,255,255,.95) 40%, rgba(255,255,255,0) 72%);
}
#bsm .bsm-hold-fg a, #bsm .bsm-hold-fg button { pointer-events: auto; }
#bsm .bsm-hold-col { max-width: 33rem; }
#bsm .bsm-hold .bsm-stats { margin-top: 26px; }
#bsm .bsm-hold .bsm-stat {
  background: rgba(255,255,255,.86); border-color: var(--bsm-line);
  backdrop-filter: blur(6px);
}
#bsm .bsm-hold .bsm-stat b { color: var(--bsm-ink); }
#bsm .bsm-hold .bsm-stat span { color: var(--bsm-blue); }
@media (min-width: 901px) {
  /* Le modele est centre dans son cadre : on decale le cadre pour qu'il se
     pose a droite du texte plutot que dessus. */
  #bsm .bsm-hold-bg { left: 26%; width: 74%; }
  #bsm .bsm-hold { align-items: center; }
  #bsm .bsm-hold-fg {
    padding: 48px 0;
    background: linear-gradient(100deg, #fff 0%, rgba(255,255,255,.96) 38%, rgba(255,255,255,0) 66%);
  }
}

/* -------------------------------------------------- systemes et gratuite */
#bsm .bsm-os { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 22px; }
#bsm .bsm-os li {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12.5px; font-weight: 600; color: var(--bsm-ink);
  border: 1px solid var(--bsm-line); background: rgba(255,255,255,.92);
  border-radius: 999px; padding: 7px 13px;
}
#bsm .bsm-os svg { width: 15px; height: 15px; flex: 0 0 auto; fill: none; stroke: var(--bsm-blue); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
#bsm .bsm-os .bsm-os--free { background: var(--bsm-blue); border-color: var(--bsm-blue); color: #fff; letter-spacing: .06em; text-transform: uppercase; font-weight: 800; font-size: 11.5px; }
#bsm .bsm-os .bsm-os--free svg { stroke: #fff; }

/* ------------------------------------------- capture du bureau Windows */
#bsm .bsm-shot { margin-top: 24px; max-width: 300px; }
#bsm .bsm-shot img {
  display: block; width: 100%; height: auto; border-radius: 16px;
  border: 1px solid var(--bsm-line); box-shadow: 0 22px 46px rgba(13,16,32,.2);
}
#bsm .bsm-shot figcaption { font-size: 12px; color: var(--bsm-mid); margin-top: 9px; font-style: italic; max-width: 34ch; }

/* ===========================================================================
   COMMANDE DE SON
   ---------------------------------------------------------------------------
   Aucun navigateur ne laisse une page demarrer un son sans geste du lecteur :
   la bande tourne donc muette, et ce bouton l'allume. Son intitule dit l'etat
   SUIVANT, jamais l'etat courant, parce que c'est ce qu'on clique.

   Il est cache tant que le script n'a pas pris la main : un bouton de son qui
   ne coupe rien vaut moins que pas de bouton du tout.
   =========================================================================== */
#bsm .bsm-sound { display: none; }
#bsm.bsm-js .bsm-sound {
  position: fixed; right: 18px; bottom: 18px; z-index: 40;
  display: inline-flex; align-items: center; gap: 9px;
  padding: 10px 16px 10px 12px; border-radius: 999px;
  border: 1px solid rgba(13,16,32,.12); background: rgba(255,255,255,.93);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  box-shadow: 0 14px 34px rgba(13,16,32,.18);
  font-family: inherit; font-size: 12.5px; font-weight: 700; color: var(--bsm-ink);
  cursor: pointer; opacity: 0; transform: translateY(14px); pointer-events: none;
  transition: opacity .35s ease, transform .35s ease, box-shadow .2s ease;
}
#bsm.bsm-js.bsm-audible .bsm-sound { opacity: 1; transform: none; pointer-events: auto; }
#bsm.bsm-js .bsm-sound:hover { box-shadow: 0 18px 40px rgba(16,1,239,.24); }
#bsm .bsm-sound svg { width: 17px; height: 17px; fill: none; stroke: var(--bsm-blue); stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
#bsm .bsm-sound .bsm-sound-on { display: none; }
#bsm.bsm-son .bsm-sound .bsm-sound-on { display: block; }
#bsm.bsm-son .bsm-sound .bsm-sound-off { display: none; }
/* Un fil d'onde qui ne bat que lorsque le son sort vraiment. */
#bsm .bsm-sound i {
  width: 6px; height: 6px; border-radius: 50%; background: var(--bsm-line); flex: 0 0 auto;
}
#bsm.bsm-son .bsm-sound i { background: #22c55e; animation: bsm-pulse 1.8s ease-out infinite; }
#bsm .bsm-audio-src { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }

/* ------------------------------------------------ vignettes du logo */
#bsm .bsm-logo-art--pair { display: flex; align-items: center; gap: 12px; padding: 16px; }
#bsm .bsm-fav { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 8px; }
#bsm .bsm-fav-plate { display: grid; place-items: center; width: 100%; aspect-ratio: 1; border-radius: 15px; padding: 11px; }
#bsm .bsm-fav-plate--light { background: #fff; border: 1px solid var(--bsm-line); }
#bsm .bsm-fav-plate--dark { background: var(--bsm-night); }
#bsm .bsm-fav-plate svg { width: 100%; }
#bsm .bsm-fav figcaption { font-size: 9.5px; font-weight: 700; color: var(--bsm-mid); }
#bsm .bsm-logo-art--footer { background: #eceff7; padding: 24px; }
#bsm .bsm-logo-art--footer svg { width: 100%; filter: drop-shadow(0 10px 22px rgba(13,16,32,.14)); }

/* ------------------------------------------------------------------ responsive */
@media (max-width: 900px) {
  /*
   * Sur telephone, le visualiseur ne peut plus etre le fond de TOUTE la
   * section : le modele est centre dans son cadre, et le cadre fait alors la
   * hauteur du texte, si bien que le telephone se serait retrouve derriere
   * les vignettes, invisible. Il prend donc une bande qui lui est propre, en
   * pied de bloc, et le texte cesse d'avoir besoin d'un voile.
   */
  /* `#bsm .bsm-sec { padding: 58px 0 }` vit plus bas dans CE MEME bloc et a
     la meme specificite : elle gagnait, et le pied de 600 pixels reserve au
     visualiseur disparaissait. D'ou le selecteur compose, qui tranche. */
  #bsm .bsm-sec.bsm-hold { min-height: 0; padding: 0 0 600px; align-items: flex-start; }
  #bsm .bsm-hold-bg { top: auto; bottom: 0; height: 600px; }
  #bsm .bsm-hold-fg { padding: 52px 0 26px; background: none; }
  #bsm .bsm-hold-col { max-width: none; }
  #bsm .bsm-hero-grid { grid-template-columns: 1fr; }
  #bsm .bsm-mark { width: min(200px, 56%); margin-inline: 0; }
  #bsm .bsm-serv, #bsm .bsm-stats, #bsm .bsm-logos, #bsm .bsm-chain { grid-template-columns: repeat(2, minmax(0,1fr)); }
  #bsm .bsm-sec { padding: 58px 0; }
  #bsm .bsm-hero { padding: 64px 0 50px; }
}
@media (max-width: 620px) {
  #bsm .bsm-steps, #bsm .bsm-serv, #bsm .bsm-chain { grid-template-columns: 1fr; }
  #bsm .bsm-logos { grid-template-columns: repeat(2, minmax(0,1fr)); }
  #bsm .bsm-hold { min-height: clamp(560px, 86vh, 760px); }
  #bsm .bsm-hold-fg { padding-top: 44px; }
  #bsm .bsm-shot { max-width: 230px; }
  #bsm.bsm-js .bsm-sound { right: 12px; bottom: 12px; padding: 9px 13px 9px 10px; font-size: 12px; }
  #bsm .bsm-badges { grid-template-columns: repeat(2, minmax(0,1fr)); }
  #bsm .bsm-badge { width: 88px; height: 88px; }
  #bsm .bsm-badge b { font-size: 26px; }
  #bsm .bsm-badge .bsm-hair { inset: 11px; }
  #bsm .bsm-map { aspect-ratio: 4 / 3; }
  #bsm .bsm-spectro { aspect-ratio: 5 / 3; }
}

@media (prefers-reduced-motion: reduce) {
  #bsm .bsm-rise, #bsm .bsm-left, #bsm .bsm-zoom,
  #bsm.bsm-js .bsm-rise, #bsm.bsm-js .bsm-left, #bsm.bsm-js .bsm-zoom {
    opacity: 1; transform: none; transition: none;
  }
  #bsm .bsm-eq i { animation: none; transform: scaleY(.5); }
  #bsm .bsm-card3d { animation: none; }
  #bsm .bsm-seal { animation: none; }
  #bsm .bsm-spectro-legend i { animation: none; }
  #bsm.bsm-js .bsm-trail, #bsm.bsm-js .bsm-in .bsm-trail { stroke-dashoffset: 0; animation: none; }
  #bsm.bsm-js .bsm-pin, #bsm.bsm-js .bsm-in .bsm-pin { opacity: 1; transform: none; transition: none; }
  #bsm .bsm-btn:hover, #bsm .bsm-step:hover, #bsm .bsm-logo:hover, #bsm .bsm-keys li:hover { transform: none; }
  #bsm .bsm-badge { transition: none; animation: none; }
  #bsm.bsm-son .bsm-sound i { animation: none; }
}
</style>

<div id="bsm">

  <!--
    La bande sonore de la page : l'enregistrement dont le spectrogramme
    ci-dessous a ete calcule. preload="none" est delibere : rien n'est
    telecharge tant que personne n'a demande a entendre.
  -->
  <audio class="bsm-audio-src" data-bsm-audio preload="none" aria-hidden="true"
         src="https://pcdlabs.presentcomposedesign.fr/tools/birdsignal/Birds-in-town_6am_Toulouse_12b-r-l.plana.mp4"></audio>

  <button class="bsm-sound" type="button" data-bsm-sound aria-pressed="false">
    <i aria-hidden="true"></i>
    <svg class="bsm-sound-off" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9.5v5h3.6L12 18.5v-13L7.6 9.5H4z"/><path d="M16.5 9.5l4.5 5M21 9.5l-4.5 5"/></svg>
    <svg class="bsm-sound-on" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9.5v5h3.6L12 18.5v-13L7.6 9.5H4z"/><path d="M16 9a4.4 4.4 0 010 6M18.9 6.6a8.2 8.2 0 010 10.8"/></svg>
    <span data-bsm-sound-lab>Activer le son</span>
  </button>

  <!-- Le signe est defini UNE fois et reutilise par <use> : il apparait douze
       fois dans la page, et douze copies de deux longs traces seraient douze
       occasions de les voir diverger. La couleur vient du CSS par heritage de
       `fill`, puisque les traces n'en portent aucune. -->
  <svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
    <defs>
      <symbol id="bsm-mark" viewBox="0 0 1024 1024">
        <path d="M880.58,0h-332.01c93.06,27.65,165.69,99.56,193.31,188.87l92.64,33.12c23,8.23,43.99,18.77,62.66,34.42-59.95,1.18-110.83,22.83-166.15,38.43-80.13,22.63-183.32,95.69-149.54,225.33,21.75,10.15,42.13,21.43,62.34,34.82,79.85,52.92,122.25,137.15,115.8,228.44-6.06,85.83-52.86,162.2-131.59,208.4-22.87,13.41-47.25,24.32-72.81,32.17h325.36c79.2,0,143.39-62.09,143.41-138.68V138.68c-.02-76.59-64.22-138.68-143.41-138.68h-.01Z"/>
        <path d="M534.78,604.76l-124.11-59.62-86.94-42.16c-20.47-9.92-37.86-23.29-55.29-38.04-55.23-46.72-86.17-115.15-82.82-188.48,39.48,62.43,103.62,97.78,171.01,128.58l141.51,61.85c2.18-38.77,21.32-106.04,38.48-135.46,39.58-67.83,101.74-105.27,180.27-123.72-27.9-9.85-49.77-27.77-65.27-53.06-20.07-32.75-47.58-56.6-81.1-75.48-32.12-14.98-65.33-27.04-102.54-27.06h-.3c-213.16-.01-385.98,178.89-385.96,399.58l.04,482.54,371.43-.34c48.61-.04,93.59-15.44,128.19-46.41,58.92-52.73,69.44-139.02,31.46-207.36-18.21-32.81-45.44-56.94-78.06-75.37h-.01ZM511.94,144.09c20.23,0,36.63,16.32,36.63,36.45s-16.4,36.47-36.63,36.47-36.63-16.32-36.63-36.47,16.42-36.45,36.63-36.45Z"/>
      </symbol>
      <linearGradient id="bsm-gold" x1="0" y1="0" x2="1" y2="1">
        <stop offset="0" stop-color="#ffe7a8"/><stop offset=".5" stop-color="#ffc95e"/><stop offset="1" stop-color="#b3801f"/>
      </linearGradient>
    </defs>
  </svg>

  <!-- ============================== HERO ================================ -->
  <header class="bsm-hero" data-bsm-reveal>
    <div class="bsm-wrap">
      <div class="bsm-hero-grid">
        <div>
          <p class="bsm-kicker bsm-rise">Étude de cas &middot; Application web sur mesure</p>
          <h1 class="bsm-h1 bsm-rise" style="--i:1">BirdSignal</h1>
          <p class="bsm-lede bsm-rise" style="--i:2;margin-top:18px">
            Une application web qui <strong>analyse le son</strong> pour identifier
            l'oiseau qui chante, et transforme chaque écoute en carte à
            collectionner géolocalisée. Imaginée, nommée, dessinée, développée,
            mise en ligne et <strong>hébergée en France</strong>.
          </p>
          <div class="bsm-tags bsm-rise" style="--i:3">
            <span class="bsm-tag bsm-tag--on">Idéation</span>
            <span class="bsm-tag">Naming</span>
            <span class="bsm-tag">Identité graphique</span>
            <span class="bsm-tag">UX / UI</span>
            <span class="bsm-tag">Développement</span>
            <span class="bsm-tag">3D temps réel</span>
            <span class="bsm-tag">IA embarquée</span>
            <span class="bsm-tag">Hébergement</span>
          </div>
          <div class="bsm-cta bsm-rise" style="--i:4">
            <a class="bsm-btn" href="https://pcdlabs.presentcomposedesign.fr/tools/birdsignal/" target="_blank" rel="noopener">Voir l'application</a>
            <a class="bsm-btn bsm-btn--ghost" href="https://presentcomposedesign.fr/contact/">Demander un rendez-vous</a>
          </div>
        </div>

        <div class="bsm-mark bsm-zoom" aria-hidden="true">
          <svg viewBox="0 0 1024 1024" role="img" aria-label="Signe BirdSignal"><use href="#bsm-mark"/></svg>
        </div>
      </div>
      <div class="bsm-eq" data-bsm-eq aria-hidden="true"></div>
    </div>
  </header>

  <!-- ============== INSTALLABLE PARTOUT, VISUALISEUR EN FOND ============ -->
  <section class="bsm-sec bsm-hold" data-bsm-reveal>
    <!--
      Le visualiseur enginR, en fond de section. Il occupe toute la surface :
      le telephone que vous voyez derriere le texte est un vrai modele 3D,
      qu'on peut faire tourner. enginR est un autre de mes outils, developpe
      et heberge au meme endroit que BirdSignal.
    -->
    <iframe
      class="bsm-hold-bg"
      src="https://pcdlabs.presentcomposedesign.fr/tools/enginR/viewer.html?s=p6hfafvq8iai&amp;embed=1"
      title="BirdSignal en volume : maquette 3D manipulable"
      allow="xr-spatial-tracking; fullscreen; camera; gyroscope; accelerometer; magnetometer; autoplay"
      allowfullscreen
      loading="lazy"
      referrerpolicy="strict-origin-when-cross-origin"></iframe>

    <div class="bsm-hold-fg">
      <div class="bsm-wrap">
        <div class="bsm-hold-col">
          <p class="bsm-kicker bsm-rise">Sans magasin d'applications</p>
          <h2 class="bsm-h2 bsm-rise" style="--i:1">Elle s'installe sur tous les téléphones, gratuitement</h2>
          <p class="bsm-p bsm-rise" style="--i:2">
            BirdSignal est une application web. Elle s'ouvre dans un navigateur,
            puis s'installe d'un geste sur l'écran d'accueil : son icône, son
            écran de lancement, son fonctionnement hors ligne. <strong>Aucun
            magasin d'applications, aucun compte, aucun téléchargement payant.</strong>
            Une seule adresse, et le même produit sur Android, sur iPhone, sur
            ordinateur.
          </p>
          <ul class="bsm-os bsm-rise" style="--i:3">
            <li class="bsm-os--free">
              <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v16M4 12h16"/></svg>
              Gratuit
            </li>
            <li>
              <svg viewBox="0 0 24 24" aria-hidden="true"><rect x="6" y="2" width="12" height="20" rx="2.5"/><path d="M10.5 18.5h3"/></svg>
              Android
            </li>
            <li>
              <svg viewBox="0 0 24 24" aria-hidden="true"><rect x="6" y="2" width="12" height="20" rx="3"/><path d="M10 5h4"/></svg>
              iPhone et iPad
            </li>
            <li>
              <svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="12" rx="1.5"/><path d="M2 20h20"/></svg>
              Windows et macOS
            </li>
          </ul>

          <figure class="bsm-shot bsm-zoom" style="--i:4">
            <img loading="lazy"
              src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAQDAwQDAwQEBAQFBQQFBwsHBwYGBw4KCggLEA4RERAOEA8SFBoWEhMYEw8QFh8XGBsbHR0dERYgIh8cIhocHRz/2wBDAQUFBQcGBw0HBw0cEhASHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBz/wgARCAE9AUcDASIAAhEBAxEB/8QAHAAAAwEBAQEBAQAAAAAAAAAAAQIDAAQFBgcI/8QAGgEAAwEBAQEAAAAAAAAAAAAAAAECAwQFBv/aAAwDAQACEAMQAAAB/N7RP3fzxXaGpBhmVOdsAHXQIVdDNSgow0nEZmDUYlKrUERmn0jmKdVXDOXSO0yyxgaTjM0mZU8yaS2nhdBO5MlxEMEFCc9Jaag4aUp2tmi60GGpB31Kiy1BUaQElME5o7OTMWIQHRMaZlHtshniRZLxHsuh+gduTnVXTNqdrfMhXTd1w0MRSxdjYbptFSGLQBpJKjOpk1EFjMMsJ5DAtRMvNtghQFdZYB01nmGtq5PsO3FzIlJZ0FZLrm2177YahoF1WolbCm1pX1ZnVYqNIqJQwbCMpWK4bqCzMihRVYMVEhDCWoOHtsz09m4OScemE1OdZVrAbXsVLajzYaSXDaIEsS3RJjN5sElV3T5xcJxW6JyV0VgEOiUZmGLGGa1PFM3QBhDHM9FsvFytK05IR6IvblON7B1fZIVehnDaQKCpO3TOc5Xq7iSdTqOQdIRxJ2yWnMHmaQn1LOvOQSmyvpK4q2yMqFddNbPherG8OXkohyISpOtuc4vVaSrqpsC3Rw9568bvN+gXOd1KLNw9JUF6Zs5Zd3M9OROmK250vNa7nu6vkLIU6h9EgdUwrCKOXNeuYX5eSbAMhLo53tI4Giupt47NVwNxbq5uu8LMnqzj51f1P6HyOz8U+o+5+JeHRzff93B1flXzH7n8x1Y/kKdnP7mHNHpktoEqaPz3RVAkFNlaxMyy1zaX6Iw5+WqgtTj0yd8woj1QsGyuUK0hal09fH2a830P7f5Hv/H+yytvN7Pivrj+eel5T/W/nfvej5H1Hl9nJ5vpfjfL7Pl/Z+TDl7YLo5WEp6EvzdhXEt4zSuj28GVrY6X2sNyc4LqIKdblHqR3A4u0S6JqzYVfqPlPt8z9p/J/2T8/+b9H670v5r+16cPbf479s249z9PN4/oc3l+p5e/L+Yeb6PJ9p4PBLonXXww6oR0woEnWs2IuZ1dWCDQuODvE7ceBaPQ4iLoEtRLqcuqboK1Ai75qP2nyn2fLr+27b433vI/Pf1ndnN+C/d/W/mPt+B+sR+L+08n0ObzPT8xL8v5+nm+0+f4JVTTs5J2Sd+ZKCNkYiTnLLGmzZobZtezgfmXprGphcyqZiXVKiAK1oGUN2aNyF+z8D9V4t/tsrfI+9tsnvlfqvj+zh+O/Ufyf9Z9Xw+fy/U8vzvR/LuenN9r4PLMyvsQELXmO0aadJ50s3Sbcmszz5tVbmcuxWOhdd+B1l6VPPvON5MKUErN6LVDT+r/cv58/XPF6utZfjWa/a9+MV6sP1L4Pz/qejj9n7LwI+N6XueLx/FbZcHF3eZ9P5cZNJ9ivBptCNN5GGYi0Wb3o8HREwzbV8OAvZymY7ToSzyMrppx0WfQJEVWkzm9+Vnn1+t857/DrLr8k8XR19EGFNuHq2zR0p7HmHipBa8iGZ1EhUxkeKxIlKDlR6ZVjNM+qvJV016SMWZkYTFS1RkdxmDNZg5IdsoZ5sTQoXLEFKtZ9DxYjmUxi3OuszYTpbn6ICm5XK3YYhDnHQssZww16cSttuhQ6Dx2QWUicqalyGJZkdQzI6nHZpmnSkaz6HnTplecJ+e0jaSMs7JYOEpssXRM0JldGmqWmH57QCObVpxMu6NyjYGR1QGDpBi1SC2EHzzIbFzgGEXNHDWW5jblt5yFmBPSy5pCpQJsOjOkGJJBqJ8yzmkKJWg213wArv0Z1cS4ukrmjlS1aUDipZ1eZyjA1EspLi5kbJzqF5cDoKHRQOWQ5sGqoEhJka6VMjC/OOTNG7ljjThOptuGIJLB3OxDl8CwOLTKgMkGdlIYsoa+mo3EwNVUrFnBpEcVEuJaQ50JTdCjB9pnOVEdpy0ZajX1z5Ry6dWbMkzIVLMHFiSTSIqCXBUZlZRWoWY0axdJMznVglM2UFRMTqgAsMbdJJLtrjKbwuhIstVq9Ep7bTr//xAArEAACAQIFAwMEAwEAAAAAAAAAAQIQEQMEEiAxBSEwBjJBExUiNBQ1QCX/2gAIAQEAAQUCQrD2vfxsvXSabF0ajUai9bmuRrOzLbriEPYx7ktyiW0jmX8lzkt45bkOnaxa5ZRNe2xY0mkt4Ex+KW5dtiiN28VzUXRYsW2p23MdPh7UOsUTxLltlixY0mk0mg0lnTV4PdVDHSXhSG9mmmk7IuXL1uXpbw+5fNHSW1Doh1scCg5HA3ct4b1vuVFsntVeFRUj2HK/hvtvW+11dJ7XRbERSG7/AOTmr2yo6R5oqpVtS1zSWLGk0jVvJfYqslVEfDyKAy1zQaDSWLllIlG3jQ+zdWMlVC4oqpElHSo3EiwoHc7l3RjRYvYcb+PlPuqSGPZ8UVUJEYmk7I5NJpZajVGM4bjfxKj5HRj5p8UVEISsIchdhMuXLlhqw+9HROw1cfhiySrLsz5oqqsaXLEYkYJvL9Elmov03jj9N45P0/mYrHy88GTVGqxlYkh+G9ZD3oQiNMn0/Hz08r6ZwIKPSclFY/Q8pirFyOa6Xj5HO/zMFkjq+WWPleRo4HWLJLe6KjH4Y93Ejx0npsuo4+Bl8PLYQ+On4mfedxcJY2HGcum52GZwsYkZv9acRjV00fOLKLxD3R8D2Ma3IhzEg7rI4EOk9OTUlTFxY4OHnOqYmaMHJ42YMLo8zDw/pRzX6+JEkrCJKjpCDJLu1ufZnFWNFq2LCQqdDwvr9TzWD/Iy3TutY3TJZTPZfOxPUmb/AC6L0z6kLWTJGa/X90ZIsNEqxdhkt6pa2xoYi1LFqelf7U670N5lr6mBiZL1HmcEz+Z/m5zAgsPAYyRm/wBeJiKzdGWHX43cVtsarYsWNJ6V/tqZzpuWzxm/TeNgDw3hHT+tuMVJTiyRmv14843udGqMdIjFsjKntPdRosWpYW30xG3VNmayODnI5vKSyOL0jNOEmSM1+ujG5dGP20YiQtiZGVOBO5bwWNJ6Y/tNvXcFYmWhLQXupGb/AF0Y/uYzkl7KSo9jPaKRGQnSLLXHHZeiMplnnMx0/wBPy6TmIShibeu5hLDjfFxH2JGb/WhzjP8AJjYnpJcUdHXSPjs0JmoUhSuKVGOJaqZ0GX/X6pm4JZLI4nT8x9/yJ9+yJ9+yJmPUOHpxsxLGn0XIO7JHUMRYeUw+cR/k6Mfto6OsHBQls4pcUxSL3LjpYsdMzMcpnsH1D0nBxc36pyWLlhF0hO5kvtmAPr+RR99ybJ9dyts91Ceda/GDdyT7t0kKjo6I+Jbr0TLmpmoujsXQmhWLpGLg4cD6GLMWUcTVCallcdN4GJrnhPDw1TEfa3YYuZbvmNLX23OxY7lxMvS5qNTLjfbGz6xZ/wApfW1ZfR9xf1pTwMjh4OJCOFjZpZlpETEl3m7YYyI98R8Rnp8Ny+y5cRc1MRekUol7iL9m9MWzEld04izl0dEIlR+VLwoij5nK5qtRi5lSNXRIRLj/AAXL11OiELjFkkmOsVYdG7KjLFiXYl/hsadiERV3cnLu+ypbs+HVVSo+7lRCdm6K1ttvAiwkIS7TmXG7s5fLdF3Jc0ifAx0XgsW8KVEhGJKyk714ONkO2xFhjLEvBbatsVRIR7VOVPg52KN3N1iJC7knV991i1XG29KqETmN7rUX4qqQxyelliT8SL7kqpUm7HJarolRbFESsh0k7ISovCkWsMSLVSOBzsc0b2JbX2IotVjdh92kWsLfY4ErntG7iQlaiRa4lYkNXq1Tg5ezgjG7t3o2PtRISLW8KRqORR2IXYuPtRj7HNYqvuZy0qWGxuli1h/imf/EACgRAAIBAwQBAwQDAAAAAAAAAAABAgMQEQQgITESBSJBIzBh8BMyUf/aAAgBAwEBPwF3jZGb5MnieJi+L4MbI/YzbkyzN8CMWW7FsmbYdud2bIXe5szuxvQu9jMjdsmRMX2URs7tmRzPI8mKQmKW9WVnZkiUrZE2RkJiZnbgaxaO2YouTwhenVpLngq6Sen91VZj+COloyj5RKunSWYkZEXZb09jJo0lBUl+bVa1PQ0vGq/LJpa1aPFKHt/fkm/byU55IsQxbc2UrJmSnHM0aqE+KlPtFP1GPVTghGOq1Mpy5SJFXplN4eBMi7dbHwMyZFIzgyUJfUjatpKdbs1FCtoX/JB8Gm1K1FPyRV6ZD+whCt8XlHI1gaE7s00ZOSn8ITT5VvUXFaafkekRaU2VOmQXuEhWQrIQ0OA4CEM08kqbhnGSpqFpkoUOUP1St/iKlSrq5fUkUnRow8YyK9eOOCn3kijGxvArswOJgaHEo0qbT8x6fTpvnJHRUZPslp6Ci5eX7gZSgJHzsl2Le7MxaQo5IK0dnz9tkmMjEQ2K7ZHdmzd2yTIxEr5tmyErOze1jYlkSxubELmzs7uzZk7EsXbu5GRC4R5b2OyEM6u3gyRR0LMnhENJlcn/xAAwEQABAwQBAgMECwAAAAAAAAABAAIRAwQQIBIFITAxMkFCUfATFBUiM2FxkbHR8f/aAAgBAgEBPwGUTkDAGYUKVOk4jE6AIIBAKEG4nMKFxUKVKOgQQQwB4RzGgQQQ2lSpU4O4QwFOSVKlSuS5ZKGoTUFOJxOoKClE7BBA7TpClDujidQVKBw97WDk4qp1mg0w2SmX7bkRSdxd+ad1G5Y7i5W3U3l4bV0KGD4AOOo3brh/b0jDKb7ypzYOMK6pUT63/e+fYmdniEO6jQI6DW5fxoPI+C6fVpd6Nf0n+VX6M4d6JlVy60tWU29iUFS9QQwRkbA6Xn4D/wBMW99Woek9lSuaN+3g8d/nyVxQNB/FUvUEPPBRGZUqMjEKFf1mNpupk9yEQR546eHG4bxXVfdVL1hDzwUcHJYuKLcjHUaL3Vg8NJgK3sDdS+5BC+xLf4lMt6Vm2abST+6r07mu/m5hVp0+q54LxAQ88lHBE6QuK4riuK4q8dWYW/RL6zeOAHGE6/uafuJl3dPeGFkf7/WD2RK9inSTmVPhE4KnE+DOpQ7IlBOOShiERgBRuEcSjg4GoHgSidgENpROk4J0JQC4rioXHedCUTpEoNUIqEGL/8QANRAAAQICBgkBBwUBAAAAAAAAAQIRAAMQEiExQFEEEyAwMkFQYXIiIzNCUnGBkhRgkaHRsf/aAAgBAQAGPwKC+Ovi3G2xZ0PvTbfFnR2GFswTxZ02zCd9+2y5sEWdHz6Q6oyHRnxD9UO2AOLnRZu7OhXbhxjWF29cY68CK0rSJCv5jjlf3HvJX9xxS1feChaWPQaslD5nkIBnrMxWQsEN+nRFiNWc0wFSyTkpMOQ0xPEKVKb1otB6BVulp41QJcpNVIoLXwROJKPicXQUHnHtVBQNhIj0LB7UTvA7Ie6CUBk5YmrnAr2Nas94cWg0qWsslMES1FEv/semWWzj2kwD6RVrqV5RN8TuH5YfRxyBrfxE2V86Wg6PpCSqWks3NMPJmBXbnRL0YXcSo187h+FMWUzvE4BjDQ29T4mg6To49r8Sfmj4kLT9iIAnDWo/uFzg9U3PEtIuCRsTvE4Nue8HgafayxW+YXwVSDrUZc4ZQZXeEy54dIsChAUkuDTN8ThGO7HidlpqLfm5iKiruRzjVKPoVd2NM7xOPHidpC+aFQFDlD0TvEwMHZupchJZSy1sDSJ2kS6rNlBqTEKbI7KJIvNphEtN6i1M7wNBpekb62M9vRPKBo6ZYnaQvhQzt3jSFTODVOVcnj3p/GPen8Y94fxj2CSTmqCpRdRj9TMHj/tM0nJoeDvFVkurlgJE5fChTmJk1ImayZeSImoRXrKSws2QqfNVMXlVsjjV+McavxiysftHyoFyYfE3RdtKEuYpSkLShQMtrVZW2xYlRSV1E8iT9Ik1j7wLPpFaxLXZwoyjMUgM3oa/7wkasupVQNbblASQASK3EGb6wSqxQmaspbs9LQTlj61bSJvqQQiYLJTX1bf8iWooWUpVMf6LPKNFlJVpIEmudZUDuWax+2cV0oVMUFSjWXZXqm8xKRLMxSteZjOCWqkcj3iUhctTollNeoFsaz3GJjIWkKm1w+VVthszvRhrtr6fsptltl6W2gMU+GOeMbH1dzWx789yIYXDZ7b6/dud132gnl+yH2n6S/S22//EACgQAAICAgICAgICAwEBAAAAAAABESEQMUFRYXEgkYHwodGx4fHBMP/aAAgBAQABPyHcmk0CQsO3jSGJwxvCQlHyO8IfQgSFIhtCI/JHhDdjbvCZ5PRJCRyF1Em4msyJlqZRGg0vHOKIfLZtkEV/DYkSCVJY6EDYkssgggvCymRKFBrEm8855GHjSxpCSUUc4cCSFJCI1p6DBtty2SbIwRkTJ9EEZjPboSu0NRiYOcvC5GvCQ2JSx3gmcRIxuBH+ZmcQRlSJ1yX2SbR4jySThvoQmpDUZVybHHJrxIzSEwWFKGLSSJFC2N4ici8yBAh3g3ICWjyJkaw0RhONFew1Y1Dw2N8NUDd4eVSzNb0iX0IkiRcnonoJ3sa5B9Rt3hJJMUxpPQ4Yn4PHMj3cRrB7Nvl0JLxQsjV0HhBJeTwgNrX+WOfeEIaWZHiWeZCY5QhvLIkaH5GrwjY940xyIexIUjEIHhBIRXake4PF4Y38E5mBSGjRYic+TZCR2bZcjEK2Uo5NsQRAkKWIr6kcdB6KHJbIZGGh5nOhDWEz+WGjgQQRY8VgtYnsSKivHI0Jcv8ACNu8HQm0RTY/DD3GWhymU/khWNRhB9NCc0PWC0NWJitjw2eYo0QK/QrG+hTVC1u2QWjxMXixvyOI4E1u/Y/GY06wmQiMSO1n+LEZVE7Fo2eZbHhQFjbCROIzvZwSvB0iehPNshNQj3IBuRFhcBPR2hSToRGE4fyZJ9wgITsoaGhzhHMQsI0IoswuCG6NsTezheCT0m8ZuhxgYghcyFL/AADRAniPghoYnYsFF0TwaYEsKzAhB7E5LELyKT8YZewzsXmNyw0c6xEXsNQ8GiSLX7hMJ/F4kJEcYQGxqByM1jHI3JMsQptj4IXkJFhNDt8EdQtpOS9qDlff/QX1939Bqa8RN/Q7kvhkdlPZYMYxxW1oTCeIHjQnEMUkRHp4tg7QzYtjxqbF2ILRVwW2vyHKyv8A1SFPyWN03ai/pPneGhXEN1vNokf/AGIaXiyX2PsKNEHC9ETGRhDwgkoGh5WQ1KxGfGE6zNJida10Lr2KWaaWJ+AqRwTpwgXqTSxyW15JrLTOW15Rue8r+sFn9GjmwvsSihMrGYeNjk+xDNYWWIXTE8NCwzmxFDQycKF4YyCpbUL2MK85v6EBiQlNc5RnWltnCs0lT9jcXspfZB0Ot2NnR+Wln6boQ1PZKZScDWCoiRiRA/kYqYstD0J6hedM38DJEwODxFYODLBiYafAl+pkv5CXLiSfQ9o5/wDHPK/ap7WGJjp7uhKlS+znyxohIklpLN+76HaXZSDGh7eBZbYkqecQIjHH0FLaEuQ9D0RhrCkPBwGhQNpFqC3i/wAeP0RNK8jVW24lghG7nX3FqqQw2Sg0H4vr4b9X0c0TJsNU8XTDRNESw94W8qGvBtD0RRuwcKZHe8LonIhiUoccFMcBIyqS9wqT8jryIpf7JRxW0kNEUYLZL2JwWk1zm/bdCQFwusDRZiDUHJC/BwTbIFLeBQhH9pDT+8GNqxijITIhwfwJp0yZ8fHiwfBP8g0unbrC/mIv3fRsmasZicEhXGxj4Fo3EvKB6OcmXg2vHZZSljfi0RKHXrDQunrCtHlaH1Fal+W0HmeGNeg3lFLspw/V9DSwwYY2o4OnjoQWzcWznGg5aCJ4fp6Epv6DP9EbrY4ag1oZQTsvpi/0ceiH8at7mW+VlqaPiwK/1oTSaQRIktLD9r0aV4wTRgayTY0zwxcRsLZU0PW+BA9i1/KIj1gXY6mfkH+wJtiDEMbLkYniR+nhm3PzB5BWTcKK5JaQn/2D/uD/ALQRZdboS/A3/dbYukRRV85TFNunt0bRN7B7GNY9RHOexAtk9jVHgJQmSn4Y19ifLQvAmRBjl5EV2NOme8JImadQ2NfGS9b9EvFhHcTb2TYmO2Pe2l/4dhFfH+xBH8ket9gVZu/pRE2Vwh/yx5RMyDk4FDnBG49i2JRmkkmiIF2JIRHynkhIJCk5GNI9A1W+jeS7IiVOf2glS+EiSJxFnfqS8Ko0O0M6OznXgUHyWas3walH4knxWApRnTmtCMWKbRwTaNkzc/bJ7JwSlkYbfQkYsGsn4c4JY9CfUbzIoEuroXRohBDynuShKuBeCEE+yZ12hOhoppUeRbiAkvk6zDkfZJimh8NG9LTANDoQYXdzdtNz50MU5lkvYS3tN9IVsp6zqxOSdSr1JAGFSqkWNeV0MiSc2SGN8rD2JMvFMRA0cnLEwKZo0P4SLCbXJMh0SfklCjokxhCTBiS2SSTSFrBukR7QsZD9JA3A2JA0sSoQ9iwW8FNPmWE3hMnEYEsISJQk2KETLEJCJlxiMNvBZQeiaKSzZ1gtMuE2/wD5CZIpJYmyWITwmS2JwKDoKS7EgWi7IG2NNDyxmyIw84RBsSvJBQKAekIQxr4oWExYTwnZT2aFZWCnoENL06RMx+WxLkYoSfJRFijyJBwRLxpS8GwkIOmUSNNvZBsl4PMZpiOxIst8k4QWEhF1XJ/qKORhNqYRAaOkNJtiNklyIlBwcDVQJZP2FSbGoeWcfAjwQQJZKQ7YkJfWNI2QQCVkgt+SkcbJVjsmCYeROHljcIZAsX+CNtiVm2IUdmoxYaELybwspCCRDeiBCSIcYkkIJihfcaJdDZryNiUvRybZAxRc/AhIIrEu37MsbNmlI0h4SvEE0sElHkSkSq5YSFrya9m2PEl9GxYRBdBO32SCqxuTgk2xBljsJTiJZMawi5qFnHCw40bwkJYQvgcnhCG+EJR7zrZd5I462Qr/AAhsS3wNzhuBuKIPZoS5NucQfZgIsci4KpbInDEiRlCyiCTg7BrrAu5vQkkhzYoqt8sjVobbz9sf8E9cCWIgmYgakiKIlkE8nK/B57Y9EQrGusKeLggg2bEJY2KRAaxWzvoY5Kd6x+NYLISNBbGWlr/IyzZHHBtxIhOxeCAgWPP0bKTyXT0f0EckEzofTkt4KPIl2xsSNE4SJJClKhWJheCYyoJTAwdsfgbibWTA7ZCjmCJ9ISCOOhItSGhQRxKIKQlIpCWhQgY//9oADAMBAAIAAwAAABCFixYnpFtHKvc0A8HofzIJUAcDYEoXfp+7T9N5SKdc/wAjxUMDyRNYqu5XjAv1aPAouzyDsmghcD5QYYktt0+CL18eePe/d6K1ZLUq+UV1/wDo8rbO7LOkDYyoaKhfpQA9A1BfVErGsGpEdfUg08UPECVL+q/6yR0+lagSxqSpMFBMkbHTsN0kcB524T8abcJFvLgBmn574SV+Md7uoYvglTxAzhcwEjyAYJrQg4tAp142Ens3QgF90+TnMwDAzx3604Z7mN+sjzoAj4t1kvM8uUAelMGXwG/l7DW53Jj5m69oyIt4EBvVskgpI6XsblJJFusa63nd498dNR7Es4/5OZ8srD80AaiUzzS7tglqPUEjyvANMQtPEIAk+Af94nv1makJvjAqOoum+LowLP4vCwD9R6nn1Z4Kov6fw//EACYRAQEBAAICAQIHAQEAAAAAAAEAESExEEFRYaFxgbHB0eHwkSD/2gAIAQMBAT8QEdeBvgTgj58IJT1Gu7EAssJFz7jmfBvhhh5eW23ZBs8NZRfIQW3fD7ErDb4HLkES546ay/Egm6+rT3Y/NohHuzOofGR8r14jh57Zl1sJgwsWbZlhsSz4jwg28Tuq9y5scSnwBsEUWW9RYeHiObMtzu7JcsuS9ymWys9t6iydRTvd8F6Hw6RzHE8kJ7vjHDHJffgy5lbdQPbLOi+FaeeMQ6WZETqRaXctOYnk8J3PiIHr8W4R+L+r8QMp4/3+Ypuj9b25LTvwDLHLduoY5vo3T4h022CPDIty/ua/T6TMGvXD2j+h/iSUqdBevwWH2mWjHLJzOOT0yE9mGJ6sMcWVvG+FDGd+S5Z/cP3mcOvk6/n9b4Zwevg/TYZfZWj4XBHJCxm2GsbjzG41eqUp3cc/J4K44/J3/f53Pmnv9k/35XRpOE+G+yuvxbLLdLdmxGJeFhyQ7zH0s2KR8OQ1+I3TTx87Dj8fX3np05+99t5dwTx0uksuL4QsT1MQTqT7kNgDpyvxno+f0l4OR55zmB1/wf5jAAHrcD+fvECfjvbDY9X4jr/JJdjwcEPFu4nxBZD5kPH/AIQzMPf1/wAf3yfDA9bnx/Z9LTcQ7zo4X3v0PzhRrhw33/a463EbclkTLfC2YyOvCSSSyPcowJ9p3MJAWnLAgw2+U8t7lt2BEeGZkkPdx6sWBLWyL4WxkMJchtYcktvNtsszfAt14tLd1sLqCcEs4I54iHNob5aHhOeZZsn6LItUYuru30S22zlnbkwtziWSy8L6ldcvhYGvED2+MOLeLbLi5c302aVd8y+pLctllsuLlvnHfB4a+Fyz/Wd92kZBIXZ83//EACcRAQEBAAICAQMEAgMAAAAAAAEAERAhMUFRYYGhcZGxwdHwIOHx/9oACAECAQE/EHtxbHFnxat44EwCZq2VhhWDOLs4LLsHEOuDAiFZk/CywsWfXBxwIJnxe4cB1GDhwJ7sLrkCRLfmFnDt6nxe48TvC6HFZbWwLqxYsXS0bpLDDJZHjnO7pdmX1yalcTgHbyXRlzndnHwlEG08I4SSTOBDdLQhtfVt7s4PriHqG21lyFbLPcwwzxdJ6jgRHTZRwBkIAPbZg+oHX5/xfRmAGP8Av6/aTMCfQjwEetOsknYhLHqHSHJHBbxD1LPaQ+FPf6/wRHhjBp40/v8A1hhIBnR5+z/MQS007+9gTEx2O56Zxs4OCcJCk/kWv4ulj0r6/V635/q2s74en9/D+L5ii+/l+/efpw/LP5uqknF4nuWMOnBZDwZbZsE/Wfxxmmnwez/r7XRse8/tf795S3T0/S/LP54c6jwe58xCOIM82QjgIu3lMO+5XBnHsMP49/iAPz7/AG6vzT+Yc3jbw3Igeyc2NiQy48CtMOt18vx/M8wmB63r6l/6B/ieJHwKfv4PxPyvgx6PibiDrvX2LzMvVuy6lLAMYNh9WEmRZgcmd1Hzhvs/rYfNX3m/J3+H67nzAZ2Xxp2+D1n1fH7WJlprj46e/T2lng+t0QxqzLZeLLYmGMsLqW22we0uiMW2ZdnhnJm7DM2REsqFgWCb4QxxD3xFvUoGQRqwk4CJllmcblt37Zy75npJerOAawui8XmTgST/AIr3nI2WzLZNkIZ3wcBDYzkbZ+F4ODYvFpItp4do4ZK3d3ZMW7jqItYd6ie48URGOBrxKnd//8QAJhABAAICAgICAwADAQEAAAAAAQARITFBUWFxgZEQobHB0fAg4f/aAAgBAQABPxDm6ldAVijbAseYAP4CEaoIZcbM2+Is4lbncTniMsIO0arWFBiAgagTvc6SNvzxAGQR56vEatB7lgWTkQt2sAi0RS8mochmpq6Y7pfJiJcJfnDONqW7aiHESEdsob8cRX9sSU4ZUeITaepeT7is7mCuZcNeJ90zBo0ShaNtrzKLlQH0iuorwfMpiDrlnFU/bGOY57mXMCLcy1y0CMwfjcJq7OmGFpjdX1EHOIuYIYNMblIEqmBaY4gZogX4EruJW5zHRG+YGflj5RhAhtaiQU2cPER9RgCF/wCESpwqpkyY1wR5MAuoKK6WDET4aiMLIrdSjm4AyhKnMoxnFgfl6jZbO45g4SxiJn4lURV6IObjwpZCzJ6l0zDojX/CUFvEUvLKqF11AFGObXmKXLmX0TJpmHRDvqFZf3LUwxdoHtFaBhjKXyRLaxI/UtxETWYbXv8AB/cR75EdkDs6lyLmBlYIDGGjzlUsWvf8RxCh5YhGo6L9y5itA4mDcC/mYV0ZpmKS2fwBHiiBW4X2QAeYdCCuaThWHgngX7l9jZMmiKZf5GAfLuV8oLkYlz6I21KqO7VTBijr3GEJkmIlhtivWPE6IZHuVHwmZFuosTiOWqgLIbe4FpLHspdxpogLt3KjqyWUqLQilVg7ZkhXlxAKQviEaRHSW5WC9wQ6SBO5yqfcFyR6nPg1C2yyImTJP7E+H8DB4loA0moipjZ5nxIBYDdwlnqZXhsjQKjtjj8LDAcxVg0YmTmcDmHg0/cVN8zNUbj45WZh9EMtA5YGV0fN6lkUvmPNSd0DEOoFZgfMy1LT3ANMLtZRRZLONT7JWxE7i8MTfmOw3HBGhHV6g1MhM2zFkeHmcxGArHdI3edRZqG7h9jNvERcxb1HOJj7w9EvZyPoiXKxC/MTRMRKc7iOp0SzmZPUod3Cx3EmfjzLekMZh3ZBciAwdzg5iXh2QHZGr7TCjqVB3KMPMGXZHZOECHmOhYfZKo6Eu7QX4RbTMLb4iKLt4h0XxOMPDth1BDQRDWkabtYuBUTmYhzLRC/wBMjFeYVxhh3+5ZdMTEMorxKPUTFkvN5iAs1/cGpV6gsUwL4lW15jlgpJnNoMw3SDG+C4rS8zBACDJ+olEu6WpdzwrIrf5AOXUZ/pGoKy26I14BBu8MQ8I11l9S0MnuKhxFR4mQ3ZKFiIHJwznzNmdweZQU7lxjhxKKeZVDY2dMqMXoTRuGw+czLxKGaPqZ5Q2rKcw5eibH3NFQoAgUCF8PzDcvTiHb/iANadwvTjuJmf6IioRiVl5vMbkJD7fM2s/cMQA6E0RdxxFmz5mYoS4ZhlxZPKJzxCK38kTMqULHolAZsxvMuD2Tf1cw/VK2MFkISNwZhtR4hlCZQ5a63MBe2YAQMEW7/oE2TQ3BMUkFameYBW3i1ECfDjnm0bd3N5I21JZqNbuCU+Fjx76ckVJnjMznHUqyzUT9zWIP3HMGIYYjefWCnjEYUckKxwyyuO3KsVTjkuKmYqIKMTe2G25iW5zFm3bLBW2WNcwbpzKigPMx/F9QpE6BD9jI80cVUOVlMSnSQ1jiWZYCZgg+Q7iU/fSU5DUbZI4hti2L+CdS44fcFKcT1EoXTPVMQVBZXEytTb4ZTTmbI4L6lEmFxZqaCO/AmSDYlD2lxlo5YQAo28sCoL7TcQE4bzhNxuplS28yzkpgzWN42Qg2EKICOpVOzh0ynRHLK1liTZE6iQYsnAwMHiFcQRGIJWZaJg4ib4cMquprwmCjHl2jrMqvDcuFcQdUPEMRYd2CMhJQzQiES/cVgGk1PKgucTM695cjx+cStfsSpGjjF+wTJjtPjs7Ilh9RorfuOI+mFGmCnwzsY4SKx5cdSlxKlDuXkeIiC4GKiqSsMsfBDr6jY0ckupUDCB4mJ4moxLCR0JhDDAUvmGq9MGSpeXxC5EYoSftr/DcpB+0UPRy92eoQWDkq/bA5wYSU+myOMaDUfqFOGMLXNXyHhp/f48oKxfEZoz6E/kMWjlIdwPSJdafyG55gJqmLpeoQuVdx9eHMFlTS5comdwQ6lCMUhmOWCUnGyCzEGGGhzK2SiVESGxi3tvMcR+eobbeLhsSAfpArjyGzQ8qa+4Wmq5Tyryvf4RYiVTReLiEabBcCrGeDiUJjo2PA8kxNOHwCDY3TTDyxl6PuzOUoJtD94pgUmyNG6zDq75ihXUrZgFDackTU6GWmJ4QMLn+cNfEW98MsuZRMMqnxKJkirPU0upcZgjNpMpUqaaZpje0wCamRjZMscx2MxL5GLS6iR4dAtXoiGKLl6BPZYPRDPkTsDpHk/KRAccRdENnS8jvohy3uksvthhi3KwUfnAfuU0Ohg+QP3P+j3gCuMH3HB5iSOJlZVZzApMj3KllgS8zEDCqMpVWalHcCmJUqpgwnmHplAqKmlyoqR4eYWPMs3LCpjGBxAPhGjX4GpeSyUN9QXMIHSgvRH7CEopy0kafhpj0LBsGkTsvh+GCGAXd8syfhZ5gR2Wi+qX5JbT+NirnxcBKsjAKA6D8eUdJ1/bCVDzLmPEcnipgWbUJascxDKQWW+EqzV3AvntDdTAfEyGNiBwy2mvlA2yLUN/Sxb/APwxQ19eYdP3KXMNyZjwzcxlqyW1iXA+mNaOdkTm0OIy5m2fgHBJhxQMf4HPxm0VXyAfSMfE2FKjsGH5PmUQ1lAAKaXzA3MFcgMzl7/HlMv+LOb8MpVQUmXH81Ds/AOaeY5ELgmC6gqDJcsQO4lN9zAOX+pm4vi9QDS8cJxHG2gYe4i1deI0wPbuIh4lMr4lJHDB0JqDQ64lhxDC6xC94lrMR+r/AIT8rWg0H6zfzcAmOSgfVa9j6l29U54FMNWKChCinJ5M+4Nfxewfjyn/AD+8sHuV27PwvZNxHLzKL6hYNjEoEyU5JQxYiSqzK2ZWrlMinIzD1c1wbiHDjjpFew4hr6ln+GJg2OZfLUJTNCLwM/2IDgt1Dac9xQ0dr4P/AC9JSih6v8HEP0ZQKGcPvsjlFMV+jw/38eU/6XeYQOvVMpUBLsmd+I2sKrm0FvlKbGhio/UqXubSsylazFlEMdPEHJnl4jQ2i9dIW+O5Spo/UlGrZsh4Zha3r+Sw7JhycHqNGXPmVqcnHiYgUg2F2SgT/wDL/wBFUAa8Ck+wfiMWiP0jYxA2iHyTlP8Ahd4oJlnRLGEWxujZgSximbj0lQ1q5q8SimccC5qoKTAxGogI1CTdPJwwcjnsgC2zlbJpDabUcso8aYbWWPtqXbLfDFXEPadQy2a8RFK06YZC/XV214JX1Bo2DGV8QbBAlDuv/JdUpHHq/b/JZc+OhauCVQUHicoqb/i8S3tWl/0T7EuvMqS8GWOltxyTo2MaHyzFmS9wSiHaEYWWzfEcgr0dQb7G+DKeaYgGx3FKckCitdoLRfGhsgF4Bybi/wBbuaISOYsTfO5Q7lDfPiM7ecGDUUSfLgCL3NG78S6CaL/l8T/pf8QUaf8A56jveUe9uT+o8Yu2/wBEQFiGzbfpWD2v48oU4D3YU/sNK0EsHEX/ABmblxDwGAdjFl6gWLmGIGqhRylncywjuClujzlVJSlNMs39OmEuCukdYXFHK+5WEbZgbFHsjRcf1ENIJuQctAhfUE2ah/lgN0pMfc+LsegLo8EEZdCBRjLMtaDQYh6/QQIGrumh3iNRk4DbyJfyo8QwBDQYD6ha0TuXB17U/LRCK5XaZ7OWC1hTEYFdwbU6lt5lqPcVAmB5ilfEs5Yde5m4KBXEs9JvOWC9EbGHWecvg5GCyHEqhq41D4S5wowDFvkuLXJ9FR4A+YdSQ+QiJf1hoNCV8WiwQgdBQCgRTjy90O2CYEoBkJkVO6zHB0MYkPcgEpVvSXXagkBXLGtviWE5F4UszREqycmSWNQSFrWYaJveNx83FGBcrfFV5uLS6lH1Ku2KmRzXV+WNlXczY1MHxuCvqe2Jtx1NkCqjbnnM2sYgW9zCsJ8pwMuDZkls1K1XjqG6LxqW7XzKV1ZL9lQA4fMyonkcwSwRt/qlLGPRAeBFulUD6h1svVysveGBS3eKOmyRlhVp3eLDiBpDyNUHuO8HSPAcStKqERVF3yKsph++n7y4GQaLiLINcAp9jLsWVgYUVoy3JwLyYBXWpqWJRmsMwMzi5Wj/AIhiXLKRFoA3Ha+PwcDmEoCw0gWlTXMVMxFNPqHLDxBc5gvcw43KNTLziaZBDmBgpqV4gXVfUExAHLFwEN5jjpZQ3WYhq6Dgjr2xHAxDY5viIJD5rMrErn5mJVTkAwV8Y6m1lUKAAJQuWMoB25ZgY8RIDqXat3HWB1CrTVTp3KMRV1i0KzaH7jCaJtLTU6UHOfwCrRC00fUHzFeCpTuviJWCUOGWOWiJ6ZnTXLN+/iHD+IdZ31NhqiXuuEFUaJkOGW2Lliso0SvcbgYczJFECz4JdrdsDQ4/HFOXEwJSdjROZii+o2rERmiXM3Cz3B/ABgWm4BuC4hyRPDEMXLHH4k+VIAwZjHom2uZZugIXZRB2yspuZyqC3LGUiIDlyzDjiLr4iKxzGU9EVLLtM7mBltvmXekO41Y2DiGumVG3VsxTMmK4BqjLMCJklQxxKz+Bcwg0xa1cQ8Qfj8RfLODT3CxyPhHLLcaqMEEFH3KLkmoqowqDkYQpLYDqYs5Yrlf9csHtNb5lVuwCOpMR8csZyagKqJQ8wzBgvEucnE2urv4gbuOI8vcolwzSY0vERqCm1mTBWVdZMEQGV2w6agHqBcKNjcBqGUBVhFQaK+4Y7TGhV/Uy1WogeY6XeIQTXfMNFaMrrxKrTTgeJWyCsvbGAcVLkBgjgcMdMbdhtWqpw8xXtj4SgjZmWi9rKCqCc5olZz4mgwEqveNLO9ysxqtwRVOkpC2o/GodoadQsMS1n8hcy51hGj3AMm5tjM/aYuM/iFwxMUC1olC1mT7ZYOhqKGlDbALe00EMNyxKUaZgVHwAkEFQkDvbF1ZlWay4JjHA1FQx8Td4wQfptmgwcTBuFssHMwIr8Rb1GZ0bjXnUu0bepY3uGBF9GiIgkW26+5fRUwlwEAjVh/ZZqsQBMRqIAcOjuXEV9vcVdxqx2/Uyrl2uv2l7O4q7xFBZNySy9EJSkwHnzC/i4NZrcILVriXDiaIjvAbT+S8BRxG2ShVgiIujRFcDE5BiPqo2olQph1CzcNe4522KxdmfEcNbiVLgKjBob5S7P8Szgyzthauh+4Cg/RAqxfM8QaO2OIbW3qIqNTkRfSZPCFDGiK0G2VUcR5qKxXCeCPddyrxd9yhdGAl6uCIBs0ER2RXlhw/ZCzRztl7xENAjm24zEgLlMQZgXbiB4xHcQN3kiWXCrtZlSajaUx5l/TKLTlEXrzKAogUYjIotcT4jZGBVKekiJeO+40WE7S54hShMgPmVezKlu0qiuiXpaMxW5riN1jmFCjbKKHaFdWXBAZOe5dSFHBFLO+WC25zC+5YUbhYELbfUXdy7gfwqU3B9QxvRFZWECP2l9Iqzt6lDZz1Nq+0vDldQ1zcSWGPolgDwvl7jNOHKSjrBj4HDiIl1DDZuAFsywwahyY9K+CNPd3DT0QUNqYa8Dh2woHlPg6goAZj5x47i4bPMDEy9y47mkPlhUDb1BC2JfiLB7uA8zBieGpzogUFsAGXwRCsHgJZGIqMsLKh2l1q1u5UnXUAEFqzyep4S+1MKjxFWhARDKPgI3AicO43+Ig2osXFYbhHAYJ547hgv8IC2tcsBbfpE1ZywKEUB6CN5Qs+pgVbBuG1zK8HmBWMvcHCv9IFfzuLRqLbxAlAgiGUnmhQwQyq4OIo0gl0FhPdVKggojxGZS5WLITUNvM1LQy2gCvLG5N9ytrjzMLl3BbUr3MJRE5IAt7lXW0VAXs8PcSlaPHECUNRgHAQVBoIOb4gguV14iPA7lWivMsKss1eJWDbzCIGWUFpbP//Z"
              alt="Raccourci BirdSignal sur le bureau de Windows, avec son icône"
              width="327" height="317" loading="lazy" decoding="async">
            <figcaption>Installée depuis le navigateur, elle se range sur le bureau comme un logiciel.</figcaption>
          </figure>

          <div class="bsm-cta bsm-rise" style="--i:5">
            <a class="bsm-btn" href="https://pcdlabs.presentcomposedesign.fr/tools/birdsignal/" target="_blank" rel="noopener">Ouvrir et installer</a>
          </div>

          <div class="bsm-stats">
            <div class="bsm-stat bsm-rise" style="--i:0"><b data-bsm-count="6522">6&#8239;522</b><span>espèces</span></div>
            <div class="bsm-stat bsm-rise" style="--i:1"><b data-bsm-count="2">2</b><span>langues</span></div>
            <div class="bsm-stat bsm-rise" style="--i:2"><b data-bsm-count="137">137</b><span>tests auto</span></div>
            <div class="bsm-stat bsm-rise" style="--i:3"><b data-bsm-count="0">0</b><span>octet audio envoyé</span></div>
          </div>
        </div>
      </div>
    </div>
  </section>


  <!-- ============================== POINTS CLES ========================= -->
  <section class="bsm-sec" data-bsm-reveal>
    <div class="bsm-wrap">
      <p class="bsm-kicker bsm-rise">Points clés</p>
      <h2 class="bsm-h2 bsm-rise" style="--i:1">Ce que j'ai réalisé</h2>
      <ul class="bsm-keys">
        <li class="bsm-left" style="--i:1">Idéation, positionnement et naming de l'application</li>
        <li class="bsm-left" style="--i:2">Création du logo, du système d'icônes et de l'identité graphique complète</li>
        <li class="bsm-left" style="--i:3">Conception de l'expérience, des parcours et de l'interface, en deux langues</li>
        <li class="bsm-left" style="--i:4">Développement intégral, front et back, et intégration d'un modèle d'intelligence artificielle</li>
        <li class="bsm-left" style="--i:5">Rendu 3D temps réel des cartes, avec gyroscope sur mobile</li>
        <li class="bsm-left" style="--i:6">Mise en ligne, sécurisation, hébergement en France et documentation</li>
      </ul>
    </div>
  </section>

  <!-- ============================== LE SON ============================== -->
  <section class="bsm-sec bsm-sec--night" data-bsm-reveal>
    <div class="bsm-wrap">
      <p class="bsm-kicker bsm-rise">Le cœur de l'application</p>
      <h2 class="bsm-h2 bsm-rise" style="--i:1">Tout part d'un son</h2>
      <p class="bsm-p bsm-rise" style="--i:2">
        L'application enregistre quelques secondes de chant, en construit le
        <strong>spectrogramme</strong>, et soumet cette image de fréquences à un
        modèle d'intelligence artificielle de 6522 espèces. Le tout dans le
        navigateur, en moins de temps qu'il n'en faut pour ranger son téléphone.
        Ce qui défile ci-dessous n'est pas une illustration : c'est
        <strong>l'analyse d'un vrai enregistrement</strong>, une rue de Toulouse
        à six heures du matin.
      </p>

      <!--
        L'image ci-dessous n'illustre pas un spectrogramme : elle EST le
        spectrogramme de la bande qui joue sur cette page, calcule par
        transformee de Fourier sur quarante secondes enregistrees a Toulouse a
        six heures du matin. Meme rampe de couleurs que dans l'application.
        Elle est cuite a l'avance plutot qu'a l'ouverture de la page : quatre
        megaoctets a telecharger et une analyse frequentielle pour une page de
        presentation, ce serait faire payer au lecteur le prix d'une
        demonstration.
      -->

      <div class="bsm-spectro bsm-zoom" style="--i:3" data-bsm-spectro>
        <img fetchpriority="high" class="bsm-spectro-fond" data-bsm-spectro-img decoding="async"
           alt="Spectrogramme de quarante secondes enregistrées à Toulouse à six heures du matin : chaque trait vertical clair est une vocalisation d'oiseau."
           width="1118" height="96" src="data:image/png;base64,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">
        <canvas aria-hidden="true"></canvas>
        <p class="bsm-spectro-legend"><i></i> Enregistrement réel</p>
        <div class="bsm-spectro-axis" aria-hidden="true"><span>8,5 kHz</span><span>4 kHz</span><span>0,4</span></div>
      </div>
      <p class="bsm-cap">
        Le temps en abscisse, la fréquence en ordonnée, l'intensité en couleur.
        Chaque trait vertical est un oiseau ; la bande claire du bas est la
        ville qui se réveille. Activez le son pour entendre ce que vous voyez.
      </p>

      <div class="bsm-chain">
        <div class="bsm-rise" style="--i:1"><b>1. Capter</b><span>Micro ou fichier importé, rééchantillonné à 48 kHz.</span></div>
        <div class="bsm-rise" style="--i:2"><b>2. Voir le son</b><span>Transformée de Fourier, spectrogramme, découpage en segments de 3 secondes.</span></div>
        <div class="bsm-rise" style="--i:3"><b>3. Reconnaître</b><span>Modèle BirdNET exécuté en WebAssembly, filtré par le lieu et la saison.</span></div>
      </div>
    </div>
  </section>

  <!-- ============================== LA CARTE ============================ -->
  <section class="bsm-sec bsm-sec--wash" data-bsm-reveal>
    <div class="bsm-wrap">
      <p class="bsm-kicker bsm-rise">Design produit</p>
      <h2 class="bsm-h2 bsm-rise" style="--i:1">Le son devient un objet</h2>
      <p class="bsm-p bsm-rise" style="--i:2">
        Chaque analyse conservée devient une carte numérotée, avec sa photo, son
        spectrogramme, sa date et son lieu. Elle se retourne en 3D et s'incline au
        gyroscope, de sorte que le reflet suit le poignet comme sur une vraie carte.
      </p>

      <div class="bsm-stage bsm-zoom" style="--i:3" aria-hidden="true">
        <div class="bsm-card3d">
          <!-- RECTO -->
          <div class="bsm-face bsm-face--front">
            <div class="bsm-cardtop">
              <span class="bsm-cardbrand"><svg viewBox="0 0 1024 1024" style="fill:#8b96ff"><use href="#bsm-mark"/></svg>Bird<em>Signal</em></span>
              <span class="bsm-pill">Corvidés</span>
            </div>
            <div class="bsm-cardphoto"><svg viewBox="0 0 1024 1024" style="fill:#fff"><use href="#bsm-mark"/></svg></div>
            <div>
              <p class="bsm-cardname">Pie bavarde</p>
              <p class="bsm-cardsci">Pica pica</p>
              <p class="bsm-cardmeta" style="margin-top:4px">chant &middot; Défense de territoire</p>
            </div>
            <div class="bsm-gauge">
              <div class="bsm-gauge-row"><span>Rareté &middot; LC</span><b>8 %</b></div>
              <div class="bsm-gauge-track"><div class="bsm-gauge-fill bsm-gauge-fill--rar"></div></div>
              <div class="bsm-gauge-row"><span>Confiance</span><b>91 %</b></div>
              <div class="bsm-gauge-track"><div class="bsm-gauge-fill bsm-gauge-fill--conf"></div></div>
              <div class="bsm-cardfoot">
                <p class="bsm-cardmeta">13 sept. 2026, 07:43<br>12,8 s &middot; localisé</p>
                <span class="bsm-seal"></span>
              </div>
            </div>
          </div>
          <!-- VERSO -->
          <div class="bsm-face bsm-face--back">
            <div class="bsm-cardtop">
              <span class="bsm-cardbrand"><svg viewBox="0 0 1024 1024" style="fill:#8b96ff"><use href="#bsm-mark"/></svg>Bird<em>Signal</em></span>
              <span class="bsm-pill">Détails</span>
            </div>
            <div class="bsm-cardrows">
              <div><span>Espèce</span><b>Pie bavarde</b></div>
              <div><span>Moteur</span><b>BirdNET</b></div>
              <div><span>Filtre local</span><b>157 espèces</b></div>
              <div><span>Durée</span><b>12,8 s</b></div>
              <div><span>Position</span><b>43.6080, 1.4812</b></div>
            </div>
            <div class="bsm-cardback-mark"><svg viewBox="0 0 1024 1024"><use href="#bsm-mark"/></svg></div>
            <p class="bsm-cardmeta" style="letter-spacing:.08em;text-transform:uppercase;font-weight:700">Spectrogramme</p>
            <div class="bsm-cardspectro"><canvas data-bsm-mini aria-hidden="true"></canvas></div>
            <div class="bsm-cardfoot" style="margin-top:auto">
              <p class="bsm-cardmeta">ÉMISE PAR BirdSignalR_001<br>presentcomposedesign.fr</p>
              <span class="bsm-badge bsm-badge--or" style="width:38px;height:38px">
                <i class="bsm-ring"></i><i class="bsm-hair" style="inset:6px"></i><i class="bsm-gloss"></i>
                <b style="font-size:13px">Or</b>
              </span>
            </div>
          </div>
        </div>
      </div>

      <p class="bsm-kicker bsm-rise" style="margin-top:34px">Les quatre rangs</p>
      <p class="bsm-p bsm-rise" style="--i:1">
        Le rang change la matière de la carte, et il se mérite. Passez la souris
        sur les poinçons : ce sont exactement ceux frappés au verso des cartes.
      </p>
      <div class="bsm-badges" data-bsm-badges>
        <div class="bsm-badge-cell bsm-zoom" style="--i:1">
          <span class="bsm-badge bsm-badge--uc" data-bsm-badge><i class="bsm-ring"></i><i class="bsm-hair"></i><i class="bsm-gloss"></i><b>Uc</b></span>
          <b class="bsm-badge-name">Non classée</b><span>Aucune espèce reconnue</span>
        </div>
        <div class="bsm-badge-cell bsm-zoom" style="--i:2">
          <span class="bsm-badge bsm-badge--sd" data-bsm-badge><i class="bsm-ring"></i><i class="bsm-hair"></i><i class="bsm-gloss"></i><b>Sd</b></span>
          <b class="bsm-badge-name">Standard</b><span>Argent</span>
        </div>
        <div class="bsm-badge-cell bsm-zoom" style="--i:3">
          <span class="bsm-badge bsm-badge--or" data-bsm-badge><i class="bsm-ring"></i><i class="bsm-hair"></i><i class="bsm-gloss"></i><b>Or</b></span>
          <b class="bsm-badge-name">Authentique</b><span>Captée sur place, localisée</span>
        </div>
        <div class="bsm-badge-cell bsm-zoom" style="--i:4">
          <span class="bsm-badge bsm-badge--dd" data-bsm-badge><i class="bsm-ring"></i><i class="bsm-hair"></i><i class="bsm-gloss"></i><b>Dd</b></span>
          <b class="bsm-badge-name">Diamant</b><span>Irisé, le plus rare</span>
        </div>
      </div>
    </div>
  </section>

  <!-- ============================== GEOLOCALISATION ===================== -->
  <section class="bsm-sec bsm-sec--night" data-bsm-reveal>
    <div class="bsm-wrap">
      <p class="bsm-kicker bsm-rise">Le point fort</p>
      <h2 class="bsm-h2 bsm-rise" style="--i:1">Votre collection raconte vos sorties</h2>
      <p class="bsm-p bsm-rise" style="--i:2">
        Chaque carte retient l'endroit où le chant a été capté. La collection
        devient une carte de vos promenades, et surtout une <strong>carte des
        espèces</strong> : on voit où elles sont, et où elles ne sont plus.
      </p>

      <div class="bsm-map bsm-zoom" style="--i:3" aria-hidden="true">
        <svg viewBox="0 0 800 450" preserveAspectRatio="xMidYMid slice">
          <g opacity=".16" stroke="#8f97ff" stroke-width=".7">
            <path d="M0 90H800M0 180H800M0 270H800M0 360H800"/>
            <path d="M120 0V450M240 0V450M360 0V450M480 0V450M600 0V450M720 0V450"/>
          </g>
          <path d="M60 330c60-80 150-40 210-120 50-66 150-52 210 6 52 50 140 24 180-40 22-36 60-40 90-30"
                fill="none" stroke="#8f97ff" stroke-width="46" stroke-linecap="round" opacity=".08"/>
          <path class="bsm-trail" pathLength="900"
                d="M80 356C170 300 200 214 300 220 400 226 396 316 470 300 560 281 570 170 660 156"/>
          <g class="bsm-pin" style="--i:1">
            <circle cx="80" cy="356" r="7" fill="#7ef0ff"/><circle cx="80" cy="356" r="14" fill="none" stroke="#7ef0ff" stroke-width="1.4" opacity=".45"/>
            <text x="80" y="392" text-anchor="middle">Merle noir<tspan x="80" dy="13">08:14 · LC</tspan></text>
          </g>
          <g class="bsm-pin" style="--i:2">
            <circle cx="300" cy="220" r="7" fill="#9dffdd"/><circle cx="300" cy="220" r="14" fill="none" stroke="#9dffdd" stroke-width="1.4" opacity=".45"/>
            <text x="300" y="184" text-anchor="middle">Mésange charbonnière<tspan x="300" dy="13">09:02 · LC</tspan></text>
          </g>
          <g class="bsm-pin" style="--i:3">
            <circle cx="470" cy="300" r="7" fill="#ffd79a"/><circle cx="470" cy="300" r="14" fill="none" stroke="#ffd79a" stroke-width="1.4" opacity=".45"/>
            <text x="470" y="336" text-anchor="middle">Pie bavarde<tspan x="470" dy="13">09:47 · LC</tspan></text>
          </g>
          <g class="bsm-pin" style="--i:4">
            <circle cx="660" cy="156" r="7" fill="#e6a8ff"/><circle cx="660" cy="156" r="14" fill="none" stroke="#e6a8ff" stroke-width="1.4" opacity=".45"/>
            <text x="660" y="120" text-anchor="middle">Tourterelle turque<tspan x="660" dy="13">10:21 · LC</tspan></text>
          </g>
        </svg>
      </div>
      <p class="bsm-cap">Représentation de principe. La position n'est enregistrée qu'avec l'accord de l'utilisateur, et elle est arrondie avant tout partage.</p>
    </div>
  </section>

  <!-- ============================== IDENTITE ============================ -->
  <section class="bsm-sec" data-bsm-reveal>
    <div class="bsm-wrap">
      <p class="bsm-kicker bsm-rise">Identité graphique</p>
      <h2 class="bsm-h2 bsm-rise" style="--i:1">Un signe qui se lit à 16 pixels</h2>
      <p class="bsm-p bsm-rise" style="--i:2">
        Un oiseau et une lettre dans la même forme. Une seule couleur pour le
        signe : une identité qui a besoin d'un dégradé pour exister ne survit
        pas à une impression en noir. Le favicon, lui, existe en deux fichiers,
        parce qu'un gris clair disparaît sur une barre d'onglets blanche.
      </p>

      <div class="bsm-logos">
        <figure class="bsm-logo bsm-zoom" style="--i:1">
          <div class="bsm-logo-art"><svg viewBox="0 0 1024 1024"><use href="#bsm-mark"/></svg></div>
          <figcaption class="bsm-logo-lab">Signe principal<span>#1001ef sur blanc</span></figcaption>
        </figure>
        <figure class="bsm-logo bsm-zoom" style="--i:2">
          <div class="bsm-logo-art bsm-logo-art--blue"><svg viewBox="0 0 1024 1024"><use href="#bsm-mark"/></svg></div>
          <figcaption class="bsm-logo-lab">En réserve<span>Blanc sur aplat de marque</span></figcaption>
        </figure>
        <figure class="bsm-logo bsm-zoom" style="--i:3">
          <div class="bsm-logo-art bsm-logo-art--tile"><span class="bsm-tile"><svg viewBox="0 0 1024 1024"><use href="#bsm-mark"/></svg></span></div>
          <figcaption class="bsm-logo-lab">Icône d'application<span>Zone sûre du masque Android</span></figcaption>
        </figure>

        <!--
          Les deux vignettes suivantes ne sont pas des redessins : ce sont les
          fichiers reellement servis par l'application, recopies ici trait pour
          trait. Le favicon existe en deux versions parce qu'un onglet n'a pas
          la meme couleur de fond selon le theme du navigateur, et qu'un signe
          gris clair disparait sur une barre blanche.
        -->
        <figure class="bsm-logo bsm-zoom" style="--i:4">
          <div class="bsm-logo-art bsm-logo-art--pair">
            <figure class="bsm-fav">
              <span class="bsm-fav-plate bsm-fav-plate--light"><svg viewBox="0 0 1024 1024" role="img" aria-label="Favicon BirdSignal"><defs><linearGradient id="bsm-favgrad-l" x1="548.56" y1="512.11" x2="1024" y2="512.11" gradientTransform="translate(0 1024.1102) scale(1 -1)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#def0f5"/><stop offset=".45" stop-color="#c0e3eb"/><stop offset="1" stop-color="#8fd0dd"/></linearGradient></defs><path fill="url(#bsm-favgrad-l)" d="M880.58,0h-332.01c93.06,27.65,165.69,99.56,193.31,188.87l92.64,33.12c23,8.23,43.99,18.77,62.66,34.42-59.95,1.18-110.83,22.83-166.15,38.43-80.13,22.63-183.32,95.69-149.54,225.33,21.75,10.15,42.13,21.43,62.34,34.82,79.85,52.92,122.25,137.15,115.8,228.44-6.06,85.83-52.86,162.2-131.59,208.4-22.87,13.41-47.25,24.32-72.81,32.17h325.36c79.2,0,143.39-62.09,143.41-138.68V138.68c-.02-76.59-64.22-138.68-143.41-138.68h-.01Z"/><path fill="#a8a9a9" d="M534.78,604.76l-124.11-59.62-86.94-42.16c-20.47-9.92-37.86-23.29-55.29-38.04-55.23-46.72-86.17-115.15-82.82-188.48,39.48,62.43,103.62,97.78,171.01,128.58l141.51,61.85c2.18-38.77,21.32-106.04,38.48-135.46,39.58-67.83,101.74-105.27,180.27-123.72-27.9-9.85-49.77-27.77-65.27-53.06-20.07-32.75-47.58-56.6-81.1-75.48-32.12-14.98-65.33-27.04-102.54-27.06h-.3c-213.16-.01-385.98,178.89-385.96,399.58l.04,482.54,371.43-.34c48.61-.04,93.59-15.44,128.19-46.41,58.92-52.73,69.44-139.02,31.46-207.36-18.21-32.81-45.44-56.94-78.06-75.37h-.01ZM511.94,144.09c20.23,0,36.63,16.32,36.63,36.45s-16.4,36.47-36.63,36.47-36.63-16.32-36.63-36.47,16.42-36.45,36.63-36.45Z"/></svg></span>
              <figcaption>Thème clair</figcaption>
            </figure>
            <figure class="bsm-fav">
              <span class="bsm-fav-plate bsm-fav-plate--dark"><svg viewBox="0 0 1024 1024" role="img" aria-label="Favicon BirdSignal"><defs><linearGradient id="bsm-favgrad-d" x1="548.56" y1="512.11" x2="1024" y2="512.11" gradientTransform="translate(0 1024.1102) scale(1 -1)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#def0f5"/><stop offset=".45" stop-color="#c0e3eb"/><stop offset="1" stop-color="#8fd0dd"/></linearGradient></defs><path fill="url(#bsm-favgrad-d)" d="M880.58,0h-332.01c93.06,27.65,165.69,99.56,193.31,188.87l92.64,33.12c23,8.23,43.99,18.77,62.66,34.42-59.95,1.18-110.83,22.83-166.15,38.43-80.13,22.63-183.32,95.69-149.54,225.33,21.75,10.15,42.13,21.43,62.34,34.82,79.85,52.92,122.25,137.15,115.8,228.44-6.06,85.83-52.86,162.2-131.59,208.4-22.87,13.41-47.25,24.32-72.81,32.17h325.36c79.2,0,143.39-62.09,143.41-138.68V138.68c-.02-76.59-64.22-138.68-143.41-138.68h-.01Z"/><path fill="#e2e1e1" d="M534.78,604.76l-124.11-59.62-86.94-42.16c-20.47-9.92-37.86-23.29-55.29-38.04-55.23-46.72-86.17-115.15-82.82-188.48,39.48,62.43,103.62,97.78,171.01,128.58l141.51,61.85c2.18-38.77,21.32-106.04,38.48-135.46,39.58-67.83,101.74-105.27,180.27-123.72-27.9-9.85-49.77-27.77-65.27-53.06-20.07-32.75-47.58-56.6-81.1-75.48-32.12-14.98-65.33-27.04-102.54-27.06h-.3c-213.16-.01-385.98,178.89-385.96,399.58l.04,482.54,371.43-.34c48.61-.04,93.59-15.44,128.19-46.41,58.92-52.73,69.44-139.02,31.46-207.36-18.21-32.81-45.44-56.94-78.06-75.37h-.01ZM511.94,144.09c20.23,0,36.63,16.32,36.63,36.45s-16.4,36.47-36.63,36.47-36.63-16.32-36.63-36.47,16.42-36.45,36.63-36.45Z"/></svg></span>
              <figcaption>Thème sombre</figcaption>
            </figure>
          </div>
          <figcaption class="bsm-logo-lab">Favicon<span>Deux fichiers, un par thème d'onglet</span></figcaption>
        </figure>

        <figure class="bsm-logo bsm-zoom" style="--i:5">
          <div class="bsm-logo-art bsm-logo-art--footer"><svg viewBox="0 0 1024 1024" role="img" aria-label="Logo BirdSignal de pied de page"><path fill="#ffffff" d="M759.62,783.43c6.45-91.29-35.95-175.52-115.8-228.44-20.21-13.39-40.59-24.67-62.34-34.82-33.78-129.64,69.41-202.7,149.54-225.33,55.32-15.6,106.2-37.26,166.15-38.43-18.68-15.65-39.66-26.19-62.66-34.42l-92.64-33.12C714.25,99.56,641.62,27.65,548.56,0H0v1024h555.22c25.55-7.86,49.94-18.76,72.81-32.17,78.72-46.19,125.53-122.56,131.59-208.4ZM581.4,887.48c-34.61,30.97-79.58,46.37-128.19,46.41l-371.43.34-.04-482.54c-.02-220.69,172.8-399.59,385.96-399.58h.3c37.21.02,70.43,12.08,102.54,27.06,33.52,18.87,61.02,42.73,81.1,75.48,15.5,25.29,37.37,43.21,65.27,53.06-78.53,18.45-140.69,55.89-180.27,123.72-17.16,29.42-36.3,96.69-38.48,135.46l-141.51-61.85c-67.39-30.81-131.52-66.15-171.01-128.58-3.35,73.33,27.59,141.76,82.82,188.48,17.43,14.76,34.82,28.13,55.29,38.04l86.94,42.16,124.11,59.62h.01c32.62,18.42,59.85,42.55,78.06,75.36,37.98,68.34,27.47,154.63-31.46,207.36Z"/><path fill="#1001ef" d="M534.79,604.75h-.01s-124.11-59.61-124.11-59.61l-86.94-42.16c-20.47-9.92-37.86-23.29-55.29-38.04-55.23-46.72-86.17-115.15-82.82-188.48,39.48,62.43,103.62,97.78,171.01,128.58l141.51,61.85c2.18-38.77,21.32-106.04,38.48-135.46,39.58-67.83,101.74-105.27,180.27-123.72-27.9-9.85-49.77-27.77-65.27-53.06-20.07-32.75-47.58-56.6-81.1-75.48-32.12-14.98-65.33-27.04-102.54-27.06h-.3c-213.16-.01-385.98,178.89-385.96,399.58l.04,482.54,371.43-.34c48.61-.04,93.59-15.44,128.19-46.41,58.92-52.73,69.44-139.02,31.46-207.36-18.21-32.81-45.44-56.94-78.06-75.37ZM511.94,144.09c20.23,0,36.63,16.32,36.63,36.45s-16.4,36.47-36.63,36.47-36.63-16.32-36.63-36.47,16.42-36.45,36.63-36.45Z"/><path fill="#ffffff" d="M511.94,217c20.23,0,36.63-16.34,36.63-36.47s-16.4-36.45-36.63-36.45-36.63,16.3-36.63,36.45,16.4,36.47,36.63,36.47Z"/></svg></div>
          <figcaption class="bsm-logo-lab">Logo de pied de page<span>Plaque blanche, signe bleu</span></figcaption>
        </figure>

        <figure class="bsm-logo bsm-zoom" style="--i:6">
          <div class="bsm-logo-art">
            <div class="bsm-favrow">
              <figure><svg viewBox="0 0 1024 1024" style="width:48px"><use href="#bsm-mark"/></svg><figcaption>48</figcaption></figure>
              <figure><svg viewBox="0 0 1024 1024" style="width:32px"><use href="#bsm-mark"/></svg><figcaption>32</figcaption></figure>
              <figure><svg viewBox="0 0 1024 1024" style="width:16px"><use href="#bsm-mark"/></svg><figcaption>16</figcaption></figure>
            </div>
          </div>
          <figcaption class="bsm-logo-lab">Lisibilité<span>Validé à sa plus petite taille</span></figcaption>
        </figure>
      </div>
    </div>
  </section>

  <!-- ============================== METHODE ============================= -->
  <section class="bsm-sec bsm-sec--wash" data-bsm-reveal>
    <div class="bsm-wrap">
      <p class="bsm-kicker bsm-rise">Méthode</p>
      <h2 class="bsm-h2 bsm-rise" style="--i:1">De l'idée à la mise en ligne</h2>
      <div class="bsm-steps">
        <article class="bsm-step bsm-rise" style="--i:1"><span class="bsm-num">01</span>
          <h3 class="bsm-h3">Idéation et positionnement</h3>
          <p class="bsm-p">Définir ce que l'application fait, ce qu'elle refuse de faire, et à qui elle s'adresse.</p></article>
        <article class="bsm-step bsm-rise" style="--i:2"><span class="bsm-num">02</span>
          <h3 class="bsm-h3">Naming</h3>
          <p class="bsm-p">Un nom qui tient dans une icône, se prononce en français comme en anglais, et dit quelque chose de vrai sur le produit.</p></article>
        <article class="bsm-step bsm-rise" style="--i:3"><span class="bsm-num">03</span>
          <h3 class="bsm-h3">Logo et identité graphique</h3>
          <p class="bsm-p">Du croquis au vectoriel, décliné en favicon, icônes d'application, thème clair et thème sombre.</p></article>
        <article class="bsm-step bsm-rise" style="--i:4"><span class="bsm-num">04</span>
          <h3 class="bsm-h3">Architecture et UX</h3>
          <p class="bsm-p">Parcours, écrans, arborescence, et une architecture choisie pour tenir sur un hébergement mutualisé français.</p></article>
        <article class="bsm-step bsm-rise" style="--i:5"><span class="bsm-num">05</span>
          <h3 class="bsm-h3">Développement</h3>
          <p class="bsm-p">Interface, API, traitement du signal audio, IA embarquée, rendu 3D temps réel et tests automatisés.</p></article>
        <article class="bsm-step bsm-rise" style="--i:6"><span class="bsm-num">06</span>
          <h3 class="bsm-h3">Mise en ligne et suivi</h3>
          <p class="bsm-p">Sécurisation, hébergement en France, application installable et hors ligne, documentation et mises à jour.</p></article>
      </div>

      <!--
        Gabarit pret a l'emploi, si des captures d'ecran sont ajoutees un jour.
        Il reste en commentaire : un cadre pointille « emplacement image » sur
        une page publiee ne dit qu'une chose, c'est que la page n'est pas finie.

        <div class="bsm-slot bsm-zoom">
          <img decoding="async" src="ADRESSE-DE-L-IMAGE" alt="Interface de BirdSignal">
          <p class="bsm-cap">L'analyse, la collection, la carte en volume.</p>
        </div>
      -->
    </div>
  </section>

  <!-- ============================== SERVICES ============================ -->
  <section class="bsm-sec" data-bsm-reveal>
    <div class="bsm-wrap">
      <p class="bsm-kicker bsm-rise">Ce que cela veut dire pour vous</p>
      <h2 class="bsm-h2 bsm-rise" style="--i:1">Un seul interlocuteur, du premier croquis à la mise en ligne</h2>
      <div class="bsm-serv">
        <article class="bsm-step bsm-rise" style="--i:1">
          <h3 class="bsm-h3">Application web sur mesure</h3>
          <p class="bsm-p">Outil métier, configurateur, application mobile installable. Conçue, développée et hébergée.</p></article>
        <article class="bsm-step bsm-rise" style="--i:2">
          <h3 class="bsm-h3">Identité et design produit</h3>
          <p class="bsm-p">Naming, logo, système d'icônes, interface et déclinaisons, pensés pour tenir à toutes les tailles.</p></article>
        <article class="bsm-step bsm-rise" style="--i:3">
          <h3 class="bsm-h3">3D, AR et expériences immersives</h3>
          <p class="bsm-p">Configurateurs 3D temps réel, visualiseurs, réalité augmentée, directement dans le navigateur.</p></article>
      </div>
    </div>
  </section>

  <!-- ============================== APPEL =============================== -->
  <section class="bsm-sec bsm-sec--wash bsm-final" data-bsm-reveal>
    <div class="bsm-wrap">
      <h2 class="bsm-h2 bsm-rise">Vous souhaitez faire appel à mes services ?</h2>
      <p class="bsm-lede bsm-rise" style="--i:1">
        Parlons de votre projet. J'interviens sur l'ensemble de la chaîne, ou sur la
        partie qui vous manque.
      </p>
      <div class="bsm-cta bsm-rise" style="--i:2">
        <a class="bsm-btn" href="https://presentcomposedesign.fr/contact/">Demander un rendez-vous</a>
        <a class="bsm-btn bsm-btn--ghost" href="https://presentcomposedesign.fr/realisations/">Voir les autres réalisations</a>
      </div>
      <p class="bsm-legal bsm-rise" style="--i:3">
        BirdSignal est une application gratuite et non commerciale, conçue et hébergée
        en France par Present Composé Design, nom commercial de l'entreprise
        individuelle Alban DESBARAX, à Toulouse. Les hypothèses d'espèce produites sont
        probabilistes et ne constituent pas une détermination scientifique.
      </p>
    </div>
  </section>
</div>

<script>
(function () {
  var root = document.getElementById('bsm');
  if (!root) return;

  /* Marque la presence du script AVANT tout le reste : c'est cette classe, et
     elle seule, qui autorise la feuille de style a cacher ce qui va apparaitre.
     Si cette ligne n'est jamais atteinte, l'article reste lisible, sans
     animation. */
  root.classList.add('bsm-js');

  var calme = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  var survol = !window.matchMedia || window.matchMedia('(hover: hover)').matches;

  /* ------------------------------------------------------------ egaliseur
     Genere plutot qu'ecrit : 44 barres a la main seraient 44 lignes a relire
     pour rien. */
  var eq = root.querySelector('[data-bsm-eq]');
  if (eq) {
    var html = '';
    for (var i = 0; i < 44; i++) {
      var hb = 18 + Math.round(Math.abs(Math.sin(i * 0.7)) * 70 + (i % 5) * 4);
      html += '<i style="height:' + hb + '%;animation-delay:' + ((i % 11) * 0.09).toFixed(2) +
              's;animation-duration:' + (1 + (i % 6) * 0.14).toFixed(2) + 's"></i>';
    }
    eq.innerHTML = html;
  }

  /* ----------------------------------------------------------- apparitions
     Seuil a zero, et non a un pourcentage : sur un ecran court, 12 % d'une
     section haute peut depasser la hauteur de la fenetre, et la section ne
     s'afficherait jamais. */
  var sections = root.querySelectorAll('[data-bsm-reveal]');
  if (calme || !('IntersectionObserver' in window)) {
    for (var k = 0; k < sections.length; k++) sections[k].classList.add('bsm-in');
  } else {
    var io = new IntersectionObserver(function (es) {
      es.forEach(function (e) {
        if (e.isIntersecting) { e.target.classList.add('bsm-in'); io.unobserve(e.target); }
      });
    }, { threshold: 0, rootMargin: '0px 0px -10% 0px' });
    for (var j = 0; j < sections.length; j++) io.observe(sections[j]);
  }

  /* -------------------------------------------------------------- compteurs
     Le HTML porte deja la valeur finale : sans script, les chiffres restent
     justes. C'est ici, et seulement ici, qu'on repart de zero. */
  function separe(v) {
    var s = String(v), out = '', c = 0;
    for (var p = s.length - 1; p >= 0; p--) {
      out = s.charAt(p) + out;
      if (++c % 3 === 0 && p > 0) out = ' ' + out;
    }
    return out;
  }
  function compte(el) {
    var cible = parseInt(el.getAttribute('data-bsm-count'), 10) || 0;
    if (calme || cible === 0) { el.textContent = separe(cible); return; }
    el.textContent = separe(0);
    var t0 = null, duree = 1000;
    function pas(t) {
      if (t0 === null) t0 = t;
      var p = Math.min(1, (t - t0) / duree);
      el.textContent = separe(Math.round(cible * (1 - Math.pow(1 - p, 3))));
      if (p < 1) requestAnimationFrame(pas);
    }
    requestAnimationFrame(pas);
  }
  var compteurs = root.querySelectorAll('[data-bsm-count]');
  if (!('IntersectionObserver' in window)) {
    for (var m = 0; m < compteurs.length; m++) compte(compteurs[m]);
  } else {
    var io2 = new IntersectionObserver(function (es) {
      es.forEach(function (e) { if (e.isIntersecting) { compte(e.target); io2.unobserve(e.target); } });
    }, { threshold: 0.5 });
    for (var q = 0; q < compteurs.length; q++) io2.observe(compteurs[q]);
  }

  /* ====================================================== SPECTROGRAMME
   *
   * Un spectrogramme, c'est le temps en abscisse, la frequence en ordonnee et
   * l'intensite en couleur. Celui-ci n'est pas synthetique : c'est la
   * transformee de Fourier de quarante secondes reellement enregistrees a
   * Toulouse a six heures du matin, la meme bande que le bouton de son
   * allume. Elle a ete calculee une fois pour toutes et rangee dans une image
   * indexee de quarante kilo-octets, plutot que recalculee dans le navigateur
   * de chaque lecteur : l'enregistrement pese quatre megaoctets, et personne
   * n'a a payer le prix d'une demonstration qu'il n'a pas demandee.
   *
   * Le defilement, lui, est vivant : la colonne de droite est l'instant
   * present. Quand la bande joue, cet instant est la tete de lecture, et ce
   * qui s'affiche est exactement ce qui s'entend.
   */
  var SPECTRO = { cols: 1118, bandes: 96, debut: 4, duree: 40, parSeconde: 28 };

  var image = root.querySelector('[data-bsm-spectro-img]');
  var piste = root.querySelector('[data-bsm-audio]');
  var origine = Date.now();

  /** Seconde courante DANS l'extrait, que la bande joue ou non. */
  function instant() {
    if (piste && !piste.paused && !piste.ended) {
      var t = piste.currentTime - SPECTRO.debut;
      if (t >= 0 && t <= SPECTRO.duree) return t;
    }
    return ((Date.now() - origine) / 1000) % SPECTRO.duree;
  }

  /**
   * Dessine la fenetre de `visibles` colonnes qui se termine a `fin`.
   * L'extrait boucle : une fenetre a cheval sur la fin et le debut se dessine
   * en deux morceaux plutot que de sauter.
   */
  function fenetre(ctx, w, h, fin, visibles) {
    if (!image || !image.complete || !image.naturalWidth) return;
    var cols = SPECTRO.cols;
    var depart = Math.round(fin - visibles) % cols;
    if (depart < 0) depart += cols;
    var premier = Math.min(visibles, cols - depart);
    var echelle = w / visibles;
    ctx.drawImage(image, depart, 0, premier, SPECTRO.bandes, 0, 0, premier * echelle, h);
    if (premier < visibles) {
      ctx.drawImage(
        image, 0, 0, visibles - premier, SPECTRO.bandes,
        premier * echelle, 0, (visibles - premier) * echelle, h
      );
    }
  }

  function defile(host, opts) {
    var cv = host.querySelector('canvas');
    if (!cv || !cv.getContext) return;
    var ctx = cv.getContext('2d');
    var w = 0, h = 0, raf = 0, vu = false;

    function taille() {
      /*
       * offsetWidth et non getBoundingClientRect : le spectrogramme du verso
       * vit DANS la carte qui tourne, et un rectangle mesure a l'ecran y
       * change a chaque image. Le canvas se serait redimensionne, donc vide,
       * soixante fois par seconde. Les dimensions de mise en page, elles,
       * ignorent les transformations.
       */
      var nw = Math.max(60, host.offsetWidth), nh = Math.max(30, host.offsetHeight);
      if (nw === w && nh === h) return;
      w = nw; h = nh; cv.width = w; cv.height = h;
    }

    function peins() {
      taille();
      if (!w || !h) return;
      var visibles = Math.max(8, Math.round(opts.secondes * SPECTRO.parSeconde));
      var fin = (instant() + opts.decalage) * SPECTRO.parSeconde;
      fenetre(ctx, w, h, fin, visibles);
    }

    function boucle() {
      raf = 0;
      if (!vu || document.hidden) return;
      peins();
      raf = requestAnimationFrame(boucle);
    }
    function relance() { if (!raf && vu && !document.hidden) raf = requestAnimationFrame(boucle); }

    peins();
    if (image && !image.complete) image.addEventListener('load', peins);
    if (calme) return; // image fixe : le propos reste lisible, rien ne bouge
    window.addEventListener('resize', peins);
    document.addEventListener('visibilitychange', relance);

    if ('IntersectionObserver' in window) {
      new IntersectionObserver(function (es) {
        vu = es[0].isIntersecting;
        if (vu) relance(); else if (raf) { cancelAnimationFrame(raf); raf = 0; }
      }, { threshold: 0 }).observe(host);
    } else { vu = true; relance(); }
  }

  var grand = root.querySelector('[data-bsm-spectro]');
  if (grand) defile(grand, { secondes: 13, decalage: 0 });
  var mini = root.querySelector('[data-bsm-mini]');
  // Le verso de la carte montre un AUTRE moment du meme enregistrement :
  // deux fois la meme seconde a l'ecran aurait eu l'air d'un decor duplique.
  if (mini) defile(mini.parentNode, { secondes: 4.5, decalage: 19 });

  /* ============================================================== LE SON
   *
   * Aucun navigateur ne laisse une page emettre du son sans geste du lecteur,
   * et c'est heureux. La bande n'est donc meme pas telechargee avant le clic :
   * quatre megaoctets imposes a qui passait lire deux paragraphes seraient
   * un abus, sur un forfait mobile comme sur une connexion lente.
   *
   * Au demarrage, la tete de lecture est posee la ou le spectrogramme en est
   * rendu, et non au debut : l'image ne saute pas, elle se met simplement a
   * dire la verite.
   */
  var bouton = root.querySelector('[data-bsm-sound]');
  if (piste && bouton) {
    var fin = SPECTRO.debut + SPECTRO.duree;
    var intitule = bouton.querySelector('[data-bsm-sound-lab]');

    function boucleExtrait() {
      if (!piste.duration) return;
      if (piste.currentTime >= fin || piste.currentTime < SPECTRO.debut) {
        try { piste.currentTime = SPECTRO.debut; } catch (e) { /* metadonnees pas encore la */ }
      }
    }
    piste.addEventListener('timeupdate', boucleExtrait);

    bouton.addEventListener('click', function () {
      if (piste.paused) {
        piste.muted = false;
        var reprise = SPECTRO.debut + instant();
        var lecture = piste.play();
        if (lecture && lecture.then) {
          lecture.then(function () {
            try { piste.currentTime = reprise; } catch (e) { /* on repart du debut */ }
          }, function () {
            // Refus du navigateur : on ne ment pas sur l'etat du bouton.
            piste.muted = true;
            root.classList.remove('bsm-son');
            bouton.setAttribute('aria-pressed', 'false');
            if (intitule) intitule.textContent = 'Activer le son';
          });
        }
      } else {
        piste.muted = !piste.muted;
      }
      var audible = !piste.muted;
      root.classList.toggle('bsm-son', audible);
      bouton.setAttribute('aria-pressed', audible ? 'true' : 'false');
      if (intitule) intitule.textContent = audible ? 'Couper le son' : 'Activer le son';
    });

    // Le bouton ne s'affiche que tant que l'article est a l'ecran : il n'a
    // rien a faire par-dessus le reste du site.
    if ('IntersectionObserver' in window) {
      new IntersectionObserver(function (es) {
        if (es[0].isIntersecting) root.classList.add('bsm-audible');
        else root.classList.remove('bsm-audible');
      }, { threshold: 0 }).observe(root);
    } else {
      root.classList.add('bsm-audible');
    }
  }

  /* ================================================ POINCONS QUI SUIVENT
   *
   * Les poincons s'inclinent vers le pointeur, leur reflet se deplace, et le
   * film irise du rang Diamant tourne avec l'angle. C'est ce reflet mobile
   * qui donne la sensation du metal, bien plus que la couleur.
   *
   * Tout est calcule dans UNE seule frame d'animation, quel que soit le
   * nombre d'evenements recus : `pointermove` se declenche des dizaines de
   * fois par seconde, et lire une position d'element a chaque fois obligerait
   * le navigateur a recalculer la mise en page autant de fois.
   */
  var badges = root.querySelectorAll('[data-bsm-badge]');
  if (badges.length && survol && !calme) {
    var px = 0, py = 0, attend = false;

    function applique() {
      attend = false;
      var vh = window.innerHeight || 800;
      for (var b = 0; b < badges.length; b++) {
        var el = badges[b], r = el.getBoundingClientRect();
        if (r.bottom < -40 || r.top > vh + 40) continue;
        var cx = r.left + r.width / 2, cy = r.top + r.height / 2;
        var dx = (px - cx) / (r.width * 2.4), dy = (py - cy) / (r.height * 2.4);
        dx = dx < -1 ? -1 : dx > 1 ? 1 : dx;
        dy = dy < -1 ? -1 : dy > 1 ? 1 : dy;
        el.style.setProperty('--ry', (dx * 17).toFixed(2) + 'deg');
        el.style.setProperty('--rx', (-dy * 17).toFixed(2) + 'deg');
        el.style.setProperty('--gx', (50 + dx * 44).toFixed(1) + '%');
        el.style.setProperty('--gy', (30 + dy * 44).toFixed(1) + '%');
        el.style.setProperty('--sweep', (Math.atan2(py - cy, px - cx) * 57.2958).toFixed(1) + 'deg');
      }
    }
    window.addEventListener('pointermove', function (e) {
      px = e.clientX; py = e.clientY;
      if (!attend) { attend = true; requestAnimationFrame(applique); }
    }, { passive: true });
  }
})();
</script>
				</div>
				</div>
				</div>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/birdsignal-infos/">BirdSignal-infos</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		<enclosure url="https://pcdlabs.presentcomposedesign.fr/tools/birdsignal/Birds-in-town_6am_Toulouse_12b-r-l.plana.mp4" length="4117624" type="video/mp4" />

			</item>
		<item>
		<title>PCd 1210 Virtual Direct Drive &#8211; infos</title>
		<link>https://presentcomposedesign.fr/pcd-1210_infos/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Sun, 13 Sep 2026 03:53:15 +0000</pubDate>
				<category><![CDATA[[ia]]]></category>
		<category><![CDATA[Design produit]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=36774</guid>

					<description><![CDATA[<p>PCd-1210Virtual Direct Drive Étude de cas &#8211; conception à mise en ligne Concevoir une platine vinyle dans un navigateur. PCd-1210 est née d&#8217;une idée simple : garder le contrôle sur la musique que l&#8217;on écoute, même quand la source est numérique. J&#8217;ai piloté le projet de A à Z, de l&#8217;idée initiale à l&#8217;identité graphique, de l&#8217;architecture de l&#8217;expérience aux itérations fonctionnelles, puis jusqu&#8217;aux tests, au déploiement PWA et à la mise en ligne sur PCdlabs. Ouvrir PCd-1210Voir le code sur GitHubSoutenir le projet Une platine virtuelle dans votre navigateur, jusque dans votre poche. L&#8217;intention Retrouver un geste physique dans une écoute numérique. L&#8217;enjeu n&#8217;était pas de dessiner une simple skin de lecteur audio, mais de construire une interface où le disque, le bras, le pitch, le choix 33/45 et la bibliothèque se répondent comme un seul instrument. 01 Idée et positionnement Définition du concept, du vocabulaire visuel et des interactions qui donnent une vraie présence à la platine. 02 Identité graphique Création du branding PCd-1210, du favicon, de l&#8217;univers sombre et lumineux et des composants visuels de l&#8217;application. 03 Direction produit Arbitrage des fonctions, du responsive, du comportement du bras, de la discothèque et des modules optionnels. Le résultat Une PWA qui reste cohérente du desktop au téléphone. Le même objet passe du grand écran au smartphone : une platine virtuelle que l’on retrouve dans sa poche, avec un plateau carré, une discothèque au swipe ou au gyroscope, la recherche accessible et un branding intégrable ou isolable. PCd-1210 Virtual Direct Drive Recherche intégrée au menu Branding isolable Mon rôle Une conception itérative, jusque dans les détails. Chaque version a servi à valider une hypothèse précise sans perdre les éléments déjà stabilisés. Conception Définition du produit, hiérarchie de l&#8217;interface, analogie avec une platine réelle et choix des interactions prioritaires. Design Logo, favicon, typographie d&#8217;interface, couleurs, effets de matière, équilibre entre skeuomorphisme et interface web contemporaine. Développement Pilotage du développement HTML, CSS et JavaScript, PWA, gestion de la lecture, bibliothèque, imports, pitch, bras, scratch, gyroscope et responsive. Tests Comparaison des versions, corrections ciblées, vérification desktop/mobile, recherche des régressions et stabilisation de l&#8217;expérience. Mise en ligne Préparation du package statique, service worker, manifest, déploiement sur IONOS et documentation de reprise du projet. Technique Le navigateur comme matière de design. La version finale reste volontairement déployable sans compilation obligatoire. L&#8217;architecture privilégie des fichiers lisibles, un comportement progressif et la possibilité d&#8217;installer l&#8217;application comme PWA. Manipulation directe Le bras et le disque ne sont pas de simples illustrations. Ils répondent à la progression, au pitch, aux gestes de souris et aux interactions tactiles. Responsive La platine conserve sa géométrie. Les modules auxiliaires changent d&#8217;emplacement sans réduire le plateau ni couper leurs sliders. Progressive Web App Manifest, service worker, installation et fonctionnement web partagent la même base de code. Open source Un projet ouvert à l&#8217;inspection et à l&#8217;évolution. PCd-1210 v15.2 est distribuée sous licence MIT. Le code peut être étudié, modifié et redéployé dans le respect de cette licence. Code disponible Le dépôt GitHub public doit accueillir la même base que celle déployée en production. Le ZIP source peut aussi être téléchargé directement depuis l&#8217;application. GitHubZIP source Stack HTML + CSS + JavaScript PWA / Service Worker YouTube iframe API Web Audio pour les fichiers compatibles Three.js pour les mises en scène 3D Déploiement statique IONOS Usage légal Lire et manipuler, sans devenir une plateforme de piratage. L&#8217;application est conçue comme une interface de lecture. Elle n&#8217;héberge pas un catalogue musical tiers, ne contourne pas les DRM et conserve YouTube dans son lecteur/API. Les fichiers locaux restent ceux fournis par l&#8217;utilisateur. Cette architecture vise un usage légal, mais elle ne peut pas transformer une source non autorisée en source autorisée. L&#8217;utilisateur reste responsable des droits attachés aux fichiers et liens qu&#8217;il choisit d&#8217;utiliser. Essayez-la Gardez le contrôle sur la musique que vous écoutez. PCd-1210 est accessible dans le navigateur et installable comme PWA. Ouvrir l&#8217;applicationSoutenir le créateurVoir GitHub PCd-1210 Virtual Direct Drive &#8211; Present Composé Design / PCdlabs &#8211; Alban &#38; Emilio DESBARAX &#8211; 2026 PCdlabs / Présent Composé Design PCd-1210 Virtual Direct Drive PCd-1210 est une platine vinyle virtuelle conçue comme une véritable expérience d&#8217;interface. Le projet transpose dans le navigateur des repères et des gestes issus de la platine physique, puis les associe à une discothèque visuelle, une playlist, un contrôle 33T/45T, un pitch, un bras dynamique, le scratch et une installation PWA. Tester PCd-1210 Voir le projet sur GitHub Sponsoriser le créateur Une idée d&#8217;abord centrée sur le geste Le point de départ n&#8217;était pas de reproduire une platine uniquement comme objet graphique. L&#8217;objectif était de construire une interface où la logique physique reste lisible : un disque qui tourne, un bras qui accompagne l&#8217;avancement d&#8217;une piste, des vitesses 33T et 45T, un pitch lié à la lecture, puis une manipulation directe du disque pour le scratch. Identité graphique J&#8217;ai défini l&#8217;identité visuelle PCd-1210, le branding, les proportions générales, la hiérarchie de l&#8217;interface, les états lumineux et la cohérence entre la platine et Ma Discothèque. Le travail sur les favicons et le SVG animé prolonge cette identité jusque dans l&#8217;onglet et l&#8217;installation PWA. Conception de l&#8217;expérience Les interactions ont été pensées autour d&#8217;actions compréhensibles sans mode d&#8217;emploi lourd : lancer ou arrêter un titre, déplacer le bras, modifier la vitesse, régler le pitch, faire défiler les pochettes, sélectionner un disque et le manipuler directement. Développement par itérations Le projet a progressé par versions successives. Chaque étape a servi à consolider la lecture, les imports, la playlist, le bras, le pitch, le scratch, la discothèque, les modules audio et le responsive. Cette méthode a permis de conserver ce qui fonctionnait déjà tout en corrigeant des points précis. Une PWA volontairement simple à déployer PCd-1210 repose sur HTML, CSS et JavaScript natifs. La version de production n&#8217;impose aucune compilation. Le manifest, le service worker et les ressources locales permettent un déploiement direct sur un hébergement mutualisé IONOS par FileZilla. Ma Discothèque, du swipe au gyroscope La bibliothèque n&#8217;est pas présentée comme une</p>
<p>Cet article <a href="https://presentcomposedesign.fr/pcd-1210_infos/">PCd 1210 Virtual Direct Drive &#8211; infos</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="36774" class="elementor elementor-36774">
				<div class="elementor-element elementor-element-072ca91 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="072ca91" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
				<div class="elementor-element elementor-element-860c849 elementor-widget elementor-widget-html" data-id="860c849" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!--
PCd-1210 v15.2 - bloc HTML WordPress prêt à coller dans une page /pcd-1210_infos/
IMPORTANT : remplacer {{GITHUB_REPOSITORY_URL}} par l'URL du dépôt GitHub une fois le dépôt publié.
Le bloc reste lisible si WordPress retire le script Three.js : les visuels et le contenu ne dépendent pas du canvas.
-->
<section id="pcd1210-story" class="pcd1210-story">
  <style>
    #pcd1210-story{--bg:#090b13;--panel:#121827;--text:#f8f9fc;--muted:#a4aec0;--line:rgba(255,255,255,.12);--pink:#ff5e78;--cyan:#26dfd8;--blue:#5570ff;background:radial-gradient(circle at 12% 4%,rgba(255,60,100,.15),transparent 26%),radial-gradient(circle at 80% 12%,rgba(70,90,255,.15),transparent 28%),var(--bg);color:var(--text);border-radius:34px;overflow:hidden;font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.pcd1210-story *{box-sizing:border-box}.pcd1210-wrap{width:min(calc(100% - 34px),1160px);margin:auto}.pcd1210-hero{display:grid;grid-template-columns:minmax(0,1fr) minmax(360px,.85fr);align-items:center;gap:34px;padding:72px 0 54px}.pcd1210-brand{display:flex;align-items:center;gap:14px;margin-bottom:26px}.pcd1210-logo{position:relative;width:66px;height:66px;flex:0 0 66px}.pcd1210-logo img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}.pcd1210-logo .anim{animation:pcdLogo 3s ease-in-out infinite}.pcd1210-brand strong{display:block;font-size:1.12rem}.pcd1210-brand span{color:var(--muted);font-size:.85rem}.pcd1210-kicker{color:var(--pink);text-transform:uppercase;font-weight:950;letter-spacing:.16em;font-size:.76rem}.pcd1210-story h1{font-size:clamp(3rem,7vw,6.5rem);line-height:.88;letter-spacing:-.065em;margin:10px 0 18px}.pcd1210-story h2{font-size:clamp(2rem,4.6vw,4.2rem);line-height:.96;letter-spacing:-.05em;margin:8px 0 16px}.pcd1210-story h3{margin:0 0 8px}.pcd1210-lead{font-size:clamp(1.15rem,2vw,1.7rem);line-height:1.25;color:#edf0f8;max-width:690px}.pcd1210-copy{color:var(--muted);line-height:1.72}.pcd1210-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:24px}.pcd1210-btn{display:inline-flex;align-items:center;justify-content:center;min-height:48px;padding:11px 17px;border:1px solid var(--line);border-radius:999px;color:#fff!important;text-decoration:none!important;font-weight:850;background:rgba(255,255,255,.05)}.pcd1210-btn.primary{background:linear-gradient(135deg,#ff496d,#ff8195);color:#12131a!important;border-color:transparent}.pcd1210-stage{position:relative;min-height:500px;border:1px solid var(--line);border-radius:30px;overflow:hidden;background:linear-gradient(145deg,rgba(255,255,255,.08),rgba(255,255,255,.02));box-shadow:0 34px 90px rgba(0,0,0,.35)}#pcd1210-three{position:absolute;inset:0;width:100%;height:100%}.pcd1210-stage-logo{position:absolute;width:90px;aspect-ratio:1;left:50%;top:50%;transform:translate(-50%,-50%);border-radius:50%;object-fit:cover}.pcd1210-section{padding:58px 0}.pcd1210-section-head{display:grid;grid-template-columns:minmax(0,1fr) minmax(250px,.55fr);gap:24px;align-items:end;margin-bottom:26px}.pcd1210-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:15px}.pcd1210-card{border:1px solid var(--line);border-radius:24px;padding:23px;background:linear-gradient(145deg,rgba(255,255,255,.07),rgba(255,255,255,.02))}.pcd1210-card p{margin:0;color:var(--muted);line-height:1.6}.pcd1210-num{display:grid;place-items:center;width:36px;height:36px;border-radius:50%;background:rgba(255,94,120,.14);color:var(--pink);font-weight:950;margin-bottom:30px}.pcd1210-visuals{display:grid;grid-template-columns:1.2fr .8fr;gap:15px}.pcd1210-shot{position:relative;min-height:310px;border:1px solid var(--line);border-radius:26px;overflow:hidden;background:#0c101a}.pcd1210-shot img{width:100%;height:100%;display:block;object-fit:cover}.pcd1210-shot.main{min-height:520px}.pcd1210-shot figcaption{position:absolute;left:15px;bottom:15px;padding:9px 12px;border:1px solid var(--line);border-radius:13px;background:rgba(7,9,15,.72);backdrop-filter:blur(12px);font-weight:800}.pcd1210-timeline{display:grid;gap:14px}.pcd1210-step{display:grid;grid-template-columns:120px 1fr;gap:18px;padding:20px 0;border-bottom:1px solid var(--line)}.pcd1210-step b{color:var(--pink)}.pcd1210-step p{margin:0;color:var(--muted);line-height:1.6}.pcd1210-open{display:grid;grid-template-columns:.8fr 1.2fr;gap:16px}.pcd1210-code{border:1px solid rgba(38,223,216,.25);border-radius:26px;padding:24px;background:linear-gradient(145deg,rgba(38,223,216,.08),rgba(85,112,255,.07))}.pcd1210-code pre{margin:14px 0 0;white-space:pre-wrap;color:#b8fff2;font:600 .82rem/1.65 ui-monospace,Menlo,monospace}.pcd1210-legal{border-color:rgba(67,218,143,.28);background:linear-gradient(145deg,rgba(67,218,143,.09),rgba(255,255,255,.02))}.pcd1210-footer-cta{text-align:center;padding:44px 24px;border:1px solid var(--line);border-radius:30px;background:radial-gradient(circle at 50% 0,rgba(255,94,120,.16),transparent 55%),rgba(255,255,255,.03)}.pcd1210-footer-cta .pcd1210-actions{justify-content:center}.pcd1210-meta{padding:26px 0 34px;color:var(--muted);text-align:center;font-size:.84rem}@keyframes pcdLogo{0%,100%{opacity:.7;filter:drop-shadow(0 0 0 rgba(255,94,120,0))}50%{opacity:1;filter:drop-shadow(0 0 18px rgba(255,94,120,.45))}}@media(max-width:900px){.pcd1210-hero,.pcd1210-section-head,.pcd1210-open,.pcd1210-visuals{grid-template-columns:1fr}.pcd1210-grid{grid-template-columns:1fr 1fr}.pcd1210-stage{min-height:420px}.pcd1210-shot.main{min-height:380px}}@media(max-width:600px){.pcd1210-grid{grid-template-columns:1fr}.pcd1210-wrap{width:min(calc(100% - 20px),1160px)}.pcd1210-hero{padding-top:46px}.pcd1210-step{grid-template-columns:1fr;gap:5px}.pcd1210-stage{min-height:340px}.pcd1210-story{border-radius:22px}}@media(prefers-reduced-motion:reduce){.pcd1210-logo .anim{animation:none}}.pcd1210-pocket-scene{position:absolute;inset:0;display:grid;grid-template-columns:1fr .78fr;align-items:center;gap:8px;padding:34px;pointer-events:none}.pcd1210-pocket-deck{position:relative;aspect-ratio:1;border-radius:28px;background:linear-gradient(145deg,#252d3c,#0e131e);box-shadow:0 28px 70px rgba(0,0,0,.35),inset 0 1px 0 rgba(255,255,255,.12);transform:perspective(900px) rotateX(58deg) rotateZ(-10deg)}.pcd1210-pocket-record{position:absolute;inset:14%;border-radius:50%;background:repeating-radial-gradient(circle,#05070a 0 10px,#1c222d 11px 12px);box-shadow:0 18px 38px rgba(0,0,0,.5),inset 0 0 0 2px rgba(255,255,255,.07)}.pcd1210-pocket-record:after{content:"PCd";position:absolute;inset:38%;display:grid;place-items:center;border-radius:50%;background:linear-gradient(135deg,var(--pink),var(--cyan));color:#090b13;font-weight:950}.pcd1210-pocket-phone{position:relative;width:min(220px,100%);justify-self:center;aspect-ratio:.52;border:8px solid #05070b;border-radius:34px;background:#05070b;box-shadow:0 30px 70px rgba(0,0,0,.48);transform:rotate(7deg);overflow:hidden}.pcd1210-pocket-phone:before{content:"";position:absolute;z-index:3;top:6px;left:50%;width:34%;height:14px;transform:translateX(-50%);border-radius:999px;background:#05070b}.pcd1210-pocket-phone img{width:100%;height:100%;object-fit:cover;object-position:left top;display:block}.pcd1210-pocket-caption{position:absolute;left:24px;bottom:20px;right:24px;text-align:center;color:#fff;font-weight:900;text-shadow:0 2px 12px rgba(0,0,0,.8)}@media(max-width:600px){.pcd1210-pocket-scene{padding:18px;grid-template-columns:1fr .7fr}.pcd1210-pocket-phone{border-width:5px;border-radius:22px}.pcd1210-pocket-caption{font-size:.78rem;left:12px;right:12px;bottom:10px}}
  </style>

  <div class="pcd1210-wrap">
    <header class="pcd1210-hero">
      <div>
        <div class="pcd1210-brand"><span class="pcd1210-logo"><img decoding="async" src="https://pcdlabs.presentcomposedesign.fr/experiences/platine-virtuelle/assets/icons/favicon_platine-virtuelle_PCd1210_nogradient.svg" alt="" /><img decoding="async" class="anim" src="https://pcdlabs.presentcomposedesign.fr/experiences/platine-virtuelle/assets/icons/favicon_platine-virtuelle_PCd1210_foranimatedgradient.svg" alt="" /></span><span><strong>PCd-1210</strong><span>Virtual Direct Drive</span></span></div>
        <span class="pcd1210-kicker">Étude de cas - conception à mise en ligne</span>
        <h1>Concevoir une platine vinyle dans un navigateur.</h1>
        <p class="pcd1210-lead">PCd-1210 est née d'une idée simple : garder le contrôle sur la musique que l'on écoute, même quand la source est numérique.</p>
        <p class="pcd1210-copy">J'ai piloté le projet de A à Z, de l'idée initiale à l'identité graphique, de l'architecture de l'expérience aux itérations fonctionnelles, puis jusqu'aux tests, au déploiement PWA et à la mise en ligne sur PCdlabs.</p>
        <div class="pcd1210-actions"><a class="pcd1210-btn primary" href="https://pcdlabs.presentcomposedesign.fr/experiences/platine-virtuelle/">Ouvrir PCd-1210</a><a class="pcd1210-btn" href="{{GITHUB_REPOSITORY_URL}}">Voir le code sur GitHub</a><a class="pcd1210-btn" href="https://buymeacoffee.com/presentcomposedesign">Soutenir le projet</a></div>
      </div>
      <div class="pcd1210-stage"><canvas id="pcd1210-three"></canvas><div class="pcd1210-pocket-scene" aria-label="PCd-1210, une platine virtuelle disponible sur ordinateur et dans la poche"><div class="pcd1210-pocket-deck"><div class="pcd1210-pocket-record"></div></div><div class="pcd1210-pocket-phone"><img decoding="async" src="https://pcdlabs.presentcomposedesign.fr/experiences/platine-virtuelle/assets/media/app-overview.webp" alt="PCd-1210 affichée sur smartphone" /></div><div class="pcd1210-pocket-caption">Une platine virtuelle dans votre navigateur, jusque dans votre poche.</div></div></div>
    </header>

    <section class="pcd1210-section">
      <div class="pcd1210-section-head"><div><span class="pcd1210-kicker">L'intention</span><h2>Retrouver un geste physique dans une écoute numérique.</h2></div><p class="pcd1210-copy">L'enjeu n'était pas de dessiner une simple skin de lecteur audio, mais de construire une interface où le disque, le bras, le pitch, le choix 33/45 et la bibliothèque se répondent comme un seul instrument.</p></div>
      <div class="pcd1210-grid"><article class="pcd1210-card"><span class="pcd1210-num">01</span><h3>Idée et positionnement</h3><p>Définition du concept, du vocabulaire visuel et des interactions qui donnent une vraie présence à la platine.</p></article><article class="pcd1210-card"><span class="pcd1210-num">02</span><h3>Identité graphique</h3><p>Création du branding PCd-1210, du favicon, de l'univers sombre et lumineux et des composants visuels de l'application.</p></article><article class="pcd1210-card"><span class="pcd1210-num">03</span><h3>Direction produit</h3><p>Arbitrage des fonctions, du responsive, du comportement du bras, de la discothèque et des modules optionnels.</p></article></div>
    </section>

    <section class="pcd1210-section">
      <div class="pcd1210-section-head"><div><span class="pcd1210-kicker">Le résultat</span><h2>Une PWA qui reste cohérente du desktop au téléphone.</h2></div><p class="pcd1210-copy">Le même objet passe du grand écran au smartphone : une platine virtuelle que l’on retrouve dans sa poche, avec un plateau carré, une discothèque au swipe ou au gyroscope, la recherche accessible et un branding intégrable ou isolable.</p></div>
      <div class="pcd1210-visuals"><figure class="pcd1210-shot main"><img decoding="async" src="https://pcdlabs.presentcomposedesign.fr/experiences/platine-virtuelle/assets/media/app-dark-reference.webp" alt="Vue complète de PCd-1210 en thème sombre" /><figcaption>PCd-1210 Virtual Direct Drive</figcaption></figure><div style="display:grid;gap:15px"><figure class="pcd1210-shot"><img decoding="async" src="https://pcdlabs.presentcomposedesign.fr/experiences/platine-virtuelle/assets/media/menu-integrated.webp" alt="Menu desktop avec recherche intégrée" /><figcaption>Recherche intégrée au menu</figcaption></figure><figure class="pcd1210-shot"><img decoding="async" src="https://pcdlabs.presentcomposedesign.fr/experiences/platine-virtuelle/assets/media/menu-isolated.webp" alt="Branding isolé du menu principal" /><figcaption>Branding isolable</figcaption></figure></div></div>
    </section>

    <section class="pcd1210-section">
      <div class="pcd1210-section-head"><div><span class="pcd1210-kicker">Mon rôle</span><h2>Une conception itérative, jusque dans les détails.</h2></div><p class="pcd1210-copy">Chaque version a servi à valider une hypothèse précise sans perdre les éléments déjà stabilisés.</p></div>
      <div class="pcd1210-timeline"><div class="pcd1210-step"><b>Conception</b><p>Définition du produit, hiérarchie de l'interface, analogie avec une platine réelle et choix des interactions prioritaires.</p></div><div class="pcd1210-step"><b>Design</b><p>Logo, favicon, typographie d'interface, couleurs, effets de matière, équilibre entre skeuomorphisme et interface web contemporaine.</p></div><div class="pcd1210-step"><b>Développement</b><p>Pilotage du développement HTML, CSS et JavaScript, PWA, gestion de la lecture, bibliothèque, imports, pitch, bras, scratch, gyroscope et responsive.</p></div><div class="pcd1210-step"><b>Tests</b><p>Comparaison des versions, corrections ciblées, vérification desktop/mobile, recherche des régressions et stabilisation de l'expérience.</p></div><div class="pcd1210-step"><b>Mise en ligne</b><p>Préparation du package statique, service worker, manifest, déploiement sur IONOS et documentation de reprise du projet.</p></div></div>
    </section>

    <section class="pcd1210-section">
      <div class="pcd1210-section-head"><div><span class="pcd1210-kicker">Technique</span><h2>Le navigateur comme matière de design.</h2></div><p class="pcd1210-copy">La version finale reste volontairement déployable sans compilation obligatoire. L'architecture privilégie des fichiers lisibles, un comportement progressif et la possibilité d'installer l'application comme PWA.</p></div>
      <div class="pcd1210-grid"><article class="pcd1210-card"><h3>Manipulation directe</h3><p>Le bras et le disque ne sont pas de simples illustrations. Ils répondent à la progression, au pitch, aux gestes de souris et aux interactions tactiles.</p></article><article class="pcd1210-card"><h3>Responsive</h3><p>La platine conserve sa géométrie. Les modules auxiliaires changent d'emplacement sans réduire le plateau ni couper leurs sliders.</p></article><article class="pcd1210-card"><h3>Progressive Web App</h3><p>Manifest, service worker, installation et fonctionnement web partagent la même base de code.</p></article></div>
    </section>

    <section class="pcd1210-section" id="pcd1210-open-source">
      <div class="pcd1210-section-head"><div><span class="pcd1210-kicker">Open source</span><h2>Un projet ouvert à l'inspection et à l'évolution.</h2></div><p class="pcd1210-copy">PCd-1210 v15.2 est distribuée sous licence MIT. Le code peut être étudié, modifié et redéployé dans le respect de cette licence.</p></div>
      <div class="pcd1210-open"><article class="pcd1210-card"><h3>Code disponible</h3><p>Le dépôt GitHub public doit accueillir la même base que celle déployée en production. Le ZIP source peut aussi être téléchargé directement depuis l'application.</p><div class="pcd1210-actions"><a class="pcd1210-btn primary" href="{{GITHUB_REPOSITORY_URL}}">GitHub</a><a class="pcd1210-btn" href="https://pcdlabs.presentcomposedesign.fr/experiences/platine-virtuelle/PCd-1210-source-v15.2.zip">ZIP source</a></div></article><div class="pcd1210-code"><span class="pcd1210-kicker">Stack</span><pre>HTML + CSS + JavaScript
PWA / Service Worker
YouTube iframe API
Web Audio pour les fichiers compatibles
Three.js pour les mises en scène 3D
Déploiement statique IONOS</pre></div></div>
    </section>

    <section class="pcd1210-section">
      <div class="pcd1210-section-head"><div><span class="pcd1210-kicker">Usage légal</span><h2>Lire et manipuler, sans devenir une plateforme de piratage.</h2></div><p class="pcd1210-copy">L'application est conçue comme une interface de lecture. Elle n'héberge pas un catalogue musical tiers, ne contourne pas les DRM et conserve YouTube dans son lecteur/API. Les fichiers locaux restent ceux fournis par l'utilisateur.</p></div>
      <article class="pcd1210-card pcd1210-legal"><p>Cette architecture vise un usage légal, mais elle ne peut pas transformer une source non autorisée en source autorisée. L'utilisateur reste responsable des droits attachés aux fichiers et liens qu'il choisit d'utiliser.</p></article>
    </section>

    <section class="pcd1210-section">
      <div class="pcd1210-footer-cta"><span class="pcd1210-kicker">Essayez-la</span><h2>Gardez le contrôle sur la musique que vous écoutez.</h2><p class="pcd1210-copy">PCd-1210 est accessible dans le navigateur et installable comme PWA.</p><div class="pcd1210-actions"><a class="pcd1210-btn primary" href="https://pcdlabs.presentcomposedesign.fr/experiences/platine-virtuelle/">Ouvrir l'application</a><a class="pcd1210-btn" href="https://buymeacoffee.com/presentcomposedesign">Soutenir le créateur</a><a class="pcd1210-btn" href="{{GITHUB_REPOSITORY_URL}}">Voir GitHub</a></div></div>
    </section>

    <div class="pcd1210-meta">PCd-1210 Virtual Direct Drive - Present Composé Design / PCdlabs - Alban &amp; Emilio DESBARAX - 2026</div>
  </div>

  <script type="module">
    import * as THREE from 'https://unpkg.com/three@0.160.1/build/three.module.js';
    const canvas=document.getElementById('pcd1210-three');if(canvas){const holder=canvas.parentElement;try{const renderer=new THREE.WebGLRenderer({canvas,alpha:true,antialias:true});renderer.setPixelRatio(Math.min(devicePixelRatio,1.7));const scene=new THREE.Scene();const camera=new THREE.PerspectiveCamera(36,1,.1,30);camera.position.set(0,2.5,5);camera.lookAt(0,0,0);scene.add(new THREE.AmbientLight(0xffffff,1.4));const light=new THREE.DirectionalLight(0xffd8e1,4);light.position.set(-3,5,4);scene.add(light);const rim=new THREE.PointLight(0x36e6ff,4,12);rim.position.set(4,1,2);scene.add(rim);const disc=new THREE.Mesh(new THREE.CylinderGeometry(1.65,1.65,.1,110),new THREE.MeshPhysicalMaterial({color:0x050609,roughness:.25,metalness:.45,clearcoat:1}));disc.rotation.x=Math.PI/2;scene.add(disc);const rings=new THREE.Group();for(let i=0;i<18;i++){const ring=new THREE.Mesh(new THREE.TorusGeometry(.56+i*.058,.006,7,100),new THREE.MeshBasicMaterial({color:i%4?0x242a36:0x596175,transparent:true,opacity:.5}));ring.position.z=.06;rings.add(ring)}scene.add(rings);const hub=new THREE.Mesh(new THREE.CylinderGeometry(.42,.42,.12,72),new THREE.MeshPhysicalMaterial({color:0xff5e78,roughness:.3,clearcoat:1}));hub.rotation.x=Math.PI/2;hub.position.z=.02;scene.add(hub);let px=0,py=0;holder.addEventListener('pointermove',e=>{const r=holder.getBoundingClientRect();px=((e.clientX-r.left)/r.width-.5)*2;py=((e.clientY-r.top)/r.height-.5)*2});const resize=()=>{const r=holder.getBoundingClientRect();renderer.setSize(r.width,r.height,false);camera.aspect=r.width/r.height;camera.updateProjectionMatrix()};new ResizeObserver(resize).observe(holder);resize();const reduced=matchMedia('(prefers-reduced-motion: reduce)').matches;const tick=()=>{if(!reduced){disc.rotation.z+=.004;rings.rotation.z=disc.rotation.z}disc.rotation.y+=(px*.08-disc.rotation.y)*.04;disc.rotation.x=Math.PI/2+(-py*.04-(disc.rotation.x-Math.PI/2))*.04;renderer.render(scene,camera);requestAnimationFrame(tick)};tick()}catch(e){canvas.style.display='none'}}
  </script>
</section>
				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-ae4a03d 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="ae4a03d" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
				<div class="elementor-element elementor-element-04304b8 elementor-widget elementor-widget-html" data-id="04304b8" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!-- PCd-1210 Virtual Direct Drive, PCdlabs / Present Composé Design -->
<style>
.pcd1210-project{max-width:1160px;margin:0 auto;padding:32px 20px;font-family:inherit;line-height:1.66;color:inherit}
.pcd1210-project *{box-sizing:border-box}
.pcd1210-project a{word-break:break-word}
.pcd1210-hero{padding:clamp(30px,6vw,70px);border:1px solid rgba(127,127,127,.22);border-radius:30px;background:linear-gradient(145deg,rgba(255,255,255,.07),rgba(127,127,127,.035));margin-bottom:34px}
.pcd1210-hero h1{font-size:clamp(2.4rem,6vw,5.4rem);line-height:.94;letter-spacing:-.055em;margin:0 0 18px}
.pcd1210-kicker{font-size:.82rem;letter-spacing:.16em;text-transform:uppercase;font-weight:800;opacity:.72}
.pcd1210-lead{max-width:860px;font-size:clamp(1.05rem,1.7vw,1.25rem)}
.pcd1210-links{display:flex;flex-wrap:wrap;gap:12px;margin-top:26px}
.pcd1210-links a{display:inline-flex;align-items:center;justify-content:center;min-height:48px;padding:11px 20px;border:1px solid currentColor;border-radius:999px;text-decoration:none;font-weight:800}
.pcd1210-links a:first-child{background:#ff5575;color:#fff;border-color:#ff5575}
.pcd1210-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:22px;margin:28px 0 36px}
.pcd1210-card{padding:24px;border:1px solid rgba(127,127,127,.2);border-radius:22px;background:rgba(127,127,127,.045)}
.pcd1210-card h2{margin-top:0}
.pcd1210-project h2{margin-top:38px}
.pcd1210-tech{display:flex;flex-wrap:wrap;gap:8px;padding:0;list-style:none}
.pcd1210-tech li{padding:7px 12px;border:1px solid rgba(127,127,127,.22);border-radius:999px;font-size:.88rem}
.pcd1210-note{padding:20px 22px;border-left:4px solid #ff5575;background:rgba(127,127,127,.05);border-radius:0 18px 18px 0}
@media(max-width:760px){.pcd1210-grid{grid-template-columns:1fr}.pcd1210-hero{border-radius:22px}.pcd1210-links{flex-direction:column}.pcd1210-links a{width:100%}}
</style>

<article class="pcd1210-project">
  <header class="pcd1210-hero">
    <p class="pcd1210-kicker">PCdlabs / Présent Composé Design</p>
    <h1>PCd-1210 Virtual Direct Drive</h1>
    <p class="pcd1210-lead">PCd-1210 est une platine vinyle virtuelle conçue comme une véritable expérience d'interface. Le projet transpose dans le navigateur des repères et des gestes issus de la platine physique, puis les associe à une discothèque visuelle, une playlist, un contrôle 33T/45T, un pitch, un bras dynamique, le scratch et une installation PWA.</p>
    <div class="pcd1210-links">
      <a href="https://pcdlabs.presentcomposedesign.fr/experiences/platine-virtuelle/" target="_blank" rel="noopener">Tester PCd-1210</a>
      <a href="{{GITHUB_REPOSITORY_URL}}" target="_blank" rel="noopener">Voir le projet sur GitHub</a>
      <a href="https://buymeacoffee.com/presentcomposedesign" target="_blank" rel="noopener">Sponsoriser le créateur</a>
    </div>
  </header>

  <h2>Une idée d'abord centrée sur le geste</h2>
  <p>Le point de départ n'était pas de reproduire une platine uniquement comme objet graphique. L'objectif était de construire une interface où la logique physique reste lisible : un disque qui tourne, un bras qui accompagne l'avancement d'une piste, des vitesses 33T et 45T, un pitch lié à la lecture, puis une manipulation directe du disque pour le scratch.</p>

  <div class="pcd1210-grid">
    <section class="pcd1210-card">
      <h2>Identité graphique</h2>
      <p>J'ai défini l'identité visuelle PCd-1210, le branding, les proportions générales, la hiérarchie de l'interface, les états lumineux et la cohérence entre la platine et Ma Discothèque. Le travail sur les favicons et le SVG animé prolonge cette identité jusque dans l'onglet et l'installation PWA.</p>
    </section>
    <section class="pcd1210-card">
      <h2>Conception de l'expérience</h2>
      <p>Les interactions ont été pensées autour d'actions compréhensibles sans mode d'emploi lourd : lancer ou arrêter un titre, déplacer le bras, modifier la vitesse, régler le pitch, faire défiler les pochettes, sélectionner un disque et le manipuler directement.</p>
    </section>
    <section class="pcd1210-card">
      <h2>Développement par itérations</h2>
      <p>Le projet a progressé par versions successives. Chaque étape a servi à consolider la lecture, les imports, la playlist, le bras, le pitch, le scratch, la discothèque, les modules audio et le responsive. Cette méthode a permis de conserver ce qui fonctionnait déjà tout en corrigeant des points précis.</p>
    </section>
    <section class="pcd1210-card">
      <h2>Une PWA volontairement simple à déployer</h2>
      <p>PCd-1210 repose sur HTML, CSS et JavaScript natifs. La version de production n'impose aucune compilation. Le manifest, le service worker et les ressources locales permettent un déploiement direct sur un hébergement mutualisé IONOS par FileZilla.</p>
    </section>
  </div>

  <h2>Ma Discothèque, du swipe au gyroscope</h2>
  <p>La bibliothèque n'est pas présentée comme une grille administrative. Les pochettes restent carrées et défilent horizontalement. Le swipe reste la manipulation principale sur téléphone. Un contrôle gyroscopique peut compléter ce geste : incliner le téléphone vers la gauche ou la droite déplace le rail dans le même sens, tandis que le doigt permet d'affiner immédiatement la sélection.</p>

  <h2>Le rôle de la platine virtuelle</h2>
  <p>Le plateau, le bras et le disque ne sont pas seulement décoratifs. Ils servent de représentation de la lecture. Le pitch et les modes 33T/45T influencent la vitesse, le bras suit la progression sur la zone utile du disque et le scratch permet de déplacer la lecture en manipulant directement le vinyle.</p>

  <p class="pcd1210-note"><strong>Choix technique important :</strong> les fichiers audio directs et importés peuvent passer par Web Audio pour les traitements compatibles. YouTube reste contrôlé via son lecteur et son API. L'application ne prétend pas appliquer un traitement audio impossible au flux isolé d'une iframe.</p>

  <h2>Mon rôle, de l'idée à la mise en ligne</h2>
  <p>J'ai porté le projet depuis son idée initiale jusqu'à sa mise en ligne : définition du concept, identité graphique, conception de l'expérience, choix fonctionnels, prototypage, suivi et itération du développement, tests, corrections responsive, préparation de la PWA et déploiement. PCd-1210 conserve les crédits du projet à Alban &amp; Emilio DESBARAX.</p>

  <h2>Technologies et contraintes</h2>
  <ul class="pcd1210-tech">
    <li>HTML natif</li>
    <li>CSS responsive</li>
    <li>JavaScript natif</li>
    <li>Web Audio API</li>
    <li>YouTube Player API</li>
    <li>PWA</li>
    <li>Service Worker</li>
    <li>DeviceOrientation</li>
    <li>Pointer Events</li>
    <li>IONOS mutualisé</li>
  </ul>

  <h2>Un projet encore évolutif</h2>
  <p>La priorité reste une expérience 2D rapide, cohérente et compatible avec le plus grand nombre de périphériques. Le code conserve des points d'accroche pour une future intégration d'un vrai modèle 3D interactif inspiré d'une Technics SL-1210/MK2, mais cette partie n'est pas présentée comme une fonctionnalité disponible tant qu'elle n'est pas finalisée.</p>

  <h2>Tester, consulter et soutenir</h2>
  <p>L'application peut être testée directement depuis PCdlabs. Le dépôt GitHub permettra de consulter la structure du projet et ses évolutions. Le lien Buy Me a Coffee permet de soutenir le temps consacré à la conception, aux tests et aux nouvelles expérimentations.</p>
  <div class="pcd1210-links">
    <a href="https://pcdlabs.presentcomposedesign.fr/experiences/platine-virtuelle/" target="_blank" rel="noopener">Ouvrir l'application</a>
    <a href="{{GITHUB_REPOSITORY_URL}}" target="_blank" rel="noopener">GitHub</a>
    <a href="https://buymeacoffee.com/presentcomposedesign" target="_blank" rel="noopener">Buy Me a Coffee</a>
  </div>
</article>
				</div>
				</div>
				</div>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/pcd-1210_infos/">PCd 1210 Virtual Direct Drive &#8211; infos</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Spotbaignade</title>
		<link>https://presentcomposedesign.fr/spotbaignade/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Fri, 17 Jul 2026 14:21:54 +0000</pubDate>
				<category><![CDATA[Design graphique]]></category>
		<category><![CDATA[Design produit]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=36638</guid>

					<description><![CDATA[<p>Cet article <a href="https://presentcomposedesign.fr/spotbaignade/">Spotbaignade</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Cet article <a href="https://presentcomposedesign.fr/spotbaignade/">Spotbaignade</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Yggdrasil</title>
		<link>https://presentcomposedesign.fr/yggdrasil/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Mon, 20 Apr 2026 10:39:50 +0000</pubDate>
				<category><![CDATA[Design produit]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=36494</guid>

					<description><![CDATA[<p>YGGDRASIL — PCdlab 2026 Yggdrasil Plante un arbre. Nourris-le. Sauve la planète. Commencer Nouvelle partie 🌿 Croissance 0% Graine 💧 Eau utilisée 0 L &#160; 🪣 5.2 mL/s Mode Éco Actif : Empreinte Graphique Minimale SOBRIÉTÉ ÉCO (N&#38;B) 5.2 mL/s RICHE COULEUR (DÉTAIL) 87.6 mL/s 🤲 Creuser 🌰 Planter 🫳 Recouvrir 💧 Arroser 📖 Réfléchir i × 🌳 YGGDRASIL — PCdlab 2026 Souris / Clavier 🖱️ Glisser → Orbiter autour de l&#8217;arbre ⚙️ Molette → Zoom avant / arrière Ctrl+Maj+R → Réinitialiser la partie Tactile 👆 1 doigt glissé → Orbiter 🤏 2 doigts pincés → Zoom Quest 3 — Hand Tracking ✊ Pincer (index+pouce) → Arroser 🤚 Mains visibles → Curseur goutte 3D AR → Placer l&#8217;arbre dans l&#8217;espace réel Mode Affichage 🌑 Sobriété N&#038;B = 5.2 mL/s 🌈 Riche Couleur = 87.6 mL/s PCdlab 2026 · PrésentComposédesignCode : Claude Sonnet × Anthropic 📖 La Vérité sur ton Arbre Je comprends 🌱</p>
<p>Cet article <a href="https://presentcomposedesign.fr/yggdrasil/">Yggdrasil</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="36494" class="elementor elementor-36494">
				<div class="elementor-element elementor-element-6fddc91 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="6fddc91" data-element_type="container" data-e-type="container">
					<div class="e-con-inner">
				<div class="elementor-element elementor-element-f2f4319 elementor-widget elementor-widget-html" data-id="f2f4319" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!--
╔══════════════════════════════════════════════════════════════════════════════════════════╗
║  YGGDRASIL — Plante ton arbre, sauve la planète                                          ║
║  PCdlab 2026 · PrésentComposédesign × Claude Sonnet (Anthropic)                          ║
╚══════════════════════════════════════════════════════════════════════════════════════════╝

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+#@@@@@@%. :%@@@@@@*+@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.  .#@@@*     *@@@*.  =@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%@@@@@@@@@:    #@*       *@#    +@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.  -#%@@@@@@-   #%=       +%#   -@@@@@@%*:  .@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.     .@@@@@@%*+%%    @   :%%++%@@@@@@.     .@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@*    #@@.      .%@%   =%@@*   @   *@@%-   @@#.      .@@#    *@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@=    -@#   ::  -@%     -@%:  @  :%@-     %@-  =.   %@-    *@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@%%   *@%    # -@%       @%-:@:-%@       %%: %   :%@+   %%@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@**=+**#@@@@@@+::%#@@   :.  :@@@@@@@:  ..   @@@@.:+@@@@@@#***+*#@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@=       :#@@@%*%%@@@@.  += :@@@@@@@: -=  .@@@@@%+@@@@#:       %@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@:     .%%          @@-     *@@*     =@@         *@%      -@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@=      *%-    :=: .@+        #@@@@@%:   :%@@@@@#       .*%. :=:    +@- .    =@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@+.  :*@@#+    :@*%@:        +@@@#.     .#@@@+        :@#*@:    *#@@@:   +%@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@%%%%%%@@@@@@@@@@@@@@@@    :=  -%@%        :%@#:  =.    @@@@@%%@@@@@@@@@%%%%%%@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

  ╭─────────────────────────────────────────────────────────────╮
  │  ░░░ CLAUDE SONNET — Anthropic AI ░░░                       │
  │  Code : PCdlab 2026 — WebXR / Three.js / Quest 3            │
  │  Fix : display !important override · Hand Tracking           │
  ╰─────────────────────────────────────────────────────────────╯
-->
<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="theme-color" content="#0a1008">
  <title>YGGDRASIL — PCdlab 2026</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
  <style>
    /* ═══════════════════════════════════════════════════════════════
       YGGDRASIL — CSS ISOLATION WORDPRESS / ELEMENTOR / ROYAL ADDONS
       ─ Tous les sélecteurs scoped sous #ygg-pcdb-root
       ─ !important uniquement sur les props de LAYOUT (jamais sur display
         des éléments contrôlés par JS → sinon le JS ne peut plus les cacher)
       ═══════════════════════════════════════════════════════════════ */

    html, body { margin: 0 !important; padding: 0 !important; overflow: hidden !important; }

    /* ─── ROOT ─── */
    #ygg-pcdb-root {
      position: fixed !important;
      inset: 0 !important;
      width: 100% !important; height: 100% !important;
      overflow: hidden !important;
      font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
      font-size: 14px !important; line-height: 1.5 !important;
      background: #0a1008 !important;
      z-index: 99998 !important;
      box-sizing: border-box !important;
    }
    #ygg-pcdb-root *, #ygg-pcdb-root *::before, #ygg-pcdb-root *::after {
      box-sizing: border-box !important;
      margin: 0 !important; padding: 0 !important;
      /* PAS de border:none ici — on le met par sélecteur précis */
      font-family: inherit !important;
      -webkit-tap-highlight-color: transparent !important;
    }

    /* ─── CANVAS ─── */
    #ygg-pcdb-root #ygg-canvas {
      position: absolute !important;
      inset: 0 !important; display: block !important;
      width: 100% !important; height: 100% !important;
      z-index: 1 !important;
      transition: filter 0.9s ease !important;
    }
    #ygg-pcdb-root #ygg-canvas.ygg-nb {
      filter: grayscale(1) contrast(1.06) brightness(0.95) !important;
    }

    /* ─── UI LAYER ─── */
    #ygg-pcdb-root #ygg-ui {
      position: absolute !important; inset: 0 !important;
      z-index: 10 !important; pointer-events: none !important;
    }
    #ygg-pcdb-root #ygg-ui > * { pointer-events: auto !important; }

    /* ─── CARD ─── */
    #ygg-pcdb-root .ygg-card {
      background: rgba(8,14,8,0.85) !important;
      backdrop-filter: blur(14px) !important; -webkit-backdrop-filter: blur(14px) !important;
      border: 1px solid rgba(100,160,80,0.28) !important;
      border-radius: 10px !important; padding: 10px 14px !important;
      color: #c8dfc0 !important; font-size: 12px !important;
      pointer-events: auto !important;
    }
    #ygg-pcdb-root .ygg-lbl {
      font-size: 9px !important; text-transform: uppercase !important;
      letter-spacing: 1.3px !important; color: rgba(130,185,110,0.55) !important;
      margin-bottom: 3px !important; display: block !important;
    }
    #ygg-pcdb-root .ygg-val {
      font-size: 18px !important; font-weight: 600 !important;
      color: #9de090 !important; font-family: 'Playfair Display', Georgia, serif !important;
      display: block !important;
    }
    #ygg-pcdb-root .ygg-val.blue { color: #7ec8e3 !important; }
    #ygg-pcdb-root .ygg-sub {
      font-size: 10px !important; color: rgba(150,195,130,0.45) !important;
      margin-top: 2px !important; display: block !important;
    }

    /* ─── HUD ─── */
    #ygg-pcdb-root #ygg-hud {
      position: absolute !important;
      top: 12px !important; left: 12px !important; right: 12px !important;
      display: none; /* contrôlé par JS — PAS de !important */
      justify-content: space-between !important; align-items: flex-start !important;
      z-index: 20 !important; pointer-events: none !important;
    }
    #ygg-pcdb-root #ygg-hud > * { pointer-events: auto !important; }
    #ygg-pcdb-root #ygg-growth-card { max-width: 195px !important; }
    #ygg-pcdb-root #ygg-water-card  { max-width: 195px !important; text-align: right !important; }

    /* ─── LOGO CENTRE ─── */
    #ygg-pcdb-root #ygg-logo-center { text-align: center !important; pointer-events: none !important; }
    #ygg-pcdb-root #ygg-logo-img {
      width: 68px !important; height: 68px !important; object-fit: contain !important;
      transition: filter 0.8s ease !important;
      filter: drop-shadow(0 0 8px rgba(80,180,80,0.35)) !important;
    }
    /* Logo N&B : inversion pour fond sombre */
    #ygg-pcdb-root #ygg-logo-img.ygg-nb-logo {
      filter: brightness(0) invert(1) opacity(0.85) !important;
    }
    #ygg-pcdb-root #ygg-intro-logo {
      width: 110px !important; height: 110px !important; object-fit: contain !important;
      margin-bottom: 22px !important;
      animation: ygg-float 3.2s ease-in-out infinite !important;
      /* Logo intro en N&B : inversion pour fond sombre */
      filter: brightness(0) invert(1) opacity(0.9) !important;
    }
    #ygg-pcdb-root #ygg-intro-logo.ygg-color-logo {
      filter: drop-shadow(0 0 18px rgba(80,200,80,0.35)) !important;
    }

    /* ─── TOGGLE BAR ─── */
    #ygg-pcdb-root #ygg-toggle-bar {
      position: absolute !important;
      bottom: 80px !important; left: 50% !important;
      transform: translateX(-50%) !important;
      display: none; /* contrôlé par JS */
      align-items: center !important; gap: 14px !important;
      background: rgba(6,10,6,0.9) !important;
      backdrop-filter: blur(14px) !important; -webkit-backdrop-filter: blur(14px) !important;
      border: 1px solid rgba(100,160,80,0.22) !important;
      border-radius: 14px !important; padding: 10px 18px !important;
      z-index: 20 !important; white-space: nowrap !important;
    }
    #ygg-pcdb-root .ygg-tside {
      display: flex !important; flex-direction: column !important;
      align-items: center !important; gap: 3px !important;
    }
    #ygg-pcdb-root .ygg-tlabel {
      font-size: 9px !important; text-transform: uppercase !important;
      letter-spacing: 1px !important; color: rgba(130,185,110,0.5) !important;
    }
    #ygg-pcdb-root .ygg-cdisp {
      font-size: 13px !important; font-weight: 700 !important;
      font-family: 'Playfair Display', Georgia, serif !important;
    }
    #ygg-pcdb-root .ygg-cdisp.eco { color: #7ec8a0 !important; }
    #ygg-pcdb-root .ygg-cdisp.rich { color: #e07c4a !important; }
    #ygg-pcdb-root .ygg-cunit {
      font-size: 9px !important; font-weight: 400 !important;
      font-family: 'Inter', sans-serif !important; opacity: 0.7 !important;
    }
    /* Toggle switch */
    #ygg-pcdb-root #ygg-toggle-btn {
      position: relative !important;
      width: 52px !important; height: 28px !important;
      background: rgba(30,50,30,0.8) !important;
      border: 1.5px solid rgba(100,160,80,0.5) !important;
      border-radius: 14px !important; cursor: pointer !important;
      flex-shrink: 0 !important; transition: background 0.4s !important;
    }
    #ygg-pcdb-root #ygg-toggle-btn.ygg-on {
      background: rgba(100,45,10,0.8) !important;
      border-color: rgba(200,120,60,0.5) !important;
    }
    #ygg-pcdb-root #ygg-toggle-thumb {
      position: absolute !important;
      width: 22px !important; height: 22px !important;
      top: 2px !important; left: 2px !important;
      background: rgba(100,200,100,0.9) !important;
      border-radius: 50% !important;
      transition: left 0.3s ease, background 0.4s !important;
      display: flex !important; align-items: center !important; justify-content: center !important;
      overflow: hidden !important;
    }
    #ygg-pcdb-root #ygg-toggle-btn.ygg-on #ygg-toggle-thumb {
      left: 26px !important; background: rgba(210,120,50,0.9) !important;
    }
    #ygg-pcdb-root #ygg-toggle-logo {
      width: 18px !important; height: 18px !important; object-fit: contain !important;
    }

    /* ─── CONSOMMATION ACTIVE ─── */
    #ygg-pcdb-root #ygg-active-cons {
      position: absolute !important;
      top: 12px !important; left: 50% !important; transform: translateX(-50%) !important;
      background: rgba(6,10,6,0.87) !important;
      backdrop-filter: blur(12px) !important; -webkit-backdrop-filter: blur(12px) !important;
      border: 1px solid rgba(100,160,80,0.2) !important;
      border-radius: 8px !important; padding: 6px 14px !important;
      display: none; /* contrôlé par JS */
      align-items: center !important; gap: 10px !important;
      font-size: 11px !important; color: #c8dfc0 !important;
      z-index: 20 !important; white-space: nowrap !important;
    }
    #ygg-pcdb-root #ygg-cons-icon { font-size: 16px !important; }
    #ygg-pcdb-root #ygg-cons-val {
      font-size: 16px !important; font-weight: 700 !important;
      font-family: 'Playfair Display', Georgia, serif !important;
      color: #9de090 !important; transition: color 0.5s !important;
    }
    #ygg-pcdb-root #ygg-cons-val.rich { color: #e07c4a !important; }
    #ygg-pcdb-root #ygg-cons-desc {
      font-size: 9px !important; color: rgba(150,195,130,0.5) !important;
    }

    /* ─── BOUTONS ACTION
         NOTE CRITIQUE : PAS de display !important ici.
         La visibilité est gérée par JS (inline style).
         Si on met display:flex !important, le JS ne peut plus cacher les boutons. ─── */
    #ygg-pcdb-root #ygg-actions {
      position: absolute !important;
      bottom: 20px !important; left: 50% !important; transform: translateX(-50%) !important;
      display: none; /* contrôlé par JS */
      gap: 8px !important; z-index: 20 !important;
      flex-wrap: wrap !important; justify-content: center !important;
      max-width: 90vw !important;
    }
    #ygg-pcdb-root .ygg-btn {
      background: rgba(8,14,8,0.84);
      backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
      border: 1px solid rgba(100,160,80,0.35);
      border-radius: 10px; padding: 11px 20px;
      color: #c8dfc0; font-family: 'Inter', sans-serif !important;
      font-size: 13px; font-weight: 500; cursor: pointer;
      /* display: flex sans !important → le JS peut le contrôler */
      display: flex; align-items: center; gap: 7px;
      white-space: nowrap;
      transition: background 0.22s, border-color 0.22s, transform 0.1s;
    }
    #ygg-pcdb-root .ygg-btn:hover {
      background: rgba(30,60,30,0.78);
      border-color: rgba(100,190,80,0.52);
    }
    #ygg-pcdb-root .ygg-btn:active { transform: scale(0.96); }

    /* ─── BOUTONS XR ─── */
    #ygg-pcdb-root #ygg-xr-btns {
      position: absolute !important; bottom: 20px !important; right: 12px !important;
      display: flex !important; gap: 8px !important; z-index: 20 !important;
    }
    #ygg-pcdb-root .ygg-xrbtn {
      background: rgba(8,16,26,0.84);
      backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
      border: 1px solid rgba(80,140,200,0.4);
      border-radius: 10px; padding: 11px 18px;
      color: #a0c8e8; font-family: 'Inter', sans-serif !important;
      font-size: 13px; font-weight: 500; cursor: pointer;
      transition: background 0.22s;
    }
    #ygg-pcdb-root .ygg-xrbtn:hover { background: rgba(20,40,80,0.78); }

    /* ─── MESSAGE ÉCO ─── */
    #ygg-pcdb-root #ygg-eco-msg {
      position: absolute !important;
      bottom: 140px !important; left: 50% !important; transform: translateX(-50%) !important;
      background: rgba(4,8,4,0.92) !important;
      backdrop-filter: blur(14px) !important; -webkit-backdrop-filter: blur(14px) !important;
      border: 1px solid rgba(100,160,80,0.2) !important;
      border-radius: 10px !important; padding: 14px 20px !important;
      max-width: 460px !important; width: calc(100vw - 24px) !important;
      text-align: center !important; color: #c8dfc0 !important;
      font-size: 12px !important; line-height: 1.65 !important;
      z-index: 20 !important;
      opacity: 0; pointer-events: none !important;
      transition: opacity 0.7s ease !important;
    }
    #ygg-pcdb-root #ygg-eco-msg.vis { opacity: 1 !important; }
    #ygg-pcdb-root .ygg-etitle {
      font-family: 'Playfair Display', Georgia, serif !important;
      font-size: 14px !important; color: #9de090 !important;
      margin-bottom: 6px !important; display: block !important;
    }

    /* ─── JOURNAL ─── */
    #ygg-pcdb-root #ygg-journal {
      position: absolute !important;
      top: 100px !important; right: 12px !important;
      background: rgba(8,14,8,0.8) !important;
      backdrop-filter: blur(12px) !important; -webkit-backdrop-filter: blur(12px) !important;
      border: 1px solid rgba(100,160,80,0.18) !important;
      border-radius: 10px !important; padding: 10px !important;
      width: 210px !important; max-height: 275px !important; overflow-y: auto !important;
      display: none; z-index: 20 !important;
    }
    #ygg-pcdb-root #ygg-journal::-webkit-scrollbar { width: 4px !important; }
    #ygg-pcdb-root #ygg-journal::-webkit-scrollbar-thumb {
      background: rgba(100,160,80,0.3) !important; border-radius: 2px !important;
    }
    #ygg-pcdb-root .ygg-entry {
      font-size: 10px !important; color: rgba(165,205,150,0.65) !important;
      padding: 3px 0 !important; border-bottom: 1px solid rgba(100,160,80,0.1) !important;
      line-height: 1.44 !important;
    }
    #ygg-pcdb-root .ygg-entry:last-child { border-bottom: none !important; }
    #ygg-pcdb-root .ygg-etime { color: rgba(100,160,80,0.45) !important; }

    /* ─── BOUTON INFO ─── */
    #ygg-pcdb-root #ygg-info-btn {
      position: absolute !important; bottom: 20px !important; left: 12px !important;
      width: 42px !important; height: 42px !important;
      background: rgba(8,14,8,0.84) !important;
      backdrop-filter: blur(12px) !important; -webkit-backdrop-filter: blur(12px) !important;
      border: 1px solid rgba(100,160,80,0.35) !important; border-radius: 50% !important;
      color: #9de090 !important; font-family: 'Playfair Display', Georgia, serif !important;
      font-size: 18px !important; font-style: italic !important; font-weight: 700 !important;
      cursor: pointer !important; z-index: 21 !important;
      display: flex !important; align-items: center !important; justify-content: center !important;
      transition: background 0.22s !important;
    }
    #ygg-pcdb-root #ygg-info-btn:hover { background: rgba(25,55,25,0.88) !important; }

    /* ─── PANNEAU AIDE ─── */
    #ygg-pcdb-root #ygg-help-panel {
      position: absolute !important; bottom: 72px !important; left: 12px !important;
      width: 295px !important; max-width: calc(100vw - 24px) !important;
      background: rgba(4,8,4,0.96) !important;
      backdrop-filter: blur(18px) !important; -webkit-backdrop-filter: blur(18px) !important;
      border: 1px solid rgba(100,160,80,0.26) !important;
      border-radius: 12px !important; padding: 16px !important;
      z-index: 22 !important; color: #c8dfc0 !important;
    }
    #ygg-pcdb-root #ygg-help-panel[hidden] { display: none !important; }
    #ygg-pcdb-root #ygg-help-close {
      position: absolute !important; top: 10px !important; right: 12px !important;
      background: none !important; border: none !important;
      color: rgba(150,195,130,0.5) !important;
      font-size: 20px !important; cursor: pointer !important; padding: 2px 6px !important;
    }
    #ygg-pcdb-root #ygg-help-close:hover { color: #9de090 !important; }
    #ygg-pcdb-root #ygg-help-panel h2 {
      font-family: 'Playfair Display', Georgia, serif !important;
      font-size: 15px !important; color: #9de090 !important; margin-bottom: 10px !important;
    }
    #ygg-pcdb-root #ygg-help-panel h3 {
      font-size: 9px !important; text-transform: uppercase !important;
      letter-spacing: 1.2px !important; color: rgba(130,185,110,0.5) !important;
      margin: 8px 0 5px !important;
    }
    #ygg-pcdb-root #ygg-help-panel ul { list-style: none !important; }
    #ygg-pcdb-root #ygg-help-panel li {
      font-size: 11px !important; color: rgba(185,215,170,0.75) !important;
      padding: 2px 0 !important; line-height: 1.5 !important;
    }
    #ygg-pcdb-root .ygg-credit {
      margin-top: 10px !important; font-size: 9px !important;
      color: rgba(110,155,90,0.4) !important; text-align: center !important;
      padding-top: 8px !important; border-top: 1px solid rgba(100,160,80,0.1) !important;
    }

    /* ─── FLASH PINCH XR ─── */
    #ygg-pcdb-root #ygg-pinch-flash {
      position: absolute !important; inset: 0 !important;
      background: rgba(80,200,255,0.07) !important;
      z-index: 18 !important; pointer-events: none !important;
      opacity: 0 !important; transition: opacity 0.12s !important;
    }
    #ygg-pcdb-root #ygg-pinch-flash.fl { opacity: 1 !important; }

    /* ─── INTRO ─── */
    #ygg-pcdb-root #ygg-intro {
      position: absolute !important; inset: 0 !important;
      background: radial-gradient(ellipse at 50% 75%, #182a1a 0%, #050c05 70%) !important;
      z-index: 50 !important;
      display: flex !important; flex-direction: column !important;
      align-items: center !important; justify-content: center !important;
      text-align: center !important; padding: 30px !important;
      transition: opacity 1.1s ease !important;
    }
    #ygg-pcdb-root #ygg-intro.ygg-out {
      opacity: 0 !important; pointer-events: none !important;
    }
    #ygg-pcdb-root #ygg-intro h1 {
      font-family: 'Playfair Display', Georgia, serif !important;
      font-size: 46px !important; color: #9de090 !important;
      letter-spacing: 3px !important; margin-bottom: 6px !important;
    }
    #ygg-pcdb-root .ygg-sub-intro {
      font-size: 13px !important; font-style: italic !important;
      color: rgba(165,205,150,0.5) !important; margin-bottom: 34px !important;
    }
    #ygg-pcdb-root #ygg-start-btn {
      background: rgba(35,72,35,0.45) !important;
      border: 1px solid rgba(100,190,80,0.45) !important;
      border-radius: 12px !important; padding: 16px 44px !important;
      color: #9de090 !important; font-family: 'Playfair Display', Georgia, serif !important;
      font-size: 18px !important; cursor: pointer !important;
      transition: background 0.28s !important; margin-bottom: 14px !important;
    }
    #ygg-pcdb-root #ygg-start-btn:hover { background: rgba(45,90,45,0.58) !important; }
    #ygg-pcdb-root #ygg-reset-btn {
      font-size: 10px !important; color: rgba(110,155,90,0.36) !important;
      cursor: pointer !important; background: none !important; border: none !important;
      text-decoration: underline !important; text-underline-offset: 3px !important;
    }

    /* ─── MORAL ─── */
    #ygg-pcdb-root #ygg-moral {
      position: absolute !important; inset: 0 !important;
      background: rgba(2,4,2,0.96) !important;
      z-index: 60 !important;
      display: flex !important; flex-direction: column !important;
      align-items: center !important; justify-content: center !important;
      padding: 30px !important; text-align: center !important; overflow-y: auto !important;
    }
    #ygg-pcdb-root #ygg-moral[hidden] { display: none !important; }
    #ygg-pcdb-root #ygg-moral h2 {
      font-family: 'Playfair Display', Georgia, serif !important;
      font-size: 25px !important; color: #e6a8a8 !important; margin-bottom: 16px !important;
    }
    #ygg-pcdb-root #ygg-moral-text {
      font-size: 13px !important; color: rgba(215,195,185,0.75) !important;
      max-width: 500px !important; line-height: 1.8 !important; margin-bottom: 12px !important;
    }
    #ygg-pcdb-root .ygg-mstat {
      font-size: 17px !important; color: #7ec8e3 !important;
      font-weight: 600 !important; margin: 12px 0 !important; line-height: 1.7 !important;
    }
    #ygg-pcdb-root #ygg-moral-advice {
      font-size: 13px !important; color: rgba(215,195,185,0.65) !important;
      max-width: 500px !important; line-height: 1.8 !important; margin-bottom: 20px !important;
    }

    /* ─── ANIMATIONS ─── */
    @keyframes ygg-float {
      0%, 100% { transform: translateY(0); }
      50% { transform: translateY(-9px); }
    }
  </style>
</head>
<body>
<div id="ygg-pcdb-root">

  <canvas id="ygg-canvas"></canvas>

  <div id="ygg-ui">

    <!-- INTRO -->
    <div id="ygg-intro">
      <img decoding="async" id="ygg-intro-logo" src="https://presentcomposedesign.fr/wp-content/uploads/2026/04/YGGDRASIL-LOGO_v1stroke_PCdlab_2026.svg" alt="Yggdrasil">
      <h1>Yggdrasil</h1>
      <p class="ygg-sub-intro">Plante un arbre. Nourris-le. Sauve la planète.</p>
      <button id="ygg-start-btn" type="button">Commencer</button>
      <button id="ygg-reset-btn" type="button">Nouvelle partie</button>
    </div>

    <!-- HUD -->
    <div id="ygg-hud">
      <div class="ygg-card" id="ygg-growth-card">
        <span class="ygg-lbl">🌿 Croissance</span>
        <span class="ygg-val" id="ygg-growth-val">0%</span>
        <span class="ygg-sub" id="ygg-stage-name">Graine</span>
      </div>
      <div id="ygg-logo-center">
        <img decoding="async" id="ygg-logo-img"
          src="https://presentcomposedesign.fr/wp-content/uploads/2026/04/YGGDRASIL-LOGO_v1stroke_PCdlab_2026.svg"
          alt="YGGDRASIL" class="ygg-nb-logo">
      </div>
      <div class="ygg-card" id="ygg-water-card">
        <span class="ygg-lbl">💧 Eau utilisée</span>
        <span class="ygg-val blue" id="ygg-water-val">0 L</span>
        <span class="ygg-sub" id="ygg-water-eq">&nbsp;</span>
      </div>
    </div>

    <!-- CONSOMMATION ACTIVE -->
    <div id="ygg-active-cons">
      <span id="ygg-cons-icon">🪣</span>
      <span id="ygg-cons-val">5.2 mL/s</span>
      <span id="ygg-cons-desc">Mode Éco Actif : Empreinte Graphique Minimale</span>
    </div>

    <!-- TOGGLE -->
    <div id="ygg-toggle-bar">
      <div class="ygg-tside">
        <span class="ygg-tlabel">SOBRIÉTÉ ÉCO (N&amp;B)</span>
        <span class="ygg-cdisp eco"><span id="ygg-eco-v">5.2</span><span class="ygg-cunit"> mL/s</span></span>
      </div>
      <button id="ygg-toggle-btn" type="button" role="switch" aria-checked="false" aria-label="Mode affichage">
        <span id="ygg-toggle-thumb">
          <img decoding="async" id="ygg-toggle-logo"
            src="https://presentcomposedesign.fr/wp-content/uploads/2026/04/YGGDRASIL-LOGO_v1_PCdlab_2026.svg"
            alt="" aria-hidden="true">
        </span>
      </button>
      <div class="ygg-tside">
        <span class="ygg-tlabel">RICHE COULEUR (DÉTAIL)</span>
        <span class="ygg-cdisp rich"><span id="ygg-rich-v">87.6</span><span class="ygg-cunit"> mL/s</span></span>
      </div>
    </div>

    <!-- JOURNAL -->
    <div id="ygg-journal" aria-live="polite"></div>

    <!-- ACTIONS — boutons cachés/montrés individuellement par JS -->
    <div id="ygg-actions">
      <button class="ygg-btn" id="ygg-btn-dig"   type="button">🤲 Creuser</button>
      <button class="ygg-btn" id="ygg-btn-plant" type="button" style="display:none">🌰 Planter</button>
      <button class="ygg-btn" id="ygg-btn-cover" type="button" style="display:none">🫳 Recouvrir</button>
      <button class="ygg-btn" id="ygg-btn-water" type="button" style="display:none">💧 Arroser</button>
      <button class="ygg-btn" id="ygg-btn-moral" type="button" style="display:none">📖 Réfléchir</button>
    </div>

    <!-- XR (injectés si disponibles) -->
    <div id="ygg-xr-btns"></div>

    <!-- ECO MSG -->
    <div id="ygg-eco-msg" role="status" aria-live="polite"></div>

    <!-- FLASH XR -->
    <div id="ygg-pinch-flash"></div>

    <!-- BOUTON INFO -->
    <button id="ygg-info-btn" type="button" aria-label="Aide et commandes" aria-expanded="false">i</button>

    <!-- PANNEAU AIDE -->
    <div id="ygg-help-panel" role="dialog" aria-label="Aide" hidden>
      <button id="ygg-help-close" type="button" aria-label="Fermer">×</button>
      <h2>🌳 YGGDRASIL — PCdlab 2026</h2>
      <h3>Souris / Clavier</h3>
      <ul>
        <li>🖱️ Glisser → Orbiter autour de l'arbre</li>
        <li>⚙️ Molette → Zoom avant / arrière</li>
        <li>Ctrl+Maj+R → Réinitialiser la partie</li>
      </ul>
      <h3>Tactile</h3>
      <ul>
        <li>👆 1 doigt glissé → Orbiter</li>
        <li>🤏 2 doigts pincés → Zoom</li>
      </ul>
      <h3>Quest 3 — Hand Tracking</h3>
      <ul>
        <li>✊ Pincer (index+pouce) → Arroser</li>
        <li>🤚 Mains visibles → Curseur goutte 3D</li>
        <li>AR → Placer l'arbre dans l'espace réel</li>
      </ul>
      <h3>Mode Affichage</h3>
      <ul>
        <li>🌑 Sobriété N&B = <strong>5.2 mL/s</strong></li>
        <li>🌈 Riche Couleur = <strong>87.6 mL/s</strong></li>
      </ul>
      <p class="ygg-credit">PCdlab 2026 · PrésentComposédesign<br>Code : Claude Sonnet × Anthropic</p>
    </div>

    <!-- MORAL -->
    <div id="ygg-moral" role="dialog" aria-modal="true" hidden>
      <h2>📖 La Vérité sur ton Arbre</h2>
      <p id="ygg-moral-text"></p>
      <div class="ygg-mstat" id="ygg-moral-stat"></div>
      <p id="ygg-moral-advice"></p>
      <button class="ygg-btn" id="ygg-close-moral" type="button">Je comprends 🌱</button>
    </div>

  </div><!-- #ygg-ui -->
</div><!-- #ygg-pcdb-root -->

<script type="module">
/* ══════════════════════════════════════════════════════════════════════
   YGGDRASIL — PCdlab 2026
   Three.js 0.161 · WebXR · Hand Tracking Quest 3
   Bug Fix : display !important removed from .ygg-btn
             → JS peut maintenant gérer la visibilité des boutons
   ══════════════════════════════════════════════════════════════════════ */

import * as THREE from 'https://unpkg.com/three@0.161.0/build/three.module.js';

/* ─── CONSTANTES ─── */
const SAVE_KEY    = 'ygg_pcdb_2026';
const WATER_PAGE  = 0.5;
const WATER_ARR   = 1.5;
const SHOWER_LPM  = 12;
const CONS_ECO    = 5.2;
const CONS_RICH   = 87.6;

const LOGO_GRADIENT = 'https://presentcomposedesign.fr/wp-content/uploads/2026/04/YGGDRASIL-LOGO_gradient_PCdlab_2026.svg';
const LOGO_STROKE   = 'https://presentcomposedesign.fr/wp-content/uploads/2026/04/YGGDRASIL-LOGO_v1stroke_PCdlab_2026.svg';
const LOGO_MAIN     = 'https://presentcomposedesign.fr/wp-content/uploads/2026/04/YGGDRASIL-LOGO_v1_PCdlab_2026.svg';

const GOLDEN_ANGLE = Math.PI * 2 * (1 - 2 / (1 + Math.sqrt(5)));
const FIB = [2, 3, 5, 8, 13, 21, 34];

const STAGES = [
  { name:'Graine',       min:0  },
  { name:'Germination',  min:5  },
  { name:'Jeune pousse', min:15 },
  { name:'Arbuste',      min:30 },
  { name:'Jeune arbre',  min:50 },
  { name:'Arbre mature', min:70 },
  { name:'Yggdrasil',   min:90 },
];

const ECO_FACTS = [
  "🚿 Une douche de 5 min = 60 litres.",
  "🥩 1 kg de bœuf = 15 400 litres d'eau.",
  "👕 Un t-shirt coton = 2 700 litres.",
  "🌾 L'agriculture utilise 70% de l'eau douce mondiale.",
  "💧 2,2 milliards sans accès à l'eau potable.",
  "🌍 La moitié du monde en stress hydrique d'ici 2025.",
  "☕ Une tasse de café = 140 litres produits.",
  "📱 Un smartphone = 12 000 litres fabriqué.",
  "🥑 Un avocat = 320 litres d'eau.",
  "🏠 Un Français consomme ~150 L/jour.",
];

/* ─── ÉTAT ─── */
function defaultState() {
  return {
    phase:'start', growth:0, totalWater:0,
    waterCount:0, pageLoads:0, pageWater:0,
    lastWaterTime:null, createdAt:Date.now(),
    journal:[], overwatered:false,
  };
}
function loadState() {
  try { const d = JSON.parse(localStorage.getItem(SAVE_KEY)); if (d?.phase) return d; } catch(e){}
  return defaultState();
}
let gs = loadState();
const save = () => localStorage.setItem(SAVE_KEY, JSON.stringify(gs));

gs.pageLoads = (gs.pageLoads||0) + 1;
gs.pageWater = (gs.pageWater||0) + WATER_PAGE;
save();

/* ─── TOGGLE STATE ─── */
let isColor = false;

/* ─── THREE.JS ─── */
const canvas = document.getElementById('ygg-canvas');

const renderer = new THREE.WebGLRenderer({ canvas, antialias:true, alpha:false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.95;
renderer.xr.enabled = true;

const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x162617, 0.013);

const camera = new THREE.PerspectiveCamera(50, window.innerWidth/window.innerHeight, 0.1, 200);

/* worldGroup : tout l'environnement. Déplacé en XR pour positionner l'utilisateur */
const worldGroup = new THREE.Group();
scene.add(worldGroup);

/* ─── LUMIÈRES ─── */
scene.add(new THREE.AmbientLight(0x3a5a3a, 0.72));

const sun = new THREE.DirectionalLight(0xffe8c0, 1.28);
sun.position.set(8, 15, 5);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
sun.shadow.camera.near = 0.5; sun.shadow.camera.far = 60;
sun.shadow.camera.left = -18; sun.shadow.camera.right = 18;
sun.shadow.camera.top  = 18; sun.shadow.camera.bottom = -18;
sun.shadow.bias = -0.001; sun.shadow.normalBias = 0.02;
scene.add(sun);

const rim = new THREE.DirectionalLight(0x88aaff, 0.32);
rim.position.set(-5, 8, -5);
scene.add(rim);

/* ─── CIEL (dégradé shader) ─── */
const skyMat = new THREE.ShaderMaterial({
  side: THREE.BackSide,
  uniforms: {
    topColor:    { value: new THREE.Color(0x0a1628) },
    bottomColor: { value: new THREE.Color(0x2a4a2a) },
    offset:  { value: 20 }, exponent: { value: 0.5 },
  },
  vertexShader:`
    varying vec3 vWP;
    void main(){ vWP=(modelMatrix*vec4(position,1.0)).xyz; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }`,
  fragmentShader:`
    uniform vec3 topColor,bottomColor; uniform float offset,exponent; varying vec3 vWP;
    void main(){float h=normalize(vWP+vec3(0,offset,0)).y; gl_FragColor=vec4(mix(bottomColor,topColor,max(pow(max(h,0.0),exponent),0.0)),1.0);}`,
});
worldGroup.add(new THREE.Mesh(new THREE.SphereGeometry(100,48,48), skyMat));

/* ─── SOL ─── */
const gnd = new THREE.Mesh(
  new THREE.PlaneGeometry(60,60,80,80),
  new THREE.MeshStandardMaterial({ color:0x2a5a1a, roughness:0.9 })
);
gnd.rotation.x = -Math.PI/2; gnd.receiveShadow = true;
worldGroup.add(gnd);

const soil = new THREE.Mesh(
  new THREE.CircleGeometry(1.5,48),
  new THREE.MeshStandardMaterial({ color:0x3a2a1a, roughness:1 })
);
soil.rotation.x = -Math.PI/2; soil.position.set(0,0.01,0); soil.receiveShadow = true;
worldGroup.add(soil);

/* ─── HERBE INSTANCIÉE ─── */
const GRASS_N = 10000;
const grassGeo = new THREE.PlaneGeometry(0.08, 0.5, 1, 3);
grassGeo.translate(0, 0.25, 0);

const grassMat = new THREE.ShaderMaterial({
  uniforms:{ time:{value:0}, wind:{value:0.3} },
  vertexShader:`
    uniform float time,wind;
    attribute float phase,ht;
    varying float vH;
    void main(){
      vH=position.y/0.5;
      vec3 p=position;
      p.x+=sin(time*2.0+phase)*wind*p.y*p.y + cos(time*1.3+phase*0.7)*wind*0.5*p.y;
      p.z+=cos(time*1.7+phase*1.3)*wind*0.3*p.y;
      p.x*=(1.0-vH*0.6); p.z*=(1.0-vH*0.6);
      gl_Position=projectionMatrix*modelViewMatrix*instanceMatrix*vec4(p*vec3(1,ht,1),1.0);
    }`,
  fragmentShader:`
    varying float vH;
    void main(){
      vec3 c=mix(vec3(0.14,0.34,0.09),vec3(0.3,0.6,0.14),vH);
      c=mix(c,vec3(0.38,0.68,0.18),vH*vH*0.5);
      gl_FragColor=vec4(c*mix(0.5,1.0,vH),1.0);
    }`,
  side:THREE.DoubleSide,
});

const grassMesh = new THREE.InstancedMesh(grassGeo, grassMat, GRASS_N);
const _pha = new Float32Array(GRASS_N);
const _ht  = new Float32Array(GRASS_N);
const _d   = new THREE.Object3D();
for(let i=0;i<GRASS_N;i++){
  const a=Math.random()*Math.PI*2, r=Math.random()*24+2;
  _d.position.set(Math.cos(a)*r, 0, Math.sin(a)*r);
  _d.rotation.set(0, Math.random()*Math.PI, 0);
  _d.scale.set(0.8+Math.random()*0.5, 1, 1);
  _d.updateMatrix();
  grassMesh.setMatrixAt(i, _d.matrix);
  _pha[i] = Math.random()*Math.PI*2;
  _ht[i]  = 0.6+Math.random()*0.8;
}
grassGeo.setAttribute('phase', new THREE.InstancedBufferAttribute(_pha,1));
grassGeo.setAttribute('ht',    new THREE.InstancedBufferAttribute(_ht,1));
grassMesh.instanceMatrix.needsUpdate = true;
worldGroup.add(grassMesh);

/* ─── FLEURS ─── */
const fCols = [0xff6b9d,0xffd93d,0xc084fc,0xffffff,0xff8fab];
for(let i=0;i<34;i++){
  const a=Math.random()*Math.PI*2, r=3+Math.random()*18;
  const fg = new THREE.Group();
  const stem = new THREE.Mesh(
    new THREE.CylinderGeometry(0.01,0.01,0.2,4),
    new THREE.MeshStandardMaterial({color:0x2e7d32})
  );
  stem.position.y=0.1; fg.add(stem);
  const petal = new THREE.Mesh(
    new THREE.SphereGeometry(0.05,6,6),
    new THREE.MeshStandardMaterial({color:fCols[i%fCols.length]})
  );
  petal.position.y=0.22; fg.add(petal);
  fg.position.set(Math.cos(a)*r, 0, Math.sin(a)*r);
  worldGroup.add(fg);
}

/* ─── CAILLOUX ─── */
for(let i=0;i<20;i++){
  const a=Math.random()*Math.PI*2, r=2+Math.random()*14;
  const pg = new THREE.SphereGeometry(0.05+Math.random()*0.08,6,6);
  pg.scale(1, 0.5, 1+Math.random()*0.3);
  const pm = new THREE.Mesh(pg, new THREE.MeshStandardMaterial({
    color: new THREE.Color().setHSL(0.08,0.1,0.3+Math.random()*0.2), roughness:0.95
  }));
  pm.position.set(Math.cos(a)*r, 0.02, Math.sin(a)*r);
  worldGroup.add(pm);
}

/* ─── GRAINE ─── */
const seedGeo = new THREE.SphereGeometry(0.12,16,16);
seedGeo.scale(1, 0.7, 0.8); // scale sur la géométrie (method correcte)
const seed = new THREE.Mesh(seedGeo, new THREE.MeshStandardMaterial({color:0x8B6914,roughness:0.8}));
seed.position.set(0, 0.5, 3);
seed.castShadow = true; seed.visible = false;
worldGroup.add(seed);

/* ─── TROU ─── */
const hole = new THREE.Mesh(
  new THREE.CylinderGeometry(0.4,0.3,0.3,32),
  new THREE.MeshStandardMaterial({color:0x1a0f05,roughness:1})
);
hole.position.set(0,-0.15,0); hole.visible = false;
worldGroup.add(hole);

/* ─── ARBRE ─── */
const treeGroup = new THREE.Group();
worldGroup.add(treeGroup);
let treeObjs = [];

/* ─── LUCIOLES ─── */
const ffN = 28;
const ffGeo = new THREE.BufferGeometry();
const ffPos = new Float32Array(ffN*3);
const ffPha = new Float32Array(ffN);
for(let i=0;i<ffN;i++){
  ffPos[i*3]=(Math.random()-.5)*18; ffPos[i*3+1]=0.5+Math.random()*4; ffPos[i*3+2]=(Math.random()-.5)*18;
  ffPha[i]=Math.random()*Math.PI*2;
}
ffGeo.setAttribute('position',new THREE.BufferAttribute(ffPos,3));
const ffMat = new THREE.PointsMaterial({color:0xffee88,size:0.1,transparent:true,opacity:0.55});
worldGroup.add(new THREE.Points(ffGeo,ffMat));
const fireflies = worldGroup.children[worldGroup.children.length-1];

/* ─── ARROSOIR ─── */
const canGroup = new THREE.Group();
canGroup.visible = false;
const canMat = new THREE.MeshStandardMaterial({color:0x4a7a8a,roughness:0.5,metalness:0.3});
const canBody = new THREE.Mesh(new THREE.CylinderGeometry(0.25,0.3,0.5,12), canMat);
canGroup.add(canBody);
const spout = new THREE.Mesh(new THREE.CylinderGeometry(0.04,0.06,0.4,8), canMat);
spout.position.set(0.2,0.15,0); spout.rotation.z=-0.8;
canGroup.add(spout);
canGroup.position.set(1.5,3,2); canGroup.rotation.z=-0.3;
worldGroup.add(canGroup);

/* ─── PARTICULES EAU ─── */
let waterP = null;
let waterA = {active:false,time:0};

(function initWater(){
  const n=200, geo=new THREE.BufferGeometry();
  const pos=new Float32Array(n*3), vel=new Float32Array(n*3);
  for(let i=0;i<n;i++){
    pos[i*3]=(Math.random()-.5)*1.5; pos[i*3+1]=3+Math.random()*2; pos[i*3+2]=(Math.random()-.5)*1.5;
    vel[i*3]=(Math.random()-.5)*.02; vel[i*3+1]=-.05-Math.random()*.08; vel[i*3+2]=(Math.random()-.5)*.02;
  }
  geo.setAttribute('position',new THREE.BufferAttribute(pos,3));
  geo.userData.velocity=vel;
  waterP = new THREE.Points(geo, new THREE.PointsMaterial({color:0x4dc8ff,size:.06,transparent:true,opacity:.7}));
  waterP.visible=false;
  worldGroup.add(waterP); // dans worldGroup pour coïncider avec l'arbre en XR
})();

function startWaterAnim(){
  waterA={active:true,time:0}; waterP.visible=true;
  const pos=waterP.geometry.attributes.position;
  for(let i=0;i<pos.count;i++) pos.setXYZ(i,(Math.random()-.5)*1.5,3+Math.random()*2,(Math.random()-.5)*1.5);
  pos.needsUpdate=true;
}

/* ─── RETICLE AR ─── */
const reticle = new THREE.Mesh(
  new THREE.RingGeometry(0.15,0.2,32).rotateX(-Math.PI/2),
  new THREE.MeshBasicMaterial({color:0x00ffbf})
);
reticle.matrixAutoUpdate=false; reticle.visible=false;
scene.add(reticle);

/* ─── HAND TRACKING QUEST 3 ─── */
const XR_JOINTS = [
  'wrist',
  'thumb-metacarpal','thumb-phalanx-proximal','thumb-phalanx-distal','thumb-tip',
  'index-finger-metacarpal','index-finger-phalanx-proximal','index-finger-phalanx-intermediate','index-finger-phalanx-distal','index-finger-tip',
  'middle-finger-metacarpal','middle-finger-phalanx-proximal','middle-finger-phalanx-intermediate','middle-finger-phalanx-distal','middle-finger-tip',
  'ring-finger-metacarpal','ring-finger-phalanx-proximal','ring-finger-phalanx-intermediate','ring-finger-phalanx-distal','ring-finger-tip',
  'pinky-finger-metacarpal','pinky-finger-phalanx-proximal','pinky-finger-phalanx-intermediate','pinky-finger-phalanx-distal','pinky-finger-tip',
];

const jGeo = new THREE.SphereGeometry(0.007,6,6);
const jMat = new THREE.MeshStandardMaterial({color:0xe8c8a0,roughness:0.6});
const handMaps = [{},{}]; // joint name → Mesh, per hand
for(let h=0;h<2;h++){
  for(const name of XR_JOINTS){
    const m=new THREE.Mesh(jGeo,jMat);
    m.visible=false; scene.add(m);
    handMaps[h][name]=m;
  }
}
const dropCursor = new THREE.Mesh(
  new THREE.SphereGeometry(0.022,8,8),
  new THREE.MeshStandardMaterial({color:0x4dc8ff,transparent:true,opacity:0.82})
);
dropCursor.visible=false; scene.add(dropCursor);

const xrHand0 = renderer.xr.getHand(0);
const xrHand1 = renderer.xr.getHand(1);
scene.add(xrHand0); scene.add(xrHand1);

// AR controller pour placement
const arCtrl = renderer.xr.getController(0);
arCtrl.addEventListener('select',()=>{
  if(reticle.visible){ treeGroup.position.setFromMatrixPosition(reticle.matrix); treeGroup.updateMatrixWorld(true); }
});
scene.add(arCtrl);

let pinchWas0=false, pinchWas1=false, lastPinch=0;

function pinchDist(hand){
  const a=hand.joints?.['index-finger-tip'], b=hand.joints?.['thumb-tip'];
  return (a&&b&&a.jointRadius!==undefined&&b.jointRadius!==undefined)
    ? a.position.distanceTo(b.position) : Infinity;
}
function updateHandViz(hand, map, isRight){
  if(!hand.joints){ Object.values(map).forEach(m=>m.visible=false); if(isRight)dropCursor.visible=false; return; }
  for(const [n,m] of Object.entries(map)){
    const j=hand.joints[n];
    if(j&&j.jointRadius!==undefined){ m.visible=true; m.position.copy(j.position); m.quaternion.copy(j.quaternion); }
    else { m.visible=false; }
  }
  if(isRight){
    const tip=hand.joints['index-finger-tip'];
    if(tip&&tip.jointRadius!==undefined){
      dropCursor.visible=true; dropCursor.position.copy(tip.position); dropCursor.position.y-=0.03;
    } else { dropCursor.visible=false; }
  }
}

/* ─── XR BOUTONS CUSTOM ─── */
let xrSession=null;
async function initXR(){
  if(!navigator.xr) return;
  const c=document.getElementById('ygg-xr-btns');
  try{ if(await navigator.xr.isSessionSupported('immersive-vr')){ const b=mkXRBtn('🥽 VR',()=>enterXR('immersive-vr')); c.appendChild(b); } }catch(e){}
  try{ if(await navigator.xr.isSessionSupported('immersive-ar')){ const b=mkXRBtn('📷 AR',()=>enterXR('immersive-ar')); c.appendChild(b); } }catch(e){}
}
function mkXRBtn(label,fn){
  const b=document.createElement('button');
  b.className='ygg-xrbtn'; b.textContent=label; b.addEventListener('click',fn); return b;
}
async function enterXR(mode){
  if(xrSession){ try{await xrSession.end();}catch(e){} return; }
  const opts={
    requiredFeatures:['local-floor'],
    optionalFeatures:['hand-tracking', mode==='immersive-ar'?'hit-test':'bounded-floor'],
  };
  if(mode==='immersive-ar'){
    opts.optionalFeatures.push('dom-overlay');
    opts.domOverlay={root:document.getElementById('ygg-ui')};
  }
  try{
    const s=await navigator.xr.requestSession(mode,opts);
    xrSession=s; renderer.xr.setSession(s);
    s.addEventListener('end',()=>{ xrSession=null; worldGroup.position.set(0,0,0); updateCam(); });
    // Décale l'utilisateur à 5m de l'arbre (évite de spawner dans le tronc)
    try{
      const rs=await s.requestReferenceSpace('local-floor');
      const off=new XRRigidTransform({x:0,y:0,z:-5,w:1});
      renderer.xr.setReferenceSpace(rs.getOffsetReferenceSpace(off));
    }catch(e){ worldGroup.position.z=-5; }
  }catch(err){ console.warn('XR failed:',err); }
}

let hitSrc=null, hitSrcReq=false;

/* ─── CAMÉRA ORBITE (desktop/mobile) ─── */
let drag=false, pmx=0, pmy=0, azimuth=0, polar=0.5, camR=12;
function updateCam(){
  if(renderer.xr.isPresenting) return;
  camera.position.set(camR*Math.sin(polar)*Math.cos(azimuth), camR*Math.cos(polar), camR*Math.sin(polar)*Math.sin(azimuth));
  camera.lookAt(0,2,0);
}
updateCam();

/* ─── LOGIQUE JEU ─── */
const getStage = g => { let s=STAGES[0]; for(const st of STAGES) if(g>=st.min) s=st; return s; };
const fmtW = l => l<1?`${(l*1000).toFixed(0)} mL`:`${l.toFixed(1)} L`;
const wEq  = l => { const s=(l/SHOWER_LPM)*60; return s<60?`≈ ${s.toFixed(0)}s de douche`:`≈ ${(s/60).toFixed(1)}min de douche`; };
const rEco = () => ECO_FACTS[Math.floor(Math.random()*ECO_FACTS.length)];

function addJournal(txt){
  const d=new Date(), t=`${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
  gs.journal.unshift({time:t,text:txt});
  if(gs.journal.length>50) gs.journal.pop();
  save(); renderJournal();
}
function renderJournal(){
  document.getElementById('ygg-journal').innerHTML =
    gs.journal.map(e=>`<div class="ygg-entry"><span class="ygg-etime">${e.time}</span> ${e.text}</div>`).join('');
}

function showEco(title,text,dur=6000){
  const el=document.getElementById('ygg-eco-msg');
  el.innerHTML=`<span class="ygg-etitle">${title}</span>${text}`;
  el.classList.add('vis');
  clearTimeout(el._t); el._t=setTimeout(()=>el.classList.remove('vis'),dur);
}

function updateUI(){
  const st=getStage(gs.growth);
  document.getElementById('ygg-growth-val').textContent = `${Math.floor(gs.growth)}%`;
  document.getElementById('ygg-stage-name').textContent = st.name;
  document.getElementById('ygg-water-val').textContent  = fmtW(gs.totalWater);
  document.getElementById('ygg-water-eq').textContent   = wEq(gs.totalWater);
  if(gs.lastWaterTime){
    const mn=Math.floor((Date.now()-gs.lastWaterTime)/60000);
    const hr=Math.floor(mn/60), dy=Math.floor(hr/24);
    const ts=mn<1?"à l'instant":dy>0?`il y a ${dy}j`:hr>0?`il y a ${hr}h`:`il y a ${mn}min`;
    document.getElementById('ygg-water-eq').textContent=ts;
  }
}

/* ─── MODE AFFICHAGE ─── */
function setDisplayMode(color){
  isColor=color;
  const btn=document.getElementById('ygg-toggle-btn');
  btn.setAttribute('aria-checked', color?'true':'false');
  btn.classList.toggle('ygg-on', color);

  const logoImg  = document.getElementById('ygg-logo-img');
  const introLgo = document.getElementById('ygg-intro-logo');
  const togLgo   = document.getElementById('ygg-toggle-logo');

  if(color){
    canvas.classList.remove('ygg-nb');
    renderer.toneMappingExposure=1.1;
    skyMat.uniforms.topColor.value.set(0x0a1628);
    skyMat.uniforms.bottomColor.value.set(0x2a4a2a);
    logoImg.src=LOGO_GRADIENT; logoImg.classList.remove('ygg-nb-logo');
    introLgo.src=LOGO_GRADIENT; introLgo.classList.add('ygg-color-logo');
    togLgo.src=LOGO_GRADIENT;
    const cv=document.getElementById('ygg-cons-val');
    cv.classList.add('rich');
    animateCons(cv,CONS_RICH);
    document.getElementById('ygg-cons-desc').textContent='Mode Riche Actif : Empreinte Graphique Maximale';
    document.getElementById('ygg-cons-icon').textContent='💻';
  } else {
    canvas.classList.add('ygg-nb');
    renderer.toneMappingExposure=0.75;
    skyMat.uniforms.topColor.value.set(0x0d1520);
    skyMat.uniforms.bottomColor.value.set(0x1e2a1e);
    logoImg.src=LOGO_STROKE; logoImg.classList.add('ygg-nb-logo');
    introLgo.src=LOGO_STROKE; introLgo.classList.remove('ygg-color-logo');
    togLgo.src=LOGO_MAIN;
    const cv=document.getElementById('ygg-cons-val');
    cv.classList.remove('rich');
    animateCons(cv,CONS_ECO);
    document.getElementById('ygg-cons-desc').textContent='Mode Éco Actif : Empreinte Graphique Minimale';
    document.getElementById('ygg-cons-icon').textContent='🪣';
  }
}

function animateCons(el,target){
  const from=parseFloat(el.textContent)||0, t0=performance.now();
  (function step(now){
    const p=Math.min((now-t0)/800,1), e=p<.5?2*p*p:-1+(4-2*p)*p;
    el.textContent=(from+(target-from)*e).toFixed(1)+' mL/s';
    if(p<1) requestAnimationFrame(step);
  })(t0);
}

/* ─── CONSTRUCTION ARBRE ─── */
function buildTree(growth){
  treeObjs.forEach(o=>{
    treeGroup.remove(o);
    o.geometry?.dispose();
    (Array.isArray(o.material)?o.material:[o.material]).forEach(m=>m?.dispose());
  });
  treeObjs=[];
  if(growth<=0) return;
  const t=growth/100, trunkH=0.3+t*8, trunkR=0.05+t*0.6;
  const trunkCol=growth>=90?0x4a3a2a:0x3a2a1a;

  const tGeo=new THREE.CylinderGeometry(trunkR*.6,trunkR,trunkH,20,12);
  const tp=tGeo.attributes.position;
  for(let i=0;i<tp.count;i++) tp.setX(i,tp.getX(i)+Math.sin(tp.getY(i)/trunkH*2)*t*.3);
  tGeo.computeVertexNormals();
  const trunk=new THREE.Mesh(tGeo,new THREE.MeshStandardMaterial({color:trunkCol,roughness:.9}));
  trunk.position.y=trunkH/2; trunk.castShadow=true;
  treeGroup.add(trunk); treeObjs.push(trunk);

  if(growth>30){
    let fi=Math.max(0,Math.floor((growth-30)/10)); if(fi>=FIB.length) fi=FIB.length-1;
    for(let i=0;i<FIB[fi];i++){
      const ang=i*GOLDEN_ANGLE+Math.random()*.2;
      const bH=.5+t*3*(.5+Math.random()*.5), bR=.02+t*.15, bY=trunkH*(.4+(i/FIB[fi])*.5);
      const b=new THREE.Mesh(new THREE.CylinderGeometry(bR*.3,bR,bH,10),
        new THREE.MeshStandardMaterial({color:trunkCol,roughness:.9}));
      b.position.set(Math.cos(ang)*trunkR*.8,bY,Math.sin(ang)*trunkR*.8);
      b.rotation.z=(Math.cos(ang)>0?-1:1)*(.4+Math.random()*.6); b.rotation.y=ang;
      b.castShadow=true; treeGroup.add(b); treeObjs.push(b);
    }
  }

  if(growth>10){
    const layers=growth>=90?5:(growth>=50?3:1);
    const leafCol=growth>=90?0xd4af37:(growth>=70?0x1a6b1a:0x2e8b2e);
    for(let l=0;l<layers;l++){
      const cR=(.3+t*3.5)*(1-l*.15), cY=trunkH*.7+l*trunkH*.15;
      const cGeo=new THREE.SphereGeometry(cR,26,16);
      const cp=cGeo.attributes.position;
      for(let i=0;i<cp.count;i++){const n=.82+Math.random()*.36; cp.setXYZ(i,cp.getX(i)*n,cp.getY(i)*n,cp.getZ(i)*n);}
      cGeo.computeVertexNormals();
      const c=new THREE.Mesh(cGeo,new THREE.MeshStandardMaterial({color:leafCol,roughness:.8,metalness:growth>=90?.2:0}));
      c.name=`canopy_${l}`; c.position.set(Math.sin(l*GOLDEN_ANGLE)*.3,cY,Math.cos(l*GOLDEN_ANGLE)*.3);
      c.castShadow=true; treeGroup.add(c); treeObjs.push(c);
    }
    if(growth>=90){
      for(let i=0;i<8;i++){
        const ra=(i/8)*Math.PI*2, rl=2+Math.random()*2;
        const r=new THREE.Mesh(new THREE.CylinderGeometry(.05,.2,rl,10),
          new THREE.MeshStandardMaterial({color:0x3a2a1a,roughness:1}));
        r.position.set(Math.cos(ra)*rl*.4,.1,Math.sin(ra)*rl*.4); r.rotation.z=Math.cos(ra)*1.2; r.rotation.y=ra;
        treeGroup.add(r); treeObjs.push(r);
      }
      const gGeo=new THREE.BufferGeometry();
      const gp=new Float32Array(50*3);
      for(let i=0;i<50;i++){gp[i*3]=(Math.random()-.5)*6;gp[i*3+1]=2+Math.random()*(trunkH*.8);gp[i*3+2]=(Math.random()-.5)*6;}
      gGeo.setAttribute('position',new THREE.BufferAttribute(gp,3));
      const gpts=new THREE.Points(gGeo,new THREE.PointsMaterial({color:0xffd700,size:.15,transparent:true,opacity:.6}));
      gpts.name='yggGlow'; treeGroup.add(gpts); treeObjs.push(gpts);
    }
  }
}

/* ─── ARROSAGE ─── */
function doWatering(){
  const now=Date.now(), mins=gs.lastWaterTime?(now-gs.lastWaterTime)/60000:Infinity;
  const used=WATER_ARR+gs.pageWater;
  gs.totalWater+=used; gs.pageWater=0; gs.waterCount++; gs.lastWaterTime=now;

  let gain=0;
  if(gs.phase==='firstWater'){
    gain=5; addJournal(`💧 Premier arrosage ! ${fmtW(used)}.`);
  } else if(mins<1){
    gain=.5; gs.overwatered=true;
    addJournal(`⚠️ Trop fréquent. ${fmtW(used)} gaspillés.`);
    showEco('⚠️ Trop d\'eau !',`${fmtW(used)} utilisés — l'arbre n'en avait pas besoin.<br>${rEco()}`);
  } else if(mins<5){
    gain=2; addJournal(`💧 Arrosage. ${fmtW(used)}.`);
    showEco('💧 Arrosage',`${fmtW(used)} (${wEq(used)}). ${rEco()}`);
  } else if(mins<60){
    gain=5; addJournal(`💧 Bon arrosage ! ${fmtW(used)}.`);
    showEco('🌿 Bon timing !',`L'arbre apprécie un arrosage espacé. ${fmtW(used)}. ${rEco()}`);
  } else {
    gain=10+Math.min(mins/120,10);
    addJournal(`🌟 Arrosage patient ! ${fmtW(used)}.`);
    showEco('🌟 Vertueux !',`Ta patience est récompensée ! Croissance bonus. ${rEco()}`);
  }

  gs.growth=Math.min(100,gs.growth+gain);
  startWaterAnim();
  canGroup.visible=true;
  setTimeout(()=>{canGroup.visible=false;},3000);
  buildTree(gs.growth);

  if(gs.phase==='firstWater'){
    setPhase('growing');
    showEco('🌱 Germination !',`Ta graine germe ! ${fmtW(used)} utilisés (dont ${fmtW(WATER_PAGE)} à l'ouverture). ${rEco()}`);
  }
  if(gs.growth>=90&&gs.phase!=='yggdrasil'){
    setPhase('yggdrasil');
    addJournal('🌳✨ Ton arbre est devenu YGGDRASIL !');
    showEco('✨ YGGDRASIL ✨',`Forme ultime ! ${fmtW(gs.totalWater)} utilisés. Clique 📖 pour réfléchir.`,9000);
  }
  save(); updateUI();
  // Flash pinch XR
  const fl=document.getElementById('ygg-pinch-flash');
  fl.classList.add('fl'); setTimeout(()=>fl.classList.remove('fl'),180);
}

/* ─── PHASES ─── */
let digA={active:false,time:0}, plantA={active:false,time:0}, coverA={active:false,time:0};

/* Utilitaire : montre/cache les boutons sans conflit !important */
function showBtns(...ids){ ['dig','plant','cover','water','moral'].forEach(id=>{ const b=document.getElementById(`ygg-btn-${id}`); if(b) b.style.display=ids.includes(id)?'flex':'none'; }); }

function setPhase(phase){
  gs.phase=phase; save();
  const showHUD=!['start','digging','planting','covering'].includes(phase);
  document.getElementById('ygg-hud').style.display       = showHUD?'flex':'none';
  document.getElementById('ygg-active-cons').style.display = showHUD?'flex':'none';
  document.getElementById('ygg-journal').style.display    = showHUD?'block':'none';
  if(showHUD) renderJournal();

  switch(phase){
    case 'start':
      seed.visible=true; seed.position.set(0,.5,3); seed.scale.setScalar(1);
      showBtns('dig'); break;
    case 'digging':
      showBtns(); digA={active:true,time:0}; break;
    case 'planting':
      seed.visible=true; seed.position.set(0,.5,3); seed.scale.setScalar(1);
      showBtns('plant'); break;
    case 'covering':
      showBtns('cover'); break;
    case 'firstWater':
      showBtns('water'); break;
    case 'growing':
    case 'yggdrasil':
      showBtns('water','moral');
      buildTree(gs.growth); break;
  }
  updateUI();
}

function showMoral(){
  document.getElementById('ygg-moral').hidden=false;
  const tw=gs.totalWater, pw=gs.pageLoads*WATER_PAGE;
  document.getElementById('ygg-moral-text').innerHTML=`
    Ton arbre est magnifique. Chaque visite consomme <strong>${fmtW(WATER_PAGE)}</strong> symboliquement,
    et chaque arrosage <strong>${fmtW(WATER_ARR)}</strong> de plus.<br><br>
    <em>Les datacenters d'Internet consomment des milliards de litres pour leur refroidissement. Chaque pixel compte.</em>`;
  document.getElementById('ygg-moral-stat').innerHTML=
    `💧 Total : ${fmtW(tw)}<br>🌱 Dont ${fmtW(pw)} en ouvrant la page ${gs.pageLoads}×<br>🚿 ${wEq(tw)}`;
  document.getElementById('ygg-moral-advice').innerHTML=
    `<strong>La morale :</strong> Le meilleur joueur n'est pas celui qui a le plus gros arbre —
     c'est celui qui a utilisé le <em>moins d'eau</em> pour y arriver.<br><br>
     <strong style="color:#9de090;">Score écologique : ${Math.max(0,100-Math.floor(tw*2))}/100</strong>`;
}

/* ─── DÉMARRAGE DU JEU ─── */
function startGame(){
  const intro=document.getElementById('ygg-intro');
  intro.classList.add('ygg-out');
  setTimeout(()=>{ intro.style.display='none'; },1200);

  document.getElementById('ygg-actions').style.display='flex';
  document.getElementById('ygg-toggle-bar').style.display='flex';

  setDisplayMode(false); // N&B par défaut

  setPhase(gs.phase);

  if(gs.phase==='start'){
    showEco('🌰 Ta graine','Tu as une graine. Creuse un trou dans le sol pour la planter.');
  }
  if(['growing','yggdrasil'].includes(gs.phase)){
    setTimeout(()=>showEco('🌱 Connexion détectée',
      `Rien qu'en ouvrant cette page, <strong>${fmtW(WATER_PAGE)}</strong> dépensés. ${rEco()}`), 1500);
  }
}

/* ─── EVENT LISTENERS ─── */
document.getElementById('ygg-start-btn').addEventListener('click', startGame);

document.getElementById('ygg-reset-btn').addEventListener('click',()=>{
  localStorage.removeItem(SAVE_KEY); location.reload();
});

document.getElementById('ygg-toggle-btn').addEventListener('click',()=> setDisplayMode(!isColor));

document.getElementById('ygg-btn-dig').addEventListener('click',()=>{
  setPhase('digging'); addJournal('🤲 Tu creuses un trou dans la terre...');
});
document.getElementById('ygg-btn-plant').addEventListener('click',()=>{
  setPhase('covering'); plantA={active:true,time:0};
  addJournal('🌰 Tu déposes la graine au creux de la terre.');
});
document.getElementById('ygg-btn-cover').addEventListener('click',()=>{
  coverA={active:true,time:0};
  addJournal('🫳 Tu recouvres doucement la graine de terre.');
  setTimeout(()=>{
    setPhase('firstWater');
    showEco('🌱 Première étape','Ta graine est plantée. Elle a besoin de son premier arrosage pour germer.');
  },1800);
});
document.getElementById('ygg-btn-water').addEventListener('click', doWatering);
document.getElementById('ygg-btn-moral').addEventListener('click', showMoral);
document.getElementById('ygg-close-moral').addEventListener('click',()=>{ document.getElementById('ygg-moral').hidden=true; });

// Bouton i
const infoBtn=document.getElementById('ygg-info-btn');
const helpPanel=document.getElementById('ygg-help-panel');
infoBtn.addEventListener('click',()=>{
  const h=helpPanel.hidden; helpPanel.hidden=!h; infoBtn.setAttribute('aria-expanded',h?'true':'false');
});
document.getElementById('ygg-help-close').addEventListener('click',()=>{ helpPanel.hidden=true; infoBtn.setAttribute('aria-expanded','false'); });

// Orbite souris
canvas.addEventListener('pointerdown',e=>{drag=true;pmx=e.clientX;pmy=e.clientY;});
canvas.addEventListener('pointerup',()=>drag=false);
canvas.addEventListener('pointerleave',()=>drag=false);
canvas.addEventListener('pointermove',e=>{
  if(!drag) return;
  azimuth-=(e.clientX-pmx)*.005; polar=Math.max(.08,Math.min(Math.PI/2-.05,polar-(e.clientY-pmy)*.005));
  pmx=e.clientX; pmy=e.clientY; updateCam();
});

// Zoom molette
window.addEventListener('wheel',e=>{
  e.preventDefault(); camR=Math.max(4,Math.min(28,camR+e.deltaY*.01)); updateCam();
},{passive:false});

// Zoom tactile
let ltd=0;
canvas.addEventListener('touchstart',e=>{ if(e.touches.length===2) ltd=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY); });
canvas.addEventListener('touchmove',e=>{
  if(e.touches.length===2){
    e.preventDefault();
    const d=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);
    camR=Math.max(4,Math.min(28,camR-(d-ltd)*.05)); ltd=d; updateCam();
  }
},{passive:false});

// Reset keyboard
window.addEventListener('keydown',e=>{ if(e.key==='r'&&e.ctrlKey&&e.shiftKey){localStorage.removeItem(SAVE_KEY);location.reload();} });

// Resize
window.addEventListener('resize',()=>{
  camera.aspect=window.innerWidth/window.innerHeight; camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth,window.innerHeight); updateCam();
});

/* ─── INIT ─── */
if(gs.phase!=='start') document.getElementById('ygg-start-btn').textContent='Retrouver mon arbre';
// Logo intro N&B par défaut (fond sombre → inversion)
document.getElementById('ygg-intro-logo').src=LOGO_STROKE;
document.getElementById('ygg-logo-img').src=LOGO_STROKE;
canvas.classList.add('ygg-nb');
initXR();

/* ─── BOUCLE ANIMATION ─── */
const clock=new THREE.Clock();

renderer.setAnimationLoop((timestamp, frame)=>{
  const dt=clock.getDelta(), t=clock.elapsedTime;

  /* Herbe */
  grassMat.uniforms.time.value=t;

  /* Lucioles */
  const fp=ffGeo.attributes.position;
  for(let i=0;i<ffN;i++){
    fp.setY(i,fp.getY(i)+Math.sin(t*.5+ffPha[i])*.002);
    fp.setX(i,fp.getX(i)+Math.cos(t*.3+ffPha[i])*.001);
  }
  fp.needsUpdate=true;
  ffMat.opacity=.3+Math.sin(t*2)*.24;

  /* Graine flottante */
  if(seed.visible&&gs.phase==='start'){
    seed.position.y=.5+Math.sin(t*2)*.05; seed.rotation.y=t*.5;
  }

  /* Phase : creuser */
  if(digA.active){
    digA.time+=dt*1.2; const p=Math.min(digA.time,1);
    if(p>=1){ digA.active=false; hole.visible=true; setPhase('planting'); showEco('🕳️ Trou creusé','Le sol est prêt. Plante ta graine !'); }
  }

  /* Phase : planter */
  if(plantA.active){
    plantA.time+=dt*1.2; const p=Math.min(plantA.time,1);
    seed.position.set(0,.5-p*.6,3-p*3); seed.scale.setScalar(1-p*.3);
    if(p>=1){ plantA.active=false; seed.visible=false; }
  }

  /* Phase : recouvrir */
  if(coverA.active){
    coverA.time+=dt*.9; const p=Math.min(coverA.time,1);
    hole.scale.y=1-p;
    if(p>=1){ coverA.active=false; hole.visible=false; hole.scale.y=1; }
  }

  /* Eau */
  if(waterA.active){
    waterA.time+=dt;
    const pos=waterP.geometry.attributes.position, vel=waterP.geometry.userData.velocity;
    for(let i=0;i<pos.count;i++){
      pos.setX(i,pos.getX(i)+vel[i*3]); pos.setY(i,pos.getY(i)+vel[i*3+1]); pos.setZ(i,pos.getZ(i)+vel[i*3+2]);
      vel[i*3+1]-=.001;
      if(pos.getY(i)<0){ pos.setXYZ(i,(Math.random()-.5)*1.5,3+Math.random(),(Math.random()-.5)*1.5); vel[i*3+1]=-.05-Math.random()*.08; }
    }
    pos.needsUpdate=true;
    if(waterA.time>3){ waterA.active=false; waterP.visible=false; }
  }

  /* Arrosoir */
  if(canGroup.visible){ canGroup.position.y=3+Math.sin(t*3)*.1; canGroup.rotation.z=-.3-Math.sin(t*2)*.1; }

  /* Canopées ondulantes */
  treeGroup.rotation.y=Math.sin(t*.28)*.018;
  treeObjs.forEach(o=>{ if(o.name?.startsWith('canopy')) o.position.x+=Math.sin(t*.8+o.id)*.0007; });
  const glow=treeGroup.getObjectByName('yggGlow');
  if(glow){ glow.rotation.y=t*.1; glow.material.opacity=.3+Math.sin(t)*.3; }

  /* ─── HAND TRACKING XR ─── */
  if(renderer.xr.isPresenting){
    updateHandViz(xrHand0,handMaps[0],false);
    updateHandViz(xrHand1,handMaps[1],true);

    const d0=pinchDist(xrHand0), isPinch0=d0<.03;
    if(isPinch0&&!pinchWas0){
      const now=Date.now();
      if(now-lastPinch>2000&&['firstWater','growing','yggdrasil'].includes(gs.phase)){ lastPinch=now; doWatering(); }
    }
    pinchWas0=isPinch0;

    const d1=pinchDist(xrHand1), isPinch1=d1<.03;
    if(isPinch1&&!pinchWas1){
      const now=Date.now();
      if(now-lastPinch>2000&&['firstWater','growing','yggdrasil'].includes(gs.phase)){ lastPinch=now; doWatering(); }
    }
    pinchWas1=isPinch1;

    if(dropCursor){
      dropCursor.material.opacity=(isPinch0||isPinch1)?1:.8;
      dropCursor.scale.setScalar((isPinch0||isPinch1)?1.4:1);
    }

    /* AR Hit Test */
    if(frame){
      const refSpace=renderer.xr.getReferenceSpace(), sess=renderer.xr.getSession();
      if(!hitSrcReq&&sess){
        sess.requestReferenceSpace('viewer').then(vs=>
          sess.requestHitTestSource({space:vs}).then(s=>{ hitSrc=s; })
        );
        sess.addEventListener('end',()=>{ hitSrcReq=false; hitSrc=null; reticle.visible=false; });
        hitSrcReq=true;
      }
      if(hitSrc){
        const res=frame.getHitTestResults(hitSrc);
        if(res.length>0){ reticle.visible=true; reticle.matrix.fromArray(res[0].getPose(refSpace).transform.matrix); }
        else { reticle.visible=false; }
      }
    }
  } else {
    handMaps.forEach(m=>Object.values(m).forEach(mesh=>mesh.visible=false));
    if(dropCursor) dropCursor.visible=false;
    updateCam();
  }

  renderer.render(scene,camera);
});
</script>
</body>
</html>
				</div>
				</div>
					</div>
				</div>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/yggdrasil/">Yggdrasil</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>spz</title>
		<link>https://presentcomposedesign.fr/spz/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Fri, 10 Apr 2026 08:18:05 +0000</pubDate>
				<category><![CDATA[Design produit]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=36271</guid>

					<description><![CDATA[<p>Initialisation Ouvrir en AR / VR Position &#38; Echelle X0.00 Y0.00 Z0.00 Echelle Reglages Ambiant0.70 Lumiere0.90 Opacite1.00 Brillance0.40 Reflexion0.20 Medias Video Plat 360 Sphere Cylindrique Cubique Taille3.0 Charger video Retirer video Image Plat 360 Sphere Cylindrique Cubique Taille3.0 Charger image Retirer image Audio Volume0.80 Charger audio Triggers XR GPS &#8212;, &#8212; Rayon m50 Activer GPS + Zone ici Image Tracker Largeur m0.20 Ajouter tracker &#160;Anim Aucune Tourniquet Flottement Respiration Expansion Suivre souris Orbite douce Scroll Zoom pulse Apparition GlisserRotation ScrollZoom Shift+dragPan Dbl-clicReset</p>
<p>Cet article <a href="https://presentcomposedesign.fr/spz/">spz</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="36271" class="elementor elementor-36271">
				<div class="elementor-element elementor-element-a43c5a5 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="a43c5a5" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
				<div class="elementor-element elementor-element-888a871 elementor-widget__width-inherit elementor-widget elementor-widget-html" data-id="888a871" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!--
╔═══════════════════════════════════════════════════════════════╗
║ XR Viewer Ultimate V2, © 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/                         ║
╚═══════════════════════════════════════════════════════════════╝

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#*++==------===**%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#++-::::::::::::::::::::::-=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+-::::::::::::::::::::::::::::::::=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+::::::::::::::::::::::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@%+:::::::::::::::::=++***+=-::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@#=::::::::::::::::+%@@@@@@@@@@%+-:::::::::::::::::::=#@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@#-::::::::::::::::#@@@@@@@@@@@@@@@@%-:::::::::::::::::::=#@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@%=::::::::::::::::*@@@@@@**==-=+##@@@@@*-:::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@=:::::::::::::::::%@@@@@+::::::::::-*@@@@#-::::::::::::::::::::=@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@#::::::::::::::::::%@@@@=::::::::::::::-%@@@#-:::::::::::::::::::::#@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@=::::::::::::::::::*@@@@=::::::::::::::::-%@@@*::::::::::::::::::::::=@@@@@@@@@@@@@@@
@@@@@@@@@@@@@%-::::::::::::::::::-@@@@=::::::::::::::::::-%@@@-::::::::::::::::::::::-%@@@@@@@@@@@@@
@@@@@@@@@@@@#::::::::::::::::::::*@@@#::::::::::::::::::::=@@@*::::::::::::::::::::::::*@@@@@@@@@@@@
@@@@@@@@@@@#:::::::::::::::::::::%@@@+::::::::::::::::::::-@@@%:::::::::::::::::::::::::*@@@@@@@@@@@
@@@@@@@@@@+::::::::::::::::::::::@@@@-:::::::::::::::::::::%@@@::::::::::::::::::::::::::*@@@@@@@@@@
@@@@@@@@@*:::::::::::::::::::::::@@@@=:::::::::::::::::::::@@@@:::::::::::::::::::::::::::*@@@@@@@@@
@@@@@@@@%::::::::::::::::::::::::@@@@*:::::::::::::::::::::@@@%::::::::::::::::::::::::::::*@@@@@@@@
@@@@@@@%:::::::::::::::::::::::::@@@@%::::::::::::::::::::+@@@*:::::::::::::::::::::::::::::%@@@@@@@
@@@@@@@::::::::::::::::::::::::::@@@@@*::::::::::::::::::-@@@@::::::::::::::::::::::::::::::-@@@@@@@
@@@@@@=::::::::::::::::::::::::::@@@@@@*::::::::::::::::-@@@@+:::::::::::::::::::::::::::::::*@@@@@@
@@@@@%:::::::::::::::::::::::::::@@@@@@@*::::::::::::::-%@@@#:::::::::::::::::::::::::::::::::*@@@@@
@@@@@::::::::::::::::::::::::::::@@@@@@@@@*::::::::::+@@@@@*::::::::::::::::::::::::::::::::::-@@@@@
@@@@*::::::::::::::::::::::::::::@@@#=@@@@@@%#*+=**%@@@@@@=::::::::::::::::::::::::::::::::::::*@@@@
@@@@:::::::::::::::::::::::::::::@@@#::*@@@@@@@@@@@@@@@@*:::::::::::::::::::::::::::::::::::::::@@@@
@@@*:::::::::::::::::::::::::::::@@@#::::=*@@@@@@@@@%#=:::::::::::::::::::::::::::::::::::::::::#@@@
@@@=:::::::::::::::::::::::::::::@@@#::::::::-==+=-:::::::::::::::::::::::::::::::::::::::::::::=@@@
@@@::::::::::::::::::::::::::::::@@@#:::::::::::::::::::::::::::::::::::::::::::::::::--:::::::::@@@
@@*::::::::::::::::::::::::::::::@@@#:::::::::+######*:::::::::::::::::::::::::::::::+@=:::::::::#@@
@@*::::::::::::::::::::::::::::::@@@#:::--::::===+*+==:::::=-:::::::=-::::::-::::::::+@=:::::::::+@@
@@:::::::::::::::::::::::::::::::@@@#::+@#@@::-#@@%@%=::=@@@@+:::+@@@@@+-::@@@@%*-::#@@%#:::::::::@@
@@:::::::::::::::::+*##%%#*=-::::@@@#::+@@=::-%@:::-*@=:%@-:=:::*@*:::-@*::@@-:=@%::=*@*=:::::::::@@
@@::::::::::::::+%@@@@@@@@@@@%*-:@@@#::+@*:::*@@@@@@@@#:+@%*-:::@@@@@@@@@-:@%:::#@:::+@=::::::::::@@
@%::::::::::::*@@@@@@@@@@@@@@@@@#+@@#::+@+:::+@=-------:::=%@*::@%-------::@%:::#@:::+@=::::::::::%@
@%::::::::::-@@@@@@++-::::-=#@@@@@+*#::+@+:::-@%=-:+%%-:-=::#@::*@*-::+%=::@%:::#@:::+@=::::::::::#@
@%:::::::::=@@@@%+::::::::::::#@@@@**::+@+::::-#@@@@%=::#@%%@+:::+@@@@@*:::@%:::#@:::+@=::::::::::%@
@%::::::::-@@@@+:::::::::::::::-@@@@+:::-::::::::--:::::::=-:::::::-=-:::::--:::-=----=-::::::::::%@
@@:::::::-@@@@*:::::::::::::::::-%%%*:::::::::::::::::::::::::::::::::::::::::::+%%%%%%#::::::::::@@
@@:::::::*@@@#:::::::+#@%*-::::::=+*#*=+#*=-:::::+#%%#-::::::=#%%#=::::-#@@#-::::=##%#+-::::::::::@@
@@-::::::@@@@-:::::-%@#+*%@*:::::%@#+#@@+*@*::::%@+==#@*::::%@#+=%@*-::%@-=*=:::*@*+=+@%-::::::::-@@
@@+:::::-@@@@::::::#@=::::#@=::::%@::=@*::#@-::*@=::::-@+::*@*::::*@*::*@*-::::-@@####%@#::::::::*@@
@@#:::::-@@@*::::::@@:::::=@*::::%@::=@*::#@-::%@::::::@*::%@-::::-@%:::=%@%-::*@#******+::::::::*@@
@@@-::::-@@@%::::::#@+::::%@-::::%@::=@*::#@-::@@-::::=@-::*@*::::*@*:::-::@%::=@*::::-=-::::::::@@@
@@@+:::::@@@@-::::::%@%*#@@+:::::%@::=@*::#@-::@@@#=+#@+::::*@%*+@@*:::#@*+@*:::*@%*+%@%::::::::-@@@
@@@#:::::#@@@+:::::::=*##+-::::::+*::-*=::+*:::@%=*%#*-::::::=*%#*-:::::=*#+:::::-*##*=--=::::::*@@@
@@@@-::::=@@@@-::::::::::::::::::=**+::::::::::@%:::::::::::::::::::::::-==++**##%%@@@@@@@-::::-@@@@
@@@@*:::::*@@@@-::::::::::::::::+@@@#::::::::::::::::::::::::-==++**##=-#@@@@@@@@@@@@@@@@@=::::*@@@@
@@@@@-:::::#@@@@+-:::::::::::::*@@@@::::::-==++--##%@@@@@@@@@#*#@@%%@@@@@@@@@@@@@@@@@@@#@@@+:::-@@@@
@@@@@%::::::*@@@@@+-:::::::::+@@@@%:=@@@@@@@@@@==@@@**%@@@@*-#@*-@@*@@@*-+**@@@:%@@==@++@@*:::%@@@@@
@@@@@@*::::::=@@@@@@@%%**##@@@@@@=::=@@@@#-:--=-=@@=+@+=@@@:%@@@=+@-*@*-@@@+:@@=#@:*@@%-@@#::+@@@@@@
@@@@@@@-:::::::+%@@@@@@@@@@@@@@#-::::@@@+-%@@@*--@@:@@=*@@@%=*%@@@@*=@-*@@@*:+@=+=#@@@%:@@%:-@@@@@@@
@@@@@@@@-::::::::=*%@@@@@@@##-:::::::@@@:#@@@@@*:@@:%==@@@@@@%+-%@@#-@*+@@@:+:@*:*@@@@@:%@@-@@@@@@@@
@@@@@@@@#-:::::::::::::-:::::::::::::@@%:%@@@@@*-@@-#@@@#-@*%@@@-#@@:@@#=+:%@-#@:@@@@@@*=@@%@@@@@@@@
@@@@@@@@@*:::::::::::::::::::::::::::#@@--@@@@%:*@@%*+*=*@@=+@@@=+@@-#@@@@@@@=#@#@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@*-:::::::::::::::::::::::::#@@@+-==-:#@@@@@@@@@@@@#++*#@@@@@@@#@@@%:@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@*-::::::::::::::::::::::::+@@@@@%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-=-:+**++==--::%@@@@@@@@@
@@@@@@@@@@@@#-:::::::::::::::::::::::=@@@@@@@@@@@@@@@@@@@@@%%##**++=---::::::::::::::::::::%@@@@@@@@
@@@@@@@@@@@@@@-::::::::::::::::::::::-@@@@@@%###**++=--::::::::::::::::::::::::::::::::::-%@@@@@@@@@
@@@@@@@@@@@@@@@+::::::::::::::::::::::--::::::::::::::::::::::::::::::::::::::::::::::::+@@@@@@@@@@@
@@@@@@@@@@@@@@@@%-::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::-%@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@+-:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::+@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@%=::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@%=::::::::::::::::::::::::::::::::::::::::::::::::::::::::::=%@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@%=-:::::::::::::::::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@*-:::::::::::::::::::::::::::::::::::::::::::::::::#@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@%+-:::::::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%*=-:::::::::::::::::::::::::::::::::::+#@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%**=-:::::::::::::::::::::::::=+#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%**++=====+++*##@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#****+++++****##%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@**+-::::::::::::::::::::==*#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*+-::::::::::::::::::::::::::::::==#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+-::::::::::::::::::::::::::::::::::::::=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@*-::::::::::::::::::::::::::::::::::::::::::::=*%@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@#+:::::::::::::::++****%%@@@@@%%##**=--:::::::::::::=#@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@*=:::::::::::::+*%@@@@@@@@@@@@@@@@@@@@@@@#*=-:::::::::::=#@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@#=:::::::::::+*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#=-::::::::::=#@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@%=::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*=::::::::::=%@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@*::::::::::*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+-:::::::::*%@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@%=::::::::=*@@@@@@@@@@@@@@%#*********#%%@@@@@@@@@@@@@@@@@@@%+-::::::::=%@@@@@@@@@@@@@@
@@@@@@@@@@@@@#:::::::::#@@@@@@@@@@@**+-::::::::::::::::-+#%@%@@@@@@@@@@@@@@%+-::::::::*@@@@@@@@@@@@@
@@@@@@@@@@@@+::::::::+@@@@@@@@@@*-::::::::::::::::::::::#@@@##%%@@@@@@@@@@@@@@+::::::::*%@@@@@@@@@@@
@@@@@@@@@@@=:::::::=#@@@@@@@@@+::::::::::::::::::::::::-*@@@@@@#%%@@@@@@@@@@@@@*-:::::::=%@@@@@@@@@@
@@@@@@@@@@=:::::::*@@@@@@@@@%=::::::::::::::::::::::::::*@@@@@@@@##%@@@@@@@@@@@@%+:::::::=%@@@@@@@@@
@@@@@@@@%=:::::::#@@@@@@@@@%:::::::::::::::::::::::::::+@@@@@@@@@@@%%@@@@@@@@@@@@@*-::::::-%@@@@@@@@
@@@@@@@@=::::::-#@@@@@@@@@%-:::::::::::::::::::::::::==#@@@@@@@@@@@@%#@@@@@@@@@@@@@#-::::::=%@@@@@@@
@@@@@@@=::::::-%@@@@@@@@@@=:::::::::::::::::::::::::*=#%@@@@@@@@@@@@@%#@@@@@@@@@@@@@#-::::::*@@@@@@@
@@@@@@*::::::-#@@@@@@@@@@*::::::::::::::::::::::::*=#+%@@@%@@@@@@@@@@@%%@@@@@@@@@@@@@%-::::::*@@@@@@
@@@@@%:::::::#@@@@@@@@@@%:::::::::::::::::::---:::-:-+*@%%@@@@@@@@@@@@@*%@@@@@@@@@@@@@%-::::::%@@@@@
@@@@@-::::::#@@@@@@@@@@@-::::::::::::::::::::+=#-=#*#=+%%@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@#-:::::-%@@@@
@@@@*::::::*@@@@@@@@@@@#:::::::::::::::::::::-++=#==#+-%%%@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@*::::::*@@@@
@@@%::::::-@@@@@@@@@@@@=::::::::::::::::::::::::+=++#%=%*@@@@@@@@@@@@@@@@*@@@@@@@@@@@@@@@-::::::%@@@
@@@*::::::#@@@@@@@@@@@%:::::::::::::::::::::::::+#:+@%+%@@@@@@@@@@@@@@@@@@#@@@@@@@@@@@@@@%-:::::*@@@
@@%::::::=@@@@@@@@@@@@*::::::::::::::::::::::::-#*-#+=@@%@%@@@@@@@@@@@@@@@#@@@@@@@@@@@@@@@*::::::%@@
@@*::::::%@@@@@@@@@@@@=:::::::::::::::::::::::+=#@++=+%@@@@@@@@@@@@@@#@@@+-%@@@@@@@@@@@@@@@-:::::*@@
@@::::::*@@@@@@@@@@@@@-:::::::::::::::::::::::-**+@+*=%@%@@@@@@@@@@@#@@%=::*@@@@@@@@@@@@@@@*::::::%@
@%::::::#@@@@@@@@@@@@@::::::::::::::::::::::::::==#@%+@%+*%%@@@@@@@#==+::=-#@@@@@@@@@@@@@@@#::::::*@
@*:::::-@@@@@@@@@@@@@@::::::::::::::::::::::::::::***%-@*@%%@@@@@@@%%=+*%%%@@@@@@@@@@@@@@@@@-:::::+@
@=:::::*@@@@@@@@@@@@@@-::::::::::::::::::::::::::-+@+=+*%*%@@@@@@@%@@@%*=:*@@@@@@@@@@@@@@@@@+:::::=@
%::::::*@@@@@@@@@@@@@@*::::::::::::::::::::::-=-=+#**#=%+@@*@@@@@@@@@@@+=**%@@@@@@@@@@@@@@@@#::::::%
%::::::%@@@@@@@@@@@@@@#:::::::::::::::::::+#@++@%@@#*+*+#=@@@@@@@@@@@@@@*%@#%@@@@@@@@@@@@@@@#::::::#
*::::::@@@@@@@@@@@@@@@@-:::::::::::::::::*@@@:::%@@@@*---**@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@%::::::*
*::::::@@@@@@@@@@@@@@@@+:::::::::::::::::@@@@%::*@@@@@+-:=%+@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@::::::*
*:::::-@@@@@@@@@@@@@@@@#:::::::::::::::::%=%@@-:+@@@@@*::%*@@@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@-:::::*
*:::::=@@@@@@@@@@@@@@@@@+::::::::::::::::*:-@*::*-*@@@@-:-=%@%@@@@@@@@@@@@@@@@@#@@@@@@@@@@@@@::::::*
*:::::-@@@@@@@@@@@@@@@@@%-:::::::::::::::*+:%@@:::-@@@@-:=+%%@%@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@::::::*
*::::::%@@@@@@@@@@@@@@@@@*::::::::::::::::*=-@*::::%@@@+:+*@@@%@@@@@@@@@@@%@@%#@@@@@@@@@@@@@%::::::*
#::::::%@@@@@@@@@@@@@@@@@%-::::::::::::::::#+**:::::%@@*:-=%@*@@@@@@@@@@@#**%@@@@@@@@@@@@@@@%::::::#
@::::::*@@@@@@@@@@@@@@@@@@*::::::::::::::::-%@@@@@**@@%=::=+@@@@@%@@@@@@@@@%@@@@@@@@@@@@@@@@*::::::@
@-:::::*@@@@@@@@@@@@@@@@@@@=:::::::::::::::::#@@@@@@@@%:::+=%@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@*:::::-@
@+:::::=@@@@@@@@@@@@@@@@@@@%-:::::::::::::::::*%@@@*%@-:::-*=*%@@@@@@@@@@@@@%@@@@@@@@@@@@@@@::::::*@
@*::::::%@@@@@@@@@@@@@@@@@@@*:::::::::::::::::::=%%+#@=:::=*++@@@@@@@@@@@@@#@@@@@@@@@@@@@@@%::::::*@
@@-:::::*@@@@@@@@@@@@@@@@@@@#::::::::::::::::----:=###::::=**%%@@@@@@@@@@@+%@@@@@@@@@@@@@@@+:::::-@@
@@+:::::-%@@@@@@@@@@@@@@@@@@@-:::::::::::::=@@@@@+::::::::-*#%%%@@@@@@@@@%*%@@@@@@@@@@@@@@%::::::*@@
@@#::::::*@@@@@@@@@@@@@@@@@@@+::::::::::::::%@@@@@-:::::::+%@%%%@@@@@@@@@%@@@@@@@@@@@@@@@@+::::::%@@
@@@+::::::%@@@@@@@@@@@@@@@@@@*::::::::::::::*@@@@@-:::::::-+#@%%@%@@@@@@@%@@@@@@@@@@@@@@@%::::::=@@@
@@@%-:::::*@@@@@@@@@@@@@@@@@@*::::::::::::::#@@@@@::::::::::*@%**%@@@@@@@#@@@@@@@@@@@@@@@=::::::%@@@
@@@@*::::::%@@@@@@@@@@@@@@@@@*::::::::::::::*@@@@@#-::::::::===+-%%@@@@@@%%@@@@@@@@@@@@@*::::::*@@@@
@@@@@-:::::-%@@@@@@@@@@@@@@@@+::::::::::::::=#%@@@@*--#=::::::--:=@@%%%@@%@@@@@@@@@@@@@#::::::-@@@@@
@@@@@#-:::::-%@@@@@@@@@@@@@@@=::::::::::::::==@%@%%%#+%%*:::::::-=#%%*%@#*@@@@@@@@@@@@%:::::::#@@@@@
@@@@@@*::::::*%@@@@@@@@@@**+-:::::::::::::::@#@@%@@@%#%@*::::-::::==::%%*@@@@@@@@@@@@%=::::::*@@@@@@
@@@@@@@+::::::*@@@@@@@@*::::::::::::::::::-##%%@#@@@@%%%*-==*%:-::==+**@@@@@@@@@@@@@%=::::::=@@@@@@@
@@@@@@@@-::::::*%@@@@*::::::::::::::::::::::%%%%%@@@@@@@#%@@%%@@@@@@@@@@@@@@@@@@@@@%=::::::=@@@@@@@@
@@@@@@@@%-::::::=%@@%:::::::::::::::::::::#@%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#:::::::-@@@@@@@@@
@@@@@@@@@@-::::::-%@#::::::::::::::::::::+@@@@@%@@@@@@@@@@@%%@@@@@@@@@@@@@@@@@@@@*:::::::=@@@@@@@@@@
@@@@@@@@@@@-:::::::*@-:::::::::::::::::::-%@@@@@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@=:::::::=@@@@@@@@@@@
@@@@@@@@@@@@+:::::::==:::::::::::::::::::-%@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@#-:::::::+@@@@@@@@@@@@
@@@@@@@@@@@@@*-:::::::::::::::::::::::::::=@@@@@@@@@@@@@@%#@@@@@@@@@@@@@@@@@%=::::::::#@@@@@@@@@@@@@
@@@@@@@@@@@@@@#-:::::::::::::::::::::::::::*@@@@@@@@@@@@#@@@@@@@@@@@@@@@@@@+::::::::=#@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@*-::::::::::::::::::::::::::*%@@@@@@@@@@*@@@@@@@@@@@@@@@@+:::::::::*@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@%=:::::::::::::::::::::::::::*%@@@@@@@@#@@@@@@@@@@@@@*=:::::::::=%@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@#-:::::::::::::::::::::::::::*%@@@@@@@%#@@@@@@@@*+-:::::::::=#@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@#-:::::::::::::::::::::::::::*%@@@@@@@%@@@@*+:::::::::::=#@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@#=-::::::::::::::::::::::::::+%@@@@%**--::::::::::::+#@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@%*-:::::::::::::::::::::::::::::::::::::::::::::*%@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@#*-:::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@##=-:::::::::::::::::::::::::::::::+*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@##*=-::::::::::::::::::::-+**%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%##****+++++****%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
-->

<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>

<style>
#xrvu-root {
  all: unset !important; display: block !important; position: relative !important;
  width: 100% !important; height: 840px !important; background: #08080e !important;
  border-radius: 14px !important; overflow: hidden !important;
  font-family: 'Inter Tight', system-ui, -apple-system, sans-serif !important;
  font-size: 13px !important; -webkit-font-smoothing: antialiased;
  color: rgba(255,255,255,.9) !important; line-height: 1.4 !important; box-sizing: border-box !important;
}
#xrvu-root *, #xrvu-root *::before, #xrvu-root *::after { box-sizing: border-box !important; }
#xrvu-root *:not(svg):not(svg *) { font-family: 'Inter Tight', system-ui, -apple-system, sans-serif !important; }
#xrvu-root {
  --xrvu-primary: #0010ef; --xrvu-accent: #1df2d6; --xrvu-text: rgba(255,255,255,.9);
  --xrvu-muted: rgba(255,255,255,.45); --xrvu-dim: rgba(255,255,255,.2);
  --xrvu-glass-bg: rgba(4,6,16,.82); --xrvu-glass-bd: rgba(255,255,255,.12); --xrvu-sep: rgba(255,255,255,.07);
}
#xrvu-gs-stage, #xrvu-three-stage { position: absolute; inset: 0; z-index: 1; }
#xrvu-gs-stage canvas, #xrvu-three-stage canvas { display: block; width: 100% !important; height: 100% !important; touch-action: none; }
#xrvu-bg { position: absolute; inset: 0; z-index: 0; background-size: cover; background-position: center; }
#xrvu-bg-video { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; object-fit: cover; }
#xrvu-overlay { position: absolute; inset: 0; z-index: 30; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; transition: opacity .7s ease; pointer-events: none; }
#xrvu-overlay.fade { opacity: 0; }
#xrvu-overlay.hidden { display: none; }
#xrvu-spinner { width: 38px; height: 38px; border: 2px solid rgba(255,255,255,.05); border-top-color: var(--xrvu-primary); border-right-color: rgba(0,16,239,.2); border-radius: 50%; animation: xrvu-spin .9s linear infinite; }
@keyframes xrvu-spin { to { transform: rotate(360deg); } }
#xrvu-status { font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--xrvu-dim); min-height: 14px; }
#xrvu-status.error { color: #e05555; text-transform: none; letter-spacing: 0; font-size: 11px; max-width: 280px; text-align: center; line-height: 1.5; }
#xrvu-barwrap { width: 160px; height: 1px; background: rgba(255,255,255,.07); border-radius: 1px; }
#xrvu-progressbar { height: 100%; width: 0%; background: var(--xrvu-primary); border-radius: 1px; transition: width .15s linear; }
#xrvu-brand { position: absolute; top: 14px; left: 14px; z-index: 20; display: flex; align-items: center; gap: 9px; pointer-events: none; }
#xrvu-brand-logo { max-height: 32px; display: block; }
#xrvu-brand-title { font-size: 14px; font-weight: 700; color: var(--xrvu-text); letter-spacing: .02em; }
#xrvu-bar {
  position: absolute !important; bottom: 10px !important; left: 50% !important; transform: translateX(-50%) !important;
  z-index: 25 !important; display: flex !important; gap: 4px !important; align-items: center !important;
  background: var(--xrvu-glass-bg) !important; backdrop-filter: blur(14px) saturate(1.6) !important;
  -webkit-backdrop-filter: blur(14px) saturate(1.6) !important; border: 1px solid var(--xrvu-glass-bd) !important;
  border-radius: 999px !important; padding: 5px 6px !important; box-shadow: 0 12px 32px rgba(0,0,0,.6) !important;
  margin: 0 !important; list-style: none !important; width: auto !important; max-width: none !important;
  white-space: nowrap !important; transition: opacity .35s ease !important;
}
#xrvu-bar.hud-hidden { opacity: 0 !important; pointer-events: none !important; }
#xrvu-instructions {
  position: absolute !important; bottom: 56px !important; left: 50% !important; transform: translateX(-50%) !important;
  z-index: 24 !important; display: flex !important; gap: 18px !important; pointer-events: none !important;
  transition: opacity .4s ease !important; white-space: nowrap !important;
}
#xrvu-instructions.inst-hidden { opacity: 0 !important; }
#xrvu-root button.xrvu-btn {
  all: unset !important; box-sizing: border-box !important; width: 34px !important; height: 34px !important;
  border-radius: 999px !important; display: flex !important; align-items: center !important; justify-content: center !important;
  cursor: pointer !important; color: var(--xrvu-muted) !important; background: transparent !important;
  transition: background .14s, color .14s, transform .14s !important; padding: 0 !important; margin: 0 !important;
  line-height: 1 !important; outline: none !important; -webkit-appearance: none !important; appearance: none !important; position: relative !important;
}
#xrvu-root button.xrvu-btn:hover { background: rgba(255,255,255,.08) !important; transform: translateY(-1px) !important; color: var(--xrvu-text) !important; }
#xrvu-root button.xrvu-btn.active { color: var(--xrvu-accent) !important; background: rgba(29,242,214,.08) !important; }
#xrvu-root button.xrvu-btn svg { width: 15px; height: 15px; pointer-events: none; flex-shrink: 0; }
#xrvu-btn-anim { width: auto !important; padding: 0 11px !important; gap: 7px !important; font-size: 10px !important; letter-spacing: .07em !important; }
#xrvu-btn-anim svg { width: 13px !important; height: 13px !important; }
.xrvu-sep { width: 1px; background: var(--xrvu-sep); margin: 6px 1px; align-self: stretch; }
#xrvu-anim-menu {
  position: absolute !important; bottom: calc(100% + 10px) !important; left: 50% !important; transform: translateX(-50%) !important;
  z-index: 40 !important; background: var(--xrvu-glass-bg) !important; backdrop-filter: blur(18px) saturate(1.8) !important;
  -webkit-backdrop-filter: blur(18px) saturate(1.8) !important; border: 1px solid var(--xrvu-glass-bd) !important;
  border-radius: 12px !important; padding: 6px !important; min-width: 176px !important;
  display: none !important; flex-direction: column !important; gap: 2px !important;
  box-shadow: 0 16px 40px rgba(0,0,0,.7) !important;
}
#xrvu-anim-menu.open { display: flex !important; }
.xrvu-anim-item { all: unset !important; box-sizing: border-box !important; display: flex !important; align-items: center !important; gap: 9px !important; padding: 7px 10px !important; border-radius: 8px !important; font-size: 11px !important; color: var(--xrvu-muted) !important; cursor: pointer !important; white-space: nowrap !important; transition: background .12s, color .12s !important; }
.xrvu-anim-item:hover { background: rgba(255,255,255,.07) !important; color: var(--xrvu-text) !important; }
.xrvu-anim-item.active { color: var(--xrvu-accent) !important; background: rgba(29,242,214,.07) !important; }
.xrvu-anim-item svg { width: 13px; height: 13px; flex-shrink: 0; pointer-events: none; }
#xrvu-panel-transform, #xrvu-panel-settings, #xrvu-panel-media, #xrvu-panel-triggers {
  position: absolute !important; bottom: 58px !important; left: 50% !important; transform: translateX(-50%) !important;
  z-index: 35 !important; background: var(--xrvu-glass-bg) !important; backdrop-filter: blur(18px) saturate(1.8) !important;
  -webkit-backdrop-filter: blur(18px) saturate(1.8) !important; border: 1px solid var(--xrvu-glass-bd) !important;
  border-radius: 14px !important; padding: 14px 16px !important; min-width: 260px !important;
  display: none !important; flex-direction: column !important; gap: 10px !important; box-shadow: 0 16px 40px rgba(0,0,0,.7) !important;
}
#xrvu-panel-media { min-width: 290px !important; max-height: 72vh !important; overflow-y: auto !important; }
#xrvu-panel-triggers { left: auto !important; right: 12px !important; transform: none !important; min-width: 230px !important; }
#xrvu-panel-transform.open, #xrvu-panel-settings.open, #xrvu-panel-media.open, #xrvu-panel-triggers.open { display: flex !important; }
.xrvu-panel-title { font-size: 9px !important; letter-spacing: .12em !important; text-transform: uppercase !important; color: var(--xrvu-dim) !important; margin-bottom: 2px !important; }
.xrvu-panel-subtitle { font-size: 9px !important; letter-spacing: .09em !important; text-transform: uppercase !important; color: var(--xrvu-accent) !important; }
.xrvu-row { display: flex !important; align-items: center !important; gap: 8px !important; }
.xrvu-row label { font-size: 10px !important; color: var(--xrvu-dim) !important; width: 46px !important; flex-shrink: 0 !important; text-align: right !important; }
.xrvu-row input[type=range] { all: unset !important; flex: 1 !important; height: 2px !important; background: rgba(255,255,255,.14) !important; border-radius: 2px !important; cursor: pointer !important; -webkit-appearance: none !important; appearance: none !important; accent-color: var(--xrvu-accent) !important; }
.xrvu-row input[type=range]::-webkit-slider-thumb { -webkit-appearance: none !important; width: 12px !important; height: 12px !important; border-radius: 50% !important; background: var(--xrvu-accent) !important; cursor: pointer !important; }
.xrvu-row input[type=range]::-moz-range-thumb { width: 12px !important; height: 12px !important; border-radius: 50% !important; background: var(--xrvu-accent) !important; border: none !important; cursor: pointer !important; }
.xrvu-val { font-size: 9px !important; color: var(--xrvu-muted) !important; width: 32px !important; text-align: right !important; flex-shrink: 0 !important; font-variant-numeric: tabular-nums !important; }
#xrvu-lock-btn { all: unset !important; box-sizing: border-box !important; width: 22px !important; height: 22px !important; border-radius: 5px !important; border: 1px solid var(--xrvu-glass-bd) !important; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer !important; color: var(--xrvu-dim) !important; flex-shrink: 0 !important; transition: color .14s, border-color .14s !important; }
#xrvu-lock-btn svg { width: 11px; height: 11px; }
#xrvu-lock-btn.locked { color: var(--xrvu-accent) !important; border-color: var(--xrvu-accent) !important; }
.xrvu-panel-sep { height: 1px; background: var(--xrvu-sep); margin: 2px 0; }
.xrvu-num-input { all: unset !important; box-sizing: border-box !important; width: 58px !important; padding: 2px 5px !important; background: rgba(255,255,255,.06) !important; border: 1px solid rgba(255,255,255,.1) !important; border-radius: 5px !important; font-size: 9px !important; color: var(--xrvu-text) !important; text-align: right !important; cursor: text !important; font-variant-numeric: tabular-nums !important; -moz-appearance: textfield !important; }
.xrvu-num-input::-webkit-inner-spin-button, .xrvu-num-input::-webkit-outer-spin-button { -webkit-appearance: none !important; }
.xrvu-num-input:focus { border-color: var(--xrvu-accent) !important; outline: none !important; }
.xrvu-url-input { all: unset !important; box-sizing: border-box !important; width: 100% !important; padding: 6px 10px !important; background: rgba(255,255,255,.06) !important; border: 1px solid rgba(255,255,255,.1) !important; border-radius: 8px !important; font-size: 10px !important; color: var(--xrvu-text) !important; cursor: text !important; }
.xrvu-url-input::placeholder { color: var(--xrvu-dim) !important; }
.xrvu-url-input:focus { border-color: var(--xrvu-accent) !important; outline: none !important; }
.xrvu-proj-row { display: flex !important; gap: 4px !important; flex-wrap: wrap !important; }
.xrvu-proj-btn { all: unset !important; box-sizing: border-box !important; padding: 3px 9px !important; border-radius: 6px !important; border: 1px solid var(--xrvu-glass-bd) !important; font-size: 9px !important; color: var(--xrvu-muted) !important; cursor: pointer !important; transition: all .12s !important; }
.xrvu-proj-btn:hover { background: rgba(255,255,255,.07) !important; color: var(--xrvu-text) !important; }
.xrvu-proj-btn.active { background: rgba(29,242,214,.1) !important; color: var(--xrvu-accent) !important; border-color: var(--xrvu-accent) !important; }
.xrvu-apply-btn { all: unset !important; box-sizing: border-box !important; width: 100% !important; padding: 6px 0 !important; border-radius: 8px !important; background: rgba(0,16,239,.25) !important; border: 1px solid rgba(0,16,239,.5) !important; font-size: 10px !important; color: var(--xrvu-text) !important; cursor: pointer !important; text-align: center !important; transition: background .12s !important; }
.xrvu-apply-btn:hover { background: rgba(0,16,239,.42) !important; }
.xrvu-apply-btn.danger { background: rgba(180,20,20,.25) !important; border-color: rgba(180,20,20,.5) !important; }
.xrvu-apply-btn.danger:hover { background: rgba(180,20,20,.45) !important; }
#xrvu-btn-sound { position: absolute !important; bottom: 14px !important; left: 14px !important; z-index: 25 !important; all: unset !important; box-sizing: border-box !important; width: 34px !important; height: 34px !important; border-radius: 999px !important; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer !important; color: var(--xrvu-muted) !important; background: var(--xrvu-glass-bg) !important; backdrop-filter: blur(14px) !important; -webkit-backdrop-filter: blur(14px) !important; border: 1px solid var(--xrvu-glass-bd) !important; box-shadow: 0 8px 24px rgba(0,0,0,.5) !important; transition: color .14s, transform .14s !important; }
#xrvu-btn-sound svg { width: 15px; height: 15px; pointer-events: none; }
#xrvu-btn-sound:hover { transform: translateY(-1px) !important; color: var(--xrvu-text) !important; }
#xrvu-btn-sound.active { color: var(--xrvu-accent) !important; }
#xrvu-qr-panel { position: absolute; bottom: 58px; right: 12px; z-index: 26; background: var(--xrvu-glass-bg); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid var(--xrvu-glass-bd); border-radius: 14px; padding: 10px; text-align: center; display: none; }
#xrvu-qr-panel p { font-size: 9px; color: var(--xrvu-muted); letter-spacing: .08em; text-transform: uppercase; margin-top: 6px; }
#xrvu-reticule { position: absolute; inset: 0; z-index: 15; display: none; align-items: center; justify-content: center; pointer-events: none; }
#xrvu-reticule svg { width: 80px; height: 80px; opacity: .7; }
.xrvu-toast { position: absolute; bottom: 70px; left: 50%; transform: translateX(-50%); z-index: 40; background: var(--xrvu-glass-bg); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid var(--xrvu-glass-bd); border-radius: 12px; padding: 9px 18px; font-size: 11px; color: var(--xrvu-muted); pointer-events: none; white-space: nowrap; animation: xrvu-fadeup .25s ease; }
@keyframes xrvu-fadeup { from { opacity:0; transform: translateX(-50%) translateY(8px); } }
.xrvu-hint { font-size: 9px; color: rgba(255,255,255,.25); letter-spacing: .06em; text-transform: uppercase; }
.xrvu-hint b { font-weight: 500; color: rgba(255,255,255,.4); background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.1); border-radius: 3px; padding: 1px 5px; margin-right: 3px; }
#xrvu-gps-coords { font-size: 9px !important; color: var(--xrvu-dim) !important; font-variant-numeric: tabular-nums !important; }
@media (max-width: 480px) {
  #xrvu-root { height: 380px !important; }
  #xrvu-bar { padding: 4px !important; gap: 2px !important; }
  #xrvu-root button.xrvu-btn { width: 30px !important; height: 30px !important; }
  #xrvu-btn-anim { padding: 0 8px !important; font-size: 9px !important; }
  #xrvu-panel-transform, #xrvu-panel-settings, #xrvu-panel-media { min-width: 200px !important; padding: 10px 12px !important; }
}
</style>

<div id="xrvu-root">
  <video id="xrvu-bg-video" autoplay muted loop playsinline style="display:none;"></video>
  <div id="xrvu-bg"></div>
  <div id="xrvu-gs-stage"    style="display:none;"></div>
  <div id="xrvu-three-stage" style="display:none;"></div>

  <div id="xrvu-overlay">
    <div id="xrvu-spinner"></div>
    <div id="xrvu-status">Initialisation</div>
    <div id="xrvu-barwrap"><div id="xrvu-progressbar"></div></div>
  </div>

  <div id="xrvu-brand" style="display:none;">
    <img decoding="async" id="xrvu-brand-logo" src="" alt="logo"/>
    <span id="xrvu-brand-title"></span>
  </div>

  <div id="xrvu-reticule">
    <svg viewBox="0 0 80 80" fill="none">
      <circle cx="40" cy="40" r="30" stroke="#1df2d6" stroke-width="1.5" stroke-dasharray="4 4"/>
      <line x1="40" y1="10" x2="40" y2="20" stroke="#1df2d6" stroke-width="1.5"/>
      <line x1="40" y1="60" x2="40" y2="70" stroke="#1df2d6" stroke-width="1.5"/>
      <line x1="10" y1="40" x2="20" y2="40" stroke="#1df2d6" stroke-width="1.5"/>
      <line x1="60" y1="40" x2="70" y2="40" stroke="#1df2d6" stroke-width="1.5"/>
    </svg>
  </div>

  <div id="xrvu-qr-panel"><div id="xrvu-qr-code"></div><p>Ouvrir en AR / VR</p></div>

  <button id="xrvu-btn-sound" style="display:none;" title="Son">
    <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><polygon points="7,3 3.5,6 2,6 2,10 3.5,10 7,13" fill="currentColor" stroke="currentColor" stroke-width="1"/><path d="M10 5.5a3.5 3.5 0 0 1 0 5M12 3a6.5 6.5 0 0 1 0 10"/></svg>
  </button>

  <div id="xrvu-panel-transform">
    <div class="xrvu-panel-title">Position &amp; Echelle</div>
    <div class="xrvu-row"><label>X</label><input type="range" id="xrvu-tx" min="-5" max="5" step="0.01" value="0"><span class="xrvu-val" id="xrvu-tx-val">0.00</span></div>
    <div class="xrvu-row"><label>Y</label><input type="range" id="xrvu-ty" min="-5" max="5" step="0.01" value="0"><span class="xrvu-val" id="xrvu-ty-val">0.00</span></div>
    <div class="xrvu-row"><label>Z</label><input type="range" id="xrvu-tz" min="-5" max="5" step="0.01" value="0"><span class="xrvu-val" id="xrvu-tz-val">0.00</span></div>
    <div class="xrvu-panel-sep"></div>
    <div class="xrvu-row">
      <label>Echelle</label>
      <input type="range" id="xrvu-tscale" min="-2" max="3" step="0.001" value="0">
      <input type="number" id="xrvu-tscale-num" class="xrvu-num-input" value="1.000" step="0.001">
      <button id="xrvu-lock-btn" class="locked" title="Proportions">
        <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="7" width="10" height="8" rx="1.5"/><path d="M5 7V5.5a3 3 0 0 1 6 0V7"/></svg>
      </button>
    </div>
  </div>

  <div id="xrvu-panel-settings">
    <div class="xrvu-panel-title">Reglages</div>
    <div class="xrvu-row"><label>Ambiant</label><input type="range" id="xrvu-s-ambient" min="0" max="2" step="0.01" value="0.7"><span class="xrvu-val" id="xrvu-s-ambient-val">0.70</span></div>
    <div class="xrvu-row"><label>Lumiere</label><input type="range" id="xrvu-s-light" min="0" max="3" step="0.01" value="0.9"><span class="xrvu-val" id="xrvu-s-light-val">0.90</span></div>
    <div class="xrvu-panel-sep"></div>
    <div class="xrvu-row"><label>Opacite</label><input type="range" id="xrvu-s-opacity" min="0" max="1" step="0.01" value="1"><span class="xrvu-val" id="xrvu-s-opacity-val">1.00</span></div>
    <div class="xrvu-row"><label>Brillance</label><input type="range" id="xrvu-s-roughness" min="0" max="1" step="0.01" value="0.4"><span class="xrvu-val" id="xrvu-s-roughness-val">0.40</span></div>
    <div class="xrvu-row"><label>Reflexion</label><input type="range" id="xrvu-s-metalness" min="0" max="1" step="0.01" value="0.2"><span class="xrvu-val" id="xrvu-s-metalness-val">0.20</span></div>
  </div>

  <div id="xrvu-panel-media">
    <div class="xrvu-panel-title">Medias</div>
    <div class="xrvu-panel-subtitle">Video</div>
    <input type="text" id="xrvu-mv-url" class="xrvu-url-input" placeholder="URL video (mp4, webm...)">
    <div class="xrvu-proj-row" id="xrvu-mv-proj">
      <button class="xrvu-proj-btn active" data-proj="flat">Plat</button>
      <button class="xrvu-proj-btn" data-proj="sphere">360 Sphere</button>
      <button class="xrvu-proj-btn" data-proj="cylinder">Cylindrique</button>
      <button class="xrvu-proj-btn" data-proj="cube">Cubique</button>
    </div>
    <div class="xrvu-row"><label>Taille</label><input type="range" id="xrvu-mv-size" min="0.5" max="20" step="0.1" value="3"><span class="xrvu-val" id="xrvu-mv-size-val">3.0</span></div>
    <button class="xrvu-apply-btn" id="xrvu-mv-apply">Charger video</button>
    <button class="xrvu-apply-btn danger" id="xrvu-mv-remove" style="display:none;">Retirer video</button>
    <div class="xrvu-panel-sep"></div>
    <div class="xrvu-panel-subtitle">Image</div>
    <input type="text" id="xrvu-mi-url" class="xrvu-url-input" placeholder="URL image (jpg, png, webp...)">
    <div class="xrvu-proj-row" id="xrvu-mi-proj">
      <button class="xrvu-proj-btn active" data-proj="flat">Plat</button>
      <button class="xrvu-proj-btn" data-proj="sphere">360 Sphere</button>
      <button class="xrvu-proj-btn" data-proj="cylinder">Cylindrique</button>
      <button class="xrvu-proj-btn" data-proj="cube">Cubique</button>
    </div>
    <div class="xrvu-row"><label>Taille</label><input type="range" id="xrvu-mi-size" min="0.5" max="20" step="0.1" value="3"><span class="xrvu-val" id="xrvu-mi-size-val">3.0</span></div>
    <button class="xrvu-apply-btn" id="xrvu-mi-apply">Charger image</button>
    <button class="xrvu-apply-btn danger" id="xrvu-mi-remove" style="display:none;">Retirer image</button>
    <div class="xrvu-panel-sep"></div>
    <div class="xrvu-panel-subtitle">Audio</div>
    <input type="text" id="xrvu-ma-url" class="xrvu-url-input" placeholder="URL audio (mp3, ogg...)">
    <div class="xrvu-row"><label>Volume</label><input type="range" id="xrvu-ma-vol" min="0" max="1" step="0.01" value="0.8"><span class="xrvu-val" id="xrvu-ma-vol-val">0.80</span></div>
    <button class="xrvu-apply-btn" id="xrvu-ma-apply">Charger audio</button>
  </div>

  <div id="xrvu-panel-triggers">
    <div class="xrvu-panel-title">Triggers XR</div>
    <div class="xrvu-panel-subtitle">GPS</div>
    <div id="xrvu-gps-coords">---, ---</div>
    <div class="xrvu-row"><label style="width:auto;">Rayon m</label><input type="range" id="xrvu-gps-radius" min="5" max="500" step="5" value="50"><span class="xrvu-val" id="xrvu-gps-radius-val">50</span></div>
    <input type="text" id="xrvu-gps-zone-url" class="xrvu-url-input" placeholder="URL contenu a declencher">
    <input type="text" id="xrvu-gps-zone-name" class="xrvu-url-input" style="margin-top:4px;" placeholder="Nom de la zone GPS">
    <div style="display:flex;gap:4px;">
      <button class="xrvu-apply-btn" id="xrvu-gps-watch" style="font-size:9px;">Activer GPS</button>
      <button class="xrvu-apply-btn" id="xrvu-gps-add" style="font-size:9px;">+ Zone ici</button>
    </div>
    <div id="xrvu-gps-zones-list" style="font-size:9px;color:var(--xrvu-dim);max-height:80px;overflow-y:auto;"></div>
    <div class="xrvu-panel-sep"></div>
    <div class="xrvu-panel-subtitle">Image Tracker</div>
    <input type="text" id="xrvu-it-url" class="xrvu-url-input" placeholder="URL image cible (jpg/png)">
    <div class="xrvu-row"><label style="width:auto;">Largeur m</label><input type="range" id="xrvu-it-width" min="0.05" max="1" step="0.01" value="0.2"><span class="xrvu-val" id="xrvu-it-width-val">0.20</span></div>
    <input type="text" id="xrvu-it-content" class="xrvu-url-input" style="margin-top:4px;" placeholder="URL contenu (modele, video...)">
    <button class="xrvu-apply-btn" id="xrvu-it-add">Ajouter tracker</button>
    <div id="xrvu-it-list" style="font-size:9px;color:var(--xrvu-dim);"></div>
  </div>

  <div id="xrvu-bar">
    <div style="position:relative;">
      <button class="xrvu-btn" id="xrvu-btn-anim" title="Animations">
        <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="3.5" width="13" height="9" rx="1.5"/><path d="M5 3.5v9M11 3.5v9M1.5 6.5h3M11.5 6.5h3M1.5 9.5h3M11.5 9.5h3"/></svg>
        &nbsp;Anim
      </button>
      <div id="xrvu-anim-menu">
        <button class="xrvu-anim-item" data-mode="none"><svg viewBox="0 0 16 16" fill="currentColor"><rect x="4" y="4" width="8" height="8" rx="1.5"/></svg>Aucune</button>
        <button class="xrvu-anim-item" data-mode="turntable"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2.5a5.5 5.5 0 1 0 5.5 5.5"/><path d="M11 1l3 1.5-1.5 3"/></svg>Tourniquet</button>
        <button class="xrvu-anim-item" data-mode="floating"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><path d="M.5 8c1.5-3.5 3-3.5 4.5 0s3 3.5 4.5 0 3-3.5 4.5 0"/></svg>Flottement</button>
        <button class="xrvu-anim-item" data-mode="breathing"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2.5v5M5 7.5C3.5 7.5 2 8.5 2 10.5s1 3 2.5 3S6.5 12.5 6.5 10.5V7.5M11 7.5c1.5 0 3 1 3 3s-1 3-2.5 3S9.5 12.5 9.5 10.5V7.5"/></svg>Respiration</button>
        <button class="xrvu-anim-item" data-mode="expandcontract"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M1 6V1h5M15 6V1h-5M1 10v5h5M15 10v5h-5"/></svg>Expansion</button>
        <button class="xrvu-anim-item" data-mode="followmouse"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="1.5" width="6" height="10" rx="3"/><line x1="8" y1="1.5" x2="8" y2="5.5"/></svg>Suivre souris</button>
        <button class="xrvu-anim-item" data-mode="smoothorbit"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="8" cy="8" r="6.5"/><circle cx="8" cy="8" r="2" fill="currentColor" stroke="none"/></svg>Orbite douce</button>
        <button class="xrvu-anim-item" data-mode="rotateonscroll"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M14 8A6 6 0 1 1 11 3"/><path d="M14 3v5h-5"/></svg>Scroll</button>
        <button class="xrvu-anim-item" data-mode="pulsezoom"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><circle cx="6.5" cy="6.5" r="5"/><path d="M6.5 4.5v4M4.5 6.5h4M13 13l-3.5-3.5"/></svg>Zoom pulse</button>
        <button class="xrvu-anim-item" data-mode="clickfade"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><path d="M2 2l12 12M6.5 6.7A3 3 0 0 0 9.3 9.5"/><path d="M1 8s2.5-4.5 7-4.5c.9 0 1.7.1 2.5.4"/><path d="M13.5 9.8C12.1 11.5 10.1 12.5 8 12.5c-4.5 0-7-4.5-7-4.5"/></svg>Apparition</button>
      </div>
    </div>
    <div class="xrvu-sep"></div>
    <button class="xrvu-btn" id="xrvu-btn-transform" title="Position / Echelle">
      <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><line x1="1" y1="4" x2="15" y2="4"/><line x1="1" y1="8" x2="15" y2="8"/><line x1="1" y1="12" x2="15" y2="12"/><circle cx="5" cy="4" r="1.8" fill="currentColor" stroke="none"/><circle cx="10" cy="8" r="1.8" fill="currentColor" stroke="none"/><circle cx="6" cy="12" r="1.8" fill="currentColor" stroke="none"/></svg>
    </button>
    <button class="xrvu-btn" id="xrvu-btn-settings" title="Reglages">
      <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><circle cx="8" cy="8" r="2.5"/><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.6 3.6l1.4 1.4M11 11l1.4 1.4M3.6 12.4l1.4-1.4M11 5l1.4-1.4"/></svg>
    </button>
    <button class="xrvu-btn" id="xrvu-btn-media" title="Medias — video, image, audio">
      <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="3" width="14" height="10" rx="1.5"/><path d="M6.5 6l4 2.5-4 2.5V6z" fill="currentColor" stroke="none"/></svg>
    </button>
    <div class="xrvu-sep"></div>
    <button class="xrvu-btn" id="xrvu-btn-ar" title="Realite Augmentee" style="display:none;">
      <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="1" width="8" height="14" rx="2"/><line x1="6" y1="3.5" x2="10" y2="3.5"/><circle cx="8" cy="12.5" r=".8" fill="currentColor" stroke="none"/></svg>
    </button>
    <button class="xrvu-btn" id="xrvu-btn-vr" title="Mode VR" style="display:none;">
      <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="5" width="14" height="7" rx="2"/><circle cx="5.5" cy="8.5" r="1.8"/><circle cx="10.5" cy="8.5" r="1.8"/><line x1="7.3" y1="8.5" x2="8.7" y2="8.5"/></svg>
    </button>
    <button class="xrvu-btn" id="xrvu-btn-triggers" title="Triggers GPS / Image tracker">
      <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="7" r="3"/><path d="M8 1v2M8 12v3M1 7h2M12 7h3M8 10v2"/></svg>
    </button>
    <button class="xrvu-btn" id="xrvu-btn-qr" title="QR Code">
      <svg viewBox="0 0 16 16" fill="currentColor"><path fill-rule="evenodd" d="M1 1h6v6H1V1zm1.5 1.5v3h3v-3h-3z"/><path fill-rule="evenodd" d="M9 1h6v6H9V1zm1.5 1.5v3h3v-3h-3z"/><path fill-rule="evenodd" d="M1 9h6v6H1V9zm1.5 1.5v3h3v-3h-3z"/><rect x="9" y="9" width="2" height="2"/><rect x="13" y="9" width="2" height="2"/><rect x="9" y="13" width="6" height="2"/><rect x="11" y="11" width="2" height="2"/></svg>
    </button>
    <button class="xrvu-btn" id="xrvu-btn-fs" title="Plein ecran">
      <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 6V2h4M15 6V2h-4M1 10v4h4M15 10v4h-4"/></svg>
    </button>
  </div>

  <div id="xrvu-instructions">
    <span class="xrvu-hint"><b>Glisser</b>Rotation</span>
    <span class="xrvu-hint"><b>Scroll</b>Zoom</span>
    <span class="xrvu-hint"><b>Shift+drag</b>Pan</span>
    <span class="xrvu-hint"><b>Dbl-clic</b>Reset</span>
  </div>
</div>

<script type="importmap">
{
  "imports": {
    "three":         "https://cdn.jsdelivr.net/npm/three@0.167.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.167.0/examples/jsm/",
    "gsplat":        "https://cdn.jsdelivr.net/npm/gsplat@1.2.9/dist/index.es.js"
  }
}
</script>

<script type="module">
import * as THREE                   from "three";
import { OrbitControls }            from "three/addons/controls/OrbitControls.js";
import { GLTFLoader }               from "three/addons/loaders/GLTFLoader.js";
import { OBJLoader }                from "three/addons/loaders/OBJLoader.js";
import { FBXLoader }                from "three/addons/loaders/FBXLoader.js";
import { STLLoader }                from "three/addons/loaders/STLLoader.js";
import { XRButton }                 from "three/addons/webxr/XRButton.js";
import { XRControllerModelFactory } from "three/addons/webxr/XRControllerModelFactory.js";
import { OculusHandModel }          from "three/addons/webxr/OculusHandModel.js";

const ICONS = {
  expand:    `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 6V2h4M15 6V2h-4M1 10v4h4M15 10v4h-4"/></svg>`,
  compress:  `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 1v4H1M11 1v4h4M5 15v-4H1M11 15v-4h4"/></svg>`,
  volumeOn:  `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><polygon points="7,3 3.5,6 2,6 2,10 3.5,10 7,13" fill="currentColor" stroke="currentColor" stroke-width="1"/><path d="M10 5.5a3.5 3.5 0 0 1 0 5M12 3a6.5 6.5 0 0 1 0 10"/></svg>`,
  volumeOff: `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><polygon points="7,3 3.5,6 2,6 2,10 3.5,10 7,13" fill="currentColor" stroke="currentColor" stroke-width="1"/><path d="M11 7l3 3M14 7l-3 3"/></svg>`,
  lock:      `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="7" width="10" height="8" rx="1.5"/><path d="M5 7V5.5a3 3 0 0 1 6 0V7"/></svg>`,
  lockOpen:  `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="7" width="10" height="8" rx="1.5"/><path d="M5 7V5.5a3 3 0 0 1 6 0"/></svg>`,
};

/* ═══════════════════════════════════════════════
   CONFIG — modifier uniquement cette section
   ═══════════════════════════════════════════════ */
const CONFIG = {
  model:      { url: 'https://presentcomposedesign.fr/wp-content/uploads/2026/04/LABYX1PresentComposedesign.glb', format: 'auto' },
  background: { color: '#08080e', opacity: 1.0, image: null, video: null },
  overlay:    { logo: null, title: null, titleColor: '#ffffff' },
  commands:   { display: true },
  animation:  { mode: 'followmouse', speed: 0.4 },
  transform:  { x: 0, y: 0, z: 0, scale: 1.0 },
  lights:     { ambient: 0.7, directional: 0.9 },
  material:   { opacity: 1.0, roughness: 0.4, metalness: 0.2 },
  media:      { sound: null, soundAutoplay: false, soundLoop: true, image: null },
  xr:         { ar: true, vr: true, gyroscope: true, hitTest: true },
  editorMode: true,
  hud:        { autoHide: true, autoHideDelay: 1500 },
  vr:         { handInteractions: true, teleport: true },
  triggers:   { gps: { enabled: false, radius: 50, zones: [] }, imageTracker: { enabled: false, targets: [] } },
};

/* ─── Helpers ─── */
const $ = id => document.getElementById(id);
const lbl = t   => { $('xrvu-status').className=''; $('xrvu-status').textContent=t; };
const prg = p   => { $('xrvu-progressbar').style.width=Math.min(100,Math.round(p*100))+'%'; };
const err = msg => { $('xrvu-spinner').style.cssText='animation:none;border-top-color:rgba(224,85,85,.4)'; $('xrvu-status').className='error'; $('xrvu-status').textContent=msg; console.error('[XRVU]',msg); };
function toast(msg,ms=2400){ const t=document.createElement('div'); t.className='xrvu-toast'; t.textContent=msg; $('xrvu-root').appendChild(t); setTimeout(()=>{ t.style.opacity='0'; t.style.transition='opacity .35s'; setTimeout(()=>t.remove(),380); },ms); }
function overlayFade(){ const o=$('xrvu-overlay'); o.classList.add('fade'); setTimeout(()=>o.classList.add('hidden'),800); }

/* ─── HUD auto-hide ─── */
let _firstInteraction=false, _hudTimer=null;
function onAnyInteraction(){
  if(!_firstInteraction){
    _firstInteraction=true;
    const inst=$('xrvu-instructions');
    if(inst){ inst.classList.add('inst-hidden'); setTimeout(()=>inst.style.display='none',450); }
  }
  if(!CONFIG.hud.autoHide) return;
  const bar=$('xrvu-bar');
  bar.classList.remove('hud-hidden');
  clearTimeout(_hudTimer);
  _hudTimer=setTimeout(()=>bar.classList.add('hud-hidden'), CONFIG.hud.autoHideDelay);
}
['mousemove','touchstart','click','keydown'].forEach(ev=>
  $('xrvu-root').addEventListener(ev, onAnyInteraction, {passive:true})
);

/* ═══════════════════════════════════════════════
   ENGINE Three.js
   ═══════════════════════════════════════════════ */
let threeRenderer, threeScene, threeCamera, threeControls, threeModelGroup=null;
let ambientLight, keyLight, fillLight;
let xrHitTestSource=null, xrRefSpace=null, xrSession=null, hand1, hand2;

function initThree(){
  const container=$('xrvu-three-stage');
  const wasHidden=container.style.display==='none';
  if(wasHidden) container.style.visibility='hidden';
  container.style.display='block';
  const W=container.clientWidth||$('xrvu-root').clientWidth||800;
  const H=container.clientHeight||$('xrvu-root').clientHeight||520;
  if(wasHidden){ container.style.display='none'; container.style.visibility=''; }
  threeScene=new THREE.Scene(); applyBackground();
  threeCamera=new THREE.PerspectiveCamera(55,W/H,0.01,2000); threeCamera.position.set(0,0.5,3);
  threeRenderer=new THREE.WebGLRenderer({antialias:true,alpha:true,premultipliedAlpha:false});
  threeRenderer.setSize(W,H); threeRenderer.setPixelRatio(Math.min(2,devicePixelRatio));
  threeRenderer.xr.enabled=true; threeRenderer.shadowMap.enabled=true; threeRenderer.shadowMap.type=THREE.PCFSoftShadowMap;
  container.appendChild(threeRenderer.domElement);
  ambientLight=new THREE.AmbientLight(0xffffff,CONFIG.lights.ambient); threeScene.add(ambientLight);
  keyLight=new THREE.DirectionalLight(0xffffff,CONFIG.lights.directional); keyLight.position.set(5,10,7); keyLight.castShadow=true; threeScene.add(keyLight);
  fillLight=new THREE.DirectionalLight(0x8899ff,0.3); fillLight.position.set(-5,5,-5); threeScene.add(fillLight);
  threeControls=new OrbitControls(threeCamera,threeRenderer.domElement);
  threeControls.enableDamping=true; threeControls.dampingFactor=0.06;
  threeControls.addEventListener('start', onAnyInteraction);
  threeControls.addEventListener('start', onUserInteract);
  setupXRButtons();
  const cmf=new XRControllerModelFactory();
  [0,1].forEach(i=>{ const g=threeRenderer.xr.getControllerGrip(i); g.add(cmf.createControllerModel(g)); threeScene.add(g); });
  hand1=threeRenderer.xr.getHand(0); hand1.add(new OculusHandModel(hand1)); threeScene.add(hand1);
  hand2=threeRenderer.xr.getHand(1); hand2.add(new OculusHandModel(hand2)); threeScene.add(hand2);
  setupVRInteractions();
  new ResizeObserver(()=>{ const w=container.clientWidth,h=container.clientHeight; threeCamera.aspect=w/h; threeCamera.updateProjectionMatrix(); threeRenderer.setSize(w,h); }).observe(container);
  threeRenderer.setAnimationLoop(threeLoop);
}

function applyBackground(){
  if(!threeScene) return;
  const c=CONFIG.background;
  if(c.color&&c.opacity>0){ const col=new THREE.Color(c.color); threeRenderer?.setClearColor(col,c.opacity); threeScene.background=c.opacity===1?col:null; }
  else { threeRenderer?.setClearColor(0x000000,0); threeScene.background=null; }
  if(c.image){ $('xrvu-bg').style.backgroundImage=`url(${c.image})`; $('xrvu-bg').style.opacity=String(1-(c.opacity||0)); }
  if(c.video){ const v=$('xrvu-bg-video'); v.src=c.video; v.style.display='block'; v.style.opacity=String(1-(c.opacity||0)); }
}

/* ═══════════════════════════════════════════════
   ENGINE Gaussian Splats
   ═══════════════════════════════════════════════ */
let GSRenderer=null,GSScene=null,GSCamera=null,GSOrbitControls=null,GSLoader=null,_gsplat_ready=false;
async function loadGsplatLib(){
  if(_gsplat_ready) return true;
  try {
    const gs=await import("gsplat");
    GSRenderer=gs.WebGLRenderer||gs.Renderer||gs.default?.WebGLRenderer||gs.default?.Renderer;
    GSScene=gs.Scene||gs.default?.Scene; GSCamera=gs.Camera||gs.default?.Camera;
    GSOrbitControls=gs.OrbitControls||gs.Controls||gs.default?.OrbitControls; GSLoader=gs.Loader||gs.default?.Loader;
    if(!GSRenderer||!GSLoader) throw new Error('exports manquants');
    _gsplat_ready=true; return true;
  } catch(e){ console.warn('[XRVU] gsplat:',e.message); return false; }
}

let gsRenderer,gsScene,gsCamera,gsControls;
let gsSceneRadius=1.0,gsCamInitialized=false,gsAnim_angle=0;
const GS_MS=1000/30; let gsLastFrameTime=0;
const GS_CLK={prev:0};

async function initGS(){
  const ok=await loadGsplatLib(); if(!ok) return;
  const container=$('xrvu-gs-stage');
  try {
    const gsCanvas=document.createElement('canvas');
    gsCanvas.style.cssText='width:100%;height:100%;display:block;touch-action:none;';
    container.appendChild(gsCanvas);
    gsRenderer=new GSRenderer(gsCanvas); gsScene=new GSScene();
    gsCamera=GSCamera?new GSCamera():null;
    gsControls=(GSOrbitControls&&gsCamera)?new GSOrbitControls(gsCamera,gsCanvas):null;
    if(gsRenderer.setPixelRatio) gsRenderer.setPixelRatio(Math.min(1.0,devicePixelRatio));
    else if(gsRenderer.renderer?.setPixelRatio) gsRenderer.renderer.setPixelRatio(Math.min(1.0,devicePixelRatio));
  } catch(e){ console.warn('[XRVU] gsplat init:',e.message); gsScene=null; return; }
  new ResizeObserver(()=>{ if(gsRenderer?.setSize){ const w=container.clientWidth||800,h=container.clientHeight||520; gsRenderer.setSize(w,h); } }).observe(container);
  gsLoop();
}

function gsLoop(ts=0){
  requestAnimationFrame(gsLoop);
  if(!gsRenderer||!gsScene) return;
  if(ts-gsLastFrameTime<GS_MS) return;
  gsLastFrameTime=ts;
  const dt=Math.min((ts-GS_CLK.prev)/1000,0.1); GS_CLK.prev=ts;
  const wasAnimated=applyGSAnimation(dt,ts/1000);
  if(!wasAnimated) gsControls?.update();
  gsRenderer.render(gsScene,gsCamera);
}

function initGSCamera(){
  if(!gsCamera||gsCamInitialized) return;
  gsCamera.position.set(0,gsSceneRadius*0.3,gsSceneRadius*3.0);
  if(gsCamera.lookAt) gsCamera.lookAt(0,0,0);
  if(gsControls?.target?.set) gsControls.target.set(0,0,0);
  if(gsControls?.update) gsControls.update();
  gsCamInitialized=true;
}

/* ═══════════════════════════════════════════════
   LOADERS
   ═══════════════════════════════════════════════ */
let activeEngine=null;
function detectFormat(url,hint){
  if(hint&&hint!=='auto') return hint;
  const low=(url||'').toLowerCase().split('?')[0];
  if(low.endsWith('.spz'))                        return 'spz';
  if(low.endsWith('.ply'))                        return 'ply';
  if(low.endsWith('.splat'))                      return 'splat';
  if(low.endsWith('.glb')||low.endsWith('.gltf')) return 'gltf';
  if(low.endsWith('.obj'))                        return 'obj';
  if(low.endsWith('.fbx'))                        return 'fbx';
  if(low.endsWith('.stl'))                        return 'stl';
  return 'iframe';
}
async function loadModel(url,hint){
  if(!url){ const p=new URLSearchParams(location.search); if(p.has('model')) url=p.get('model'); }
  const fmt=detectFormat(url,hint);
  console.log('[XRVU] Format:',fmt,url); lbl('Chargement...'); prg(0.02);
  try {
    if(fmt==='spz')                     await loadGS_SPZ(url);
    else if(fmt==='ply'||fmt==='splat') await loadGS_Direct(url,fmt);
    else if(fmt==='gltf')               await loadThreeMesh_GLTF(url);
    else if(fmt==='obj')                await loadThreeMesh_OBJ(url);
    else if(fmt==='fbx')                await loadThreeMesh_FBX(url);
    else if(fmt==='stl')                await loadThreeMesh_STL(url);
    else                                loadIframe(url);
  } catch(e){ err(e.message||String(e)); }
}
function showEngine(type){ $('xrvu-gs-stage').style.display=type==='gs'?'block':'none'; $('xrvu-three-stage').style.display=type==='three'?'block':'none'; activeEngine=type; }

/* ─── SPZ decoder ─── */
async function loadGS_SPZ(url){
  if(!gsScene){ await initGS(); } if(!gsScene) throw new Error('gsplat indisponible');
  lbl('Telechargement SPZ...'); prg(0.05);
  const resp=await fetch(url,{mode:'cors'}); if(!resp.ok) throw new Error(`HTTP ${resp.status}`);
  const compressed=await resp.arrayBuffer(); prg(0.15);
  lbl('Decompression...'); prg(0.2);
  if(!window.DecompressionStream) throw new Error('DecompressionStream absent');
  const ds=new DecompressionStream('gzip'); const chunks=[];
  const readTask=(async()=>{ const rd=ds.readable.getReader(); for(;;){ const{done,value}=await rd.read(); if(done) break; chunks.push(value); } })();
  const wr=ds.writable.getWriter(); await wr.write(new Uint8Array(compressed)); await wr.close(); await readTask;
  const rawLen=chunks.reduce((s,c)=>s+c.length,0); const raw=new Uint8Array(rawLen);
  let off=0; for(const c of chunks){ raw.set(c,off); off+=c.length; }
  prg(0.35);
  const dv=new DataView(raw.buffer);
  const magic=dv.getUint32(0,true); if(magic!==0x5053474e) throw new Error('Magic SPZ invalide');
  const version=dv.getUint32(4,true),N=dv.getUint32(8,true);
  const shDeg=raw[12],fracBits=raw[13];
  const shDims=[0,3,8,15,24][Math.min(shDeg,4)];
  const posScale=1.0/(1<<fracBits);
  console.log('[XRVU] SPZ v'+version+' — '+N+' pts'); let p=16;
  lbl('Positions...'); prg(0.4);
  const pos=new Float32Array(N*3);
  if(version===1){ p+=N*6; }
  else { for(let i=0;i<N;i++){ for(let j=0;j<3;j++){ let v=raw[p]|(raw[p+1]<<8)|(raw[p+2]<<16); p+=3; if(v&0x800000) v|=0xFF000000; pos[i*3+j]=(v|0)*posScale; } pos[i*3+1]*=-1; pos[i*3+2]*=-1; } }
  let _normMx=1.0;
  { let sx=0,sy=0,sz=0; for(let i=0;i<N;i++){ sx+=pos[i*3]; sy+=pos[i*3+1]; sz+=pos[i*3+2]; }
    const cx=sx/N,cy=sy/N,cz=sz/N; let mx=0;
    for(let i=0;i<N;i++){ const dx=pos[i*3]-cx,dy=pos[i*3+1]-cy,dz=pos[i*3+2]-cz; mx=Math.max(mx,dx*dx+dy*dy+dz*dz); }
    mx=Math.sqrt(mx);
    for(let i=0;i<N;i++){ pos[i*3]-=cx; pos[i*3+1]-=cy; pos[i*3+2]-=cz; }
    if(mx>0.0001){ const inv=1/mx; for(let i=0;i<N;i++){ pos[i*3]*=inv; pos[i*3+1]*=inv; pos[i*3+2]*=inv; } _normMx=mx; }
    gsSceneRadius=1.0; }
  const opac=new Float32Array(N);
  for(let i=0;i<N;i++){ const a=Math.max(1e-7,Math.min(1-1e-7,raw[p++]/255)); opac[i]=Math.log(a/(1-a)); }
  const col=new Float32Array(N*3);
  for(let i=0;i<N*3;i++) col[i]=(raw[p++]/255-0.5)/0.15;
  const scl=new Float32Array(N*3);
  for(let i=0;i<N*3;i++) scl[i]=raw[p++]/16-10;
  if(_normMx>0.0001){ const logInv=-Math.log(_normMx); for(let i=0;i<N*3;i++) scl[i]+=logInv; }
  lbl('Rotations...'); prg(0.55);
  const rot=new Float32Array(N*4);
  if(version<=2){ for(let i=0;i<N;i++){ const rx=raw[p++]/127.5-1,ry=raw[p++]/127.5-1,rz=raw[p++]/127.5-1; rot[i*4]=Math.sqrt(Math.max(0,1-rx*rx-ry*ry-rz*rz)); rot[i*4+1]=rx; rot[i*4+2]=-ry; rot[i*4+3]=-rz; } }
  else { const S=Math.SQRT1_2/511; for(let i=0;i<N;i++){ const pk=dv.getUint32(p,true); p+=4; const li=pk&3; const dc=b=>((b>>9)&1?-1:1)*(b&511)*S; const a_=dc((pk>>2)&1023),b_=dc((pk>>12)&1023),c_=dc((pk>>22)&1023); const w_=Math.sqrt(Math.max(0,1-a_*a_-b_*b_-c_*c_)); const q=[0,0,0,0]; let si=0; for(let j=0;j<4;j++) q[j]=j===li?w_:[a_,b_,c_][si++]; rot[i*4]=q[0]; rot[i*4+1]=q[1]; rot[i*4+2]=-q[2]; rot[i*4+3]=-q[3]; } }
  prg(0.65);
  const sh=new Float32Array(N*shDims*3);
  for(let i=0;i<N*shDims*3;i++) sh[i]=(raw[p++]-128)/128;
  lbl('Construction PLY...'); prg(0.7);
  let hdr='ply\nformat binary_little_endian 1.0\nelement vertex '+N+'\n';
  hdr+='property float x\nproperty float y\nproperty float z\nproperty float nx\nproperty float ny\nproperty float nz\n';
  hdr+='property float f_dc_0\nproperty float f_dc_1\nproperty float f_dc_2\n';
  for(let j=0;j<shDims*3;j++) hdr+='property float f_rest_'+j+'\n';
  hdr+='property float opacity\nproperty float scale_0\nproperty float scale_1\nproperty float scale_2\n';
  hdr+='property float rot_0\nproperty float rot_1\nproperty float rot_2\nproperty float rot_3\nend_header\n';
  const nP=3+3+3+shDims*3+1+3+4;
  const hB=new TextEncoder().encode(hdr);
  const plyBuf=new ArrayBuffer(hB.length+N*nP*4); new Uint8Array(plyBuf).set(hB);
  const out=new DataView(plyBuf,hB.length); let op=0;
  for(let i=0;i<N;i++){
    out.setFloat32(op,pos[i*3],true); op+=4; out.setFloat32(op,pos[i*3+1],true); op+=4; out.setFloat32(op,pos[i*3+2],true); op+=4;
    out.setFloat32(op,0,true); op+=4; out.setFloat32(op,0,true); op+=4; out.setFloat32(op,0,true); op+=4;
    out.setFloat32(op,col[i*3],true); op+=4; out.setFloat32(op,col[i*3+1],true); op+=4; out.setFloat32(op,col[i*3+2],true); op+=4;
    for(let j=0;j<shDims*3;j++){ out.setFloat32(op,sh[i*shDims*3+j],true); op+=4; }
    out.setFloat32(op,opac[i],true); op+=4;
    out.setFloat32(op,scl[i*3],true); op+=4; out.setFloat32(op,scl[i*3+1],true); op+=4; out.setFloat32(op,scl[i*3+2],true); op+=4;
    out.setFloat32(op,rot[i*4],true); op+=4; out.setFloat32(op,rot[i*4+1],true); op+=4; out.setFloat32(op,rot[i*4+2],true); op+=4; out.setFloat32(op,rot[i*4+3],true); op+=4;
  }
  prg(0.8); lbl('Rendu 3D...');
  const plyFile=new File([plyBuf],'scene.ply',{type:'application/octet-stream'});
  showEngine('gs');
  await GSLoader.LoadFromFileAsync(plyFile,gsScene,pp=>prg(0.8+pp*0.19));
  prg(1); gsCamInitialized=false; initGSCamera();
  overlayFade(); console.log('[XRVU] GS —',N,'gaussians SPZ v'+version);
  setAnimation(CONFIG.animation.mode);
}

async function loadGS_Direct(url,fmt){
  if(!gsScene){ await initGS(); } if(!gsScene) throw new Error('gsplat indisponible');
  lbl('Telechargement '+fmt.toUpperCase()+'...'); prg(0.1);
  const resp=await fetch(url,{mode:'cors'}); if(!resp.ok) throw new Error('HTTP '+resp.status);
  const buf=await resp.arrayBuffer(); prg(0.5); lbl('Rendu 3D...');
  const file=new File([buf],'scene.'+fmt,{type:'application/octet-stream'});
  showEngine('gs');
  await GSLoader.LoadFromFileAsync(file,gsScene,pp=>prg(0.5+pp*0.49));
  prg(1); gsSceneRadius=2.0; gsCamInitialized=false; initGSCamera();
  overlayFade(); setAnimation(CONFIG.animation.mode);
}

let _baseScale=1,_baseY=0;
function showThreeMesh(mesh){
  const box=new THREE.Box3().setFromObject(mesh);
  const center=box.getCenter(new THREE.Vector3());
  const size=box.getSize(new THREE.Vector3()).length();
  mesh.position.sub(center); const nscale=2.0/(size||1); mesh.scale.setScalar(nscale);
  threeModelGroup=new THREE.Group(); threeModelGroup.add(mesh);
  threeModelGroup.position.set(CONFIG.transform.x,CONFIG.transform.y,CONFIG.transform.z);
  _baseScale=nscale*CONFIG.transform.scale; _baseY=CONFIG.transform.y;
  threeModelGroup.scale.setScalar(_baseScale); threeScene.add(threeModelGroup);
  threeCamera.position.set(0,0.5,2.5); threeControls.target.set(0,0,0); threeControls.update();
  applyMaterialConfig(mesh); showEngine('three'); prg(1); overlayFade(); setAnimation(CONFIG.animation.mode);
}
function applyMaterialConfig(obj){
  if(!obj) return;
  obj.traverse(child=>{ if(child.isMesh&&child.material){ const mats=Array.isArray(child.material)?child.material:[child.material]; mats.forEach(m=>{ if(m.roughness!==undefined) m.roughness=CONFIG.material.roughness; if(m.metalness!==undefined) m.metalness=CONFIG.material.metalness; if(m.opacity!==undefined){ m.opacity=CONFIG.material.opacity; m.transparent=CONFIG.material.opacity<1; } m.needsUpdate=true; }); } });
}
function clearThreeMesh(){ if(threeModelGroup){ threeScene.remove(threeModelGroup); threeModelGroup=null; } }
async function loadThreeMesh_GLTF(url){ lbl('Chargement GLB/GLTF...'); clearThreeMesh(); return new Promise((res,rej)=>{ new GLTFLoader().load(url,g=>{ showThreeMesh(g.scene); res(); },e=>prg((e.loaded/e.total)||0),e=>rej(e)); }); }
async function loadThreeMesh_OBJ(url){  lbl('Chargement OBJ...');      clearThreeMesh(); return new Promise((res,rej)=>{ new OBJLoader().load(url,m=>{ showThreeMesh(m); res(); },e=>prg((e.loaded/e.total)||0),e=>rej(e)); }); }
async function loadThreeMesh_FBX(url){  lbl('Chargement FBX...');      clearThreeMesh(); return new Promise((res,rej)=>{ new FBXLoader().load(url,m=>{ showThreeMesh(m); res(); },e=>prg((e.loaded/e.total)||0),e=>rej(e)); }); }
async function loadThreeMesh_STL(url){  lbl('Chargement STL...');      clearThreeMesh(); return new Promise((res,rej)=>{ new STLLoader().load(url,geo=>{ const m=new THREE.Mesh(geo,new THREE.MeshStandardMaterial({color:0xdddddd,roughness:CONFIG.material.roughness,metalness:CONFIG.material.metalness})); showThreeMesh(m); res(); },e=>prg((e.loaded/e.total)||0),e=>rej(e)); }); }
function loadIframe(url){ let src=url; if(url.includes('sketchfab.com/models')){ const id=url.split('/models/')[1]?.split('/')[0]; if(id) src='https://sketchfab.com/models/'+id+'/embed?autostart=1&ui_infos=0'; } const iframe=document.createElement('iframe'); iframe.src=src; iframe.allow='accelerometer;magnetometer;gyroscope;vr;xr-spatial-tracking;fullscreen;autoplay'; iframe.style.cssText='position:absolute;inset:0;width:100%;height:100%;border:0;z-index:10;'; $('xrvu-root').appendChild(iframe); activeEngine='iframe'; overlayFade(); }

/* ═══════════════════════════════════════════════
   MEDIA PROJECTIONS
   ═══════════════════════════════════════════════ */
let _mediaMeshes=[], _mediaVideos=[];

function buildProjectionGeo(projType,size){
  switch(projType){
    case 'sphere':   return new THREE.SphereGeometry(size,64,32);
    case 'cylinder': return new THREE.CylinderGeometry(size,size,size*0.75,64,1,true);
    case 'cube':     return new THREE.BoxGeometry(size,size,size);
    default:         return new THREE.PlaneGeometry(size,size*0.5625);
  }
}

function addVideoProjection(url,projType,size){
  if(!threeScene) return;
  const video=document.createElement('video');
  video.src=url; video.crossOrigin='anonymous'; video.loop=true; video.playsInline=true;
  const tex=new THREE.VideoTexture(video);
  const side=projType==='flat'?THREE.DoubleSide:THREE.BackSide;
  const mesh=new THREE.Mesh(buildProjectionGeo(projType,size),new THREE.MeshBasicMaterial({map:tex,side,toneMapped:false}));
  mesh.position.set(0,projType==='flat'?1.2:0,0);
  threeScene.add(mesh); _mediaMeshes.push(mesh); _mediaVideos.push(video);
  video.play().catch(()=>{ toast('Cliquez pour activer la video'); const once=()=>{ video.play().catch(()=>{}); $('xrvu-root').removeEventListener('click',once); }; $('xrvu-root').addEventListener('click',once); });
  if(activeEngine!=='three') showEngine('three');
}

function addImageProjection(url,projType,size){
  if(!threeScene) return;
  new THREE.TextureLoader().load(url,tex=>{
    tex.colorSpace=THREE.SRGBColorSpace;
    const side=projType==='flat'?THREE.DoubleSide:THREE.BackSide;
    const mesh=new THREE.Mesh(buildProjectionGeo(projType,size),new THREE.MeshBasicMaterial({map:tex,side,toneMapped:false}));
    mesh.position.set(0,projType==='flat'?1.2:0,0);
    threeScene.add(mesh); _mediaMeshes.push(mesh);
    if(activeEngine!=='three') showEngine('three');
  },undefined,()=>toast('Erreur chargement image'));
}

function clearMediaMeshes(){
  _mediaMeshes.forEach(m=>{ m.geometry.dispose(); if(m.material.map) m.material.map.dispose(); m.material.dispose(); threeScene?.remove(m); });
  _mediaMeshes=[];
  _mediaVideos.forEach(v=>{ v.pause(); v.src=''; });
  _mediaVideos=[];
}

/* ═══════════════════════════════════════════════
   VR INTERACTIONS
   ═══════════════════════════════════════════════ */
let vrControllers=[], vrTeleportRing=null, vrTeleportActive=false;
const vrTeleportPoint=new THREE.Vector3();
let vrXRRefSpace=null, vrGrabController=null, vrGrabOffset=new THREE.Matrix4();
let vrBothSqueeze=[false,false], vrInitialSqueezeDist=0, vrInitialSqueezeScale=1;
const _vrRay=new THREE.Raycaster(), _vrMat=new THREE.Matrix4();

function setupVRInteractions(){
  for(let i=0;i<2;i++){
    const ctrl=threeRenderer.xr.getController(i);
    ctrl.userData.index=i;
    ctrl.addEventListener('selectstart',()=>onVRSelectStart(i));
    ctrl.addEventListener('selectend',  ()=>onVRSelectEnd(i));
    ctrl.addEventListener('squeezestart',()=>onVRSqueezeStart(i));
    ctrl.addEventListener('squeezeend',  ()=>onVRSqueezeEnd(i));
    const ray=new THREE.Line(
      new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0,0,0),new THREE.Vector3(0,0,-1)]),
      new THREE.LineBasicMaterial({color:0x1df2d6,transparent:true,opacity:0.5})
    );
    ray.scale.z=5; ctrl.add(ray);
    threeScene.add(ctrl); vrControllers.push(ctrl);
  }
  const ringGeo=new THREE.RingGeometry(0.28,0.34,32);
  vrTeleportRing=new THREE.Mesh(ringGeo,new THREE.MeshBasicMaterial({color:0x1df2d6,side:THREE.DoubleSide,transparent:true,opacity:0.75}));
  vrTeleportRing.rotation.x=-Math.PI/2; vrTeleportRing.visible=false;
  threeScene.add(vrTeleportRing);
  threeRenderer.xr.addEventListener('sessionstart',()=>{ vrXRRefSpace=threeRenderer.xr.getReferenceSpace(); });
}

function onVRSelectStart(idx){
  const ctrl=vrControllers[idx];
  if(!ctrl) return;
  if(CONFIG.vr.handInteractions&&threeModelGroup){
    _vrMat.identity().extractRotation(ctrl.matrixWorld);
    _vrRay.ray.origin.setFromMatrixPosition(ctrl.matrixWorld);
    _vrRay.ray.direction.set(0,0,-1).applyMatrix4(_vrMat);
    if(_vrRay.intersectObject(threeModelGroup,true).length>0){
      vrGrabController=ctrl;
      vrGrabOffset.copy(ctrl.matrixWorld).invert().multiply(threeModelGroup.matrixWorld);
      return;
    }
  }
  if(CONFIG.vr.teleport&&idx===1) vrTeleportActive=true;
}

function onVRSelectEnd(idx){
  if(vrGrabController===vrControllers[idx]) vrGrabController=null;
  if(CONFIG.vr.teleport&&idx===1&&vrTeleportActive){
    vrTeleportActive=false;
    if(vrTeleportRing.visible&&vrXRRefSpace&&typeof XRRigidTransform!=='undefined'){
      const cam=threeRenderer.xr.getCamera();
      const tf=new XRRigidTransform({x:-(vrTeleportPoint.x-cam.position.x),y:0,z:-(vrTeleportPoint.z-cam.position.z),w:1});
      const newSpace=vrXRRefSpace.getOffsetReferenceSpace(tf);
      vrXRRefSpace=newSpace; threeRenderer.xr.setReferenceSpace(newSpace);
    }
    vrTeleportRing.visible=false;
  }
}

function onVRSqueezeStart(idx){
  vrBothSqueeze[idx]=true;
  if(vrBothSqueeze[0]&&vrBothSqueeze[1]&&threeModelGroup&&vrControllers.length===2){
    const p0=new THREE.Vector3().setFromMatrixPosition(vrControllers[0].matrixWorld);
    const p1=new THREE.Vector3().setFromMatrixPosition(vrControllers[1].matrixWorld);
    vrInitialSqueezeDist=p0.distanceTo(p1);
    vrInitialSqueezeScale=threeModelGroup.scale.x;
  }
}
function onVRSqueezeEnd(idx){ vrBothSqueeze[idx]=false; }

function updateVRInteractions(){
  if(vrGrabController&&threeModelGroup){
    const m=new THREE.Matrix4().copy(vrGrabController.matrixWorld).multiply(vrGrabOffset);
    m.decompose(threeModelGroup.position,threeModelGroup.quaternion,threeModelGroup.scale);
  }
  if(vrBothSqueeze[0]&&vrBothSqueeze[1]&&threeModelGroup&&!vrGrabController&&vrControllers.length===2){
    const p0=new THREE.Vector3().setFromMatrixPosition(vrControllers[0].matrixWorld);
    const p1=new THREE.Vector3().setFromMatrixPosition(vrControllers[1].matrixWorld);
    const dist=p0.distanceTo(p1);
    if(vrInitialSqueezeDist>0.001){
      const ns=Math.max(0.001,vrInitialSqueezeScale*(dist/vrInitialSqueezeDist));
      threeModelGroup.scale.setScalar(ns); syncScaleUI(ns);
    }
  }
  if(vrTeleportActive&&vrControllers[1]){
    const ctrl=vrControllers[1];
    _vrMat.identity().extractRotation(ctrl.matrixWorld);
    _vrRay.ray.origin.setFromMatrixPosition(ctrl.matrixWorld);
    _vrRay.ray.direction.set(0,0,-1).applyMatrix4(_vrMat);
    const doty=_vrRay.ray.direction.y;
    if(doty<-0.1){
      const t=-_vrRay.ray.origin.y/doty;
      if(t>0&&t<20){ vrTeleportPoint.copy(_vrRay.ray.origin).addScaledVector(_vrRay.ray.direction,t); vrTeleportRing.position.set(vrTeleportPoint.x,0.01,vrTeleportPoint.z); vrTeleportRing.visible=true; return; }
    }
    vrTeleportRing.visible=false;
  }
}

/* ═══════════════════════════════════════════════
   AR + VR BUTTONS
   ═══════════════════════════════════════════════ */
function setupXRButtons(){
  if(!threeRenderer) return;
  if(CONFIG.xr.ar&&navigator.xr) navigator.xr.isSessionSupported('immersive-ar').then(ok=>{ if(ok||/android|iphone|ipad/i.test(navigator.userAgent)) $('xrvu-btn-ar').style.display='flex'; }).catch(()=>{});
  if(CONFIG.xr.vr&&navigator.xr) navigator.xr.isSessionSupported('immersive-vr').then(ok=>{ if(ok) $('xrvu-btn-vr').style.display='flex'; }).catch(()=>{});
  $('xrvu-btn-ar').addEventListener('click',async()=>{
    if(!navigator.xr){ toast('WebXR non disponible'); return; }
    try {
      const ok=await navigator.xr.isSessionSupported('immersive-ar');
      if(!ok){ toast('AR non supporte — ouvrez depuis un telephone'); return; }
      const opts={requiredFeatures:['local-floor'],optionalFeatures:['hit-test','dom-overlay','image-tracking'],domOverlay:{root:$('xrvu-root')}};
      const itTargets=CONFIG.triggers.imageTracker.targets;
      if(itTargets.length>0){
        try {
          const bitmaps=await Promise.all(itTargets.map(t=>fetch(t.image).then(r=>r.blob()).then(b=>createImageBitmap(b))));
          opts.trackedImages=bitmaps.map((bmp,i)=>({image:bmp,widthInMeters:itTargets[i].widthInMeters||0.2}));
        } catch(e){ console.warn('[XRVU] ImageTracker:',e.message); }
      }
      xrSession=await navigator.xr.requestSession('immersive-ar',opts);
      await threeRenderer.xr.setSession(xrSession);
      const bgSave=threeScene.background; threeScene.background=null; threeRenderer.setClearColor(0x000000,0);
      $('xrvu-btn-ar').classList.add('active');
      if(CONFIG.xr.hitTest){ xrRefSpace=await xrSession.requestReferenceSpace('local-floor'); const vs=await xrSession.requestReferenceSpace('viewer'); xrHitTestSource=await xrSession.requestHitTestSource({space:vs}); $('xrvu-reticule').style.display='flex'; }
      xrSession.addEventListener('end',()=>{ threeScene.background=bgSave; applyBackground(); xrHitTestSource=null; xrRefSpace=null; $('xrvu-btn-ar').classList.remove('active'); $('xrvu-reticule').style.display='none'; });
    } catch(e){ toast('AR: '+e.message); }
  });
  const vrBtn=XRButton.createButton(threeRenderer,{mode:'immersive-vr',requiredFeatures:['local-floor'],optionalFeatures:['hand-tracking']});
  vrBtn.style.display='none'; document.body.appendChild(vrBtn);
  $('xrvu-btn-vr').addEventListener('click',async()=>{ if(!navigator.xr){ toast('WebXR non disponible'); return; } const ok=await navigator.xr.isSessionSupported('immersive-vr'); if(ok) vrBtn.click(); else toast('VR non supporte'); });
}
function updateHitTest(frame){ if(!xrHitTestSource||!xrRefSpace||!frame) return; const r=frame.getHitTestResults(xrHitTestSource); if(r.length>0&&r[0].getPose(xrRefSpace)) $('xrvu-reticule').style.display='flex'; }

/* ═══════════════════════════════════════════════
   GPS TRIGGER
   ═══════════════════════════════════════════════ */
let _gpsWatchId=null, _lastGPSTrigger=null;
function haversineDistance(lat1,lon1,lat2,lon2){ const R=6371000,dLat=(lat2-lat1)*Math.PI/180,dLon=(lon2-lon1)*Math.PI/180,a=Math.sin(dLat/2)**2+Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*Math.sin(dLon/2)**2; return R*2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)); }

function startGPSWatch(){
  if(!navigator.geolocation){ toast('GPS non disponible'); return; }
  if(_gpsWatchId!==null) return;
  _gpsWatchId=navigator.geolocation.watchPosition(pos=>{
    const{latitude:lat,longitude:lon}=pos.coords;
    $('xrvu-gps-coords').textContent=lat.toFixed(6)+', '+lon.toFixed(6);
    for(const zone of CONFIG.triggers.gps.zones){
      const dist=haversineDistance(lat,lon,zone.lat,zone.lon);
      if(dist<=(zone.radius||CONFIG.triggers.gps.radius)){
        if(_lastGPSTrigger!==zone.name){ _lastGPSTrigger=zone.name; toast('Zone GPS : '+(zone.name||'Zone')); if(zone.url) loadModel(zone.url); }
        return;
      }
    }
    _lastGPSTrigger=null;
  },e=>{ toast('GPS erreur: '+e.message); _gpsWatchId=null; },{enableHighAccuracy:true,maximumAge:3000});
  toast('GPS actif'); $('xrvu-btn-triggers').classList.add('active');
}

function addGPSZone(lat,lon,name,url,radius){
  CONFIG.triggers.gps.zones.push({lat,lon,name:name||'Zone '+(CONFIG.triggers.gps.zones.length+1),url:url||null,radius:radius||CONFIG.triggers.gps.radius});
  renderGPSZonesList();
}

function renderGPSZonesList(){
  const list=$('xrvu-gps-zones-list'); if(!list) return;
  list.innerHTML=CONFIG.triggers.gps.zones.map((z,i)=>`<div style="padding:2px 0;border-bottom:1px solid rgba(255,255,255,.05);">[${i+1}] ${z.name} — ${z.lat.toFixed(4)},${z.lon.toFixed(4)} (${z.radius||50}m) <span style="cursor:pointer;color:#e05555;" data-del="${i}">x</span></div>`).join('');
  list.querySelectorAll('[data-del]').forEach(el=>el.addEventListener('click',()=>{ CONFIG.triggers.gps.zones.splice(parseInt(el.dataset.del),1); renderGPSZonesList(); }));
}

/* ═══════════════════════════════════════════════
   GYROSCOPE + INTERACTIONS
   ═══════════════════════════════════════════════ */
let _gyroAlpha0=null,_userInteracting=false,_idleTimer=null;
function onUserInteract(){ _userInteracting=true; clearTimeout(_idleTimer); _idleTimer=setTimeout(()=>{ _userInteracting=false; },3000); }
function setupGyroscope(){
  if(!CONFIG.xr.gyroscope||!threeCamera) return;
  window.addEventListener('deviceorientation',e=>{ if(!e.gamma) return; if(_gyroAlpha0===null) _gyroAlpha0=e.alpha; if(_userInteracting) return; const alpha=THREE.MathUtils.degToRad((e.alpha||0)-_gyroAlpha0); const beta=THREE.MathUtils.degToRad(e.beta||0); const gamma=THREE.MathUtils.degToRad(e.gamma||0); threeCamera.quaternion.setFromEuler(new THREE.Euler(beta,alpha,-gamma,'YXZ')); });
}
let _pinch0=null;
$('xrvu-root').addEventListener('touchstart',e=>{ if(e.touches.length===2) _pinch0=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY); },{passive:true});
$('xrvu-root').addEventListener('touchmove',e=>{ if(e.touches.length===2&&_pinch0&&threeModelGroup){ const d=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY); const ns=Math.max(0.001,threeModelGroup.scale.x*(d/_pinch0)); threeModelGroup.scale.setScalar(ns); syncScaleUI(ns); _pinch0=d; } },{passive:true});
window.addEventListener('wheel',e=>{ if(animMode==='rotateonscroll'){ if(threeModelGroup) threeModelGroup.rotation.y+=e.deltaY*0.003; else if(gsCamera) gsAnim_angle+=e.deltaY*0.003; onUserInteract(); } },{passive:true});
$('xrvu-root').addEventListener('click',()=>{ if(animMode==='clickfade') _clickFadeDir*=-1; });

/* ═══════════════════════════════════════════════
   ANIMATIONS
   ═══════════════════════════════════════════════ */
let animMode='none',animClock=new THREE.Clock(),_clickFadeDir=1;
const _mouse=new THREE.Vector2();
window.addEventListener('mousemove',e=>{ const r=$('xrvu-root').getBoundingClientRect(); _mouse.x=((e.clientX-r.left)/r.width-0.5)*2; _mouse.y=((e.clientY-r.top)/r.height-0.5)*2; });

function setAnimation(mode){
  animMode=mode;
  document.querySelectorAll('.xrvu-anim-item').forEach(el=>el.classList.toggle('active',el.dataset.mode===mode));
  if(threeControls){ threeControls.autoRotate=(mode==='smoothorbit'); threeControls.autoRotateSpeed=CONFIG.animation.speed*2; }
  if(threeModelGroup){ _baseY=threeModelGroup.position.y; _baseScale=threeModelGroup.scale.x; }
}
function tickAnimations(dt){
  if(_userInteracting||!threeModelGroup) return;
  const s=CONFIG.animation.speed,t=animClock.getElapsedTime();
  switch(animMode){
    case 'turntable':      threeModelGroup.rotation.y+=dt*s*0.5; break;
    case 'floating':       threeModelGroup.position.y=_baseY+Math.sin(t*s*1.2)*0.08; threeModelGroup.rotation.y+=dt*s*0.12; break;
    case 'breathing':      { const p=1+Math.sin(t*s*1.5)*0.04; threeModelGroup.scale.setScalar(_baseScale*p); break; }
    case 'expandcontract': { const p=1+Math.sin(t*s)*0.18; threeModelGroup.scale.setScalar(_baseScale*p); break; }
    case 'followmouse':    threeModelGroup.rotation.y+=(_mouse.x*0.6-threeModelGroup.rotation.y)*dt*2; threeModelGroup.rotation.x+=(-_mouse.y*0.3-threeModelGroup.rotation.x)*dt*2; break;
    case 'pulsezoom':      { const d=2.5+Math.sin(t*s*0.8)*0.9; threeCamera.position.normalize().multiplyScalar(Math.max(0.5,d)); break; }
    case 'clickfade':      threeModelGroup.traverse(c=>{ if(c.isMesh&&c.material){ const ms=Array.isArray(c.material)?c.material:[c.material]; ms.forEach(m=>{ m.transparent=true; m.opacity=THREE.MathUtils.lerp(m.opacity,_clickFadeDir===1?0:1,dt*1.5); }); } }); break;
  }
}
function applyGSAnimation(dt,t){
  if(!gsCamera) return false;
  const s=CONFIG.animation.speed,r=gsSceneRadius*2.5;
  switch(animMode){
    case 'turntable':      if(_userInteracting) return false; gsAnim_angle+=dt*s*0.5; gsCamera.position.x=Math.sin(gsAnim_angle)*r; gsCamera.position.z=Math.cos(gsAnim_angle)*r; gsCamera.position.y=gsSceneRadius*0.5; if(gsCamera.lookAt) gsCamera.lookAt(0,0,0); return true;
    case 'smoothorbit':    if(_userInteracting) return false; gsAnim_angle+=dt*s*0.3; gsCamera.position.x=Math.sin(gsAnim_angle)*r; gsCamera.position.z=Math.cos(gsAnim_angle)*r; gsCamera.position.y=gsSceneRadius*0.4; if(gsCamera.lookAt) gsCamera.lookAt(0,0,0); return true;
    case 'followmouse':    if(_userInteracting){ gsControls?.update(); return false; } { const lf=Math.min(1,dt*1.8); gsCamera.position.x+=(_mouse.x*gsSceneRadius*1.5-gsCamera.position.x)*lf; gsCamera.position.y+=(gsSceneRadius*0.3-_mouse.y*gsSceneRadius*0.6-gsCamera.position.y)*lf; gsCamera.position.z=gsSceneRadius*3.0; if(gsCamera.lookAt) gsCamera.lookAt(0,0,0); } return true;
    case 'floating':       if(_userInteracting) return false; gsCamera.position.y=gsSceneRadius*0.5+Math.sin(t*s)*gsSceneRadius*0.15; return true;
    case 'pulsezoom':      if(_userInteracting) return false; gsCamera.position.z=r+Math.sin(t*s*0.8)*gsSceneRadius*0.5; return true;
    case 'rotateonscroll': gsCamera.position.x=Math.sin(gsAnim_angle)*r; gsCamera.position.z=Math.cos(gsAnim_angle)*r; gsCamera.position.y=gsSceneRadius*0.5; if(gsCamera.lookAt) gsCamera.lookAt(0,0,0); return true;
    default: return false;
  }
}

/* ═══════════════════════════════════════════════
   AUDIO
   ═══════════════════════════════════════════════ */
let audioEl=null,_soundMuted=false;
function setupMedia(){
  if(CONFIG.media.image){ const img=document.createElement('img'); img.src=CONFIG.media.image; img.style.cssText='position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:2;pointer-events:none;'; $('xrvu-root').insertBefore(img,$('xrvu-gs-stage')); }
  if(CONFIG.media.sound) initAudio(CONFIG.media.sound);
}
function initAudio(src){
  if(audioEl){ audioEl.pause(); audioEl.src=''; }
  audioEl=document.createElement('audio'); audioEl.src=src; audioEl.loop=CONFIG.media.soundLoop; audioEl.volume=0.8;
  if(CONFIG.media.soundAutoplay){ audioEl.play().catch(()=>{ toast('Cliquez pour activer le son'); const once=()=>{ audioEl?.play(); $('xrvu-root').removeEventListener('click',once); }; $('xrvu-root').addEventListener('click',once); }); }
  const btn=$('xrvu-btn-sound'); btn.style.display='flex';
  if(CONFIG.media.soundAutoplay) btn.classList.add('active');
  btn.onclick=e=>{ e.stopPropagation(); _soundMuted=!_soundMuted; audioEl.muted=_soundMuted; if(_soundMuted){ audioEl.pause(); btn.classList.remove('active'); btn.innerHTML=ICONS.volumeOff; } else { audioEl.play().catch(()=>{}); btn.classList.add('active'); btn.innerHTML=ICONS.volumeOn; } };
}

/* ═══════════════════════════════════════════════
   PANNEAUX UI
   ═══════════════════════════════════════════════ */
let _proportionalLock=true;

function syncScaleUI(actualScale){
  const sl=$('xrvu-tscale'),nm=$('xrvu-tscale-num');
  if(sl) sl.value=Math.max(-2,Math.min(3,Math.log10(Math.max(0.0001,actualScale))));
  if(nm) nm.value=actualScale.toFixed(4);
}
function applyScale(actualScale){
  actualScale=Math.max(0.0001,actualScale);
  if(threeModelGroup) threeModelGroup.scale.setScalar(actualScale);
  if(activeEngine==='gs') gsSceneRadius=actualScale;
  syncScaleUI(actualScale);
}

function setupTransformPanel(){
  const mkSl=(id,vid,cb)=>{ const s=$(id),v=$(vid); if(!s) return; s.addEventListener('input',()=>{ v.textContent=parseFloat(s.value).toFixed(2); cb(parseFloat(s.value)); }); };
  mkSl('xrvu-tx','xrvu-tx-val',val=>{ if(threeModelGroup) threeModelGroup.position.x=val; });
  mkSl('xrvu-ty','xrvu-ty-val',val=>{ if(threeModelGroup) threeModelGroup.position.y=_baseY+val; });
  mkSl('xrvu-tz','xrvu-tz-val',val=>{ if(threeModelGroup) threeModelGroup.position.z=val; });
  const scaleSl=$('xrvu-tscale'),scaleNum=$('xrvu-tscale-num');
  if(scaleSl) scaleSl.addEventListener('input',()=>{ const actual=Math.pow(10,parseFloat(scaleSl.value)); scaleNum.value=actual.toFixed(4); applyScale(actual); });
  if(scaleNum) scaleNum.addEventListener('change',()=>{ const v=parseFloat(scaleNum.value); if(!isNaN(v)&&v>0) applyScale(v); });
  const lb=$('xrvu-lock-btn');
  lb.addEventListener('click',e=>{ e.stopPropagation(); _proportionalLock=!_proportionalLock; lb.classList.toggle('locked',_proportionalLock); lb.innerHTML=_proportionalLock?ICONS.lock:ICONS.lockOpen; });
}

function setupSettingsPanel(){
  const mkSl=(id,vid,cb)=>{ const s=$(id),v=$(vid); if(!s) return; s.addEventListener('input',()=>{ v.textContent=parseFloat(s.value).toFixed(2); cb(parseFloat(s.value)); }); };
  mkSl('xrvu-s-ambient','xrvu-s-ambient-val',v=>{ if(ambientLight) ambientLight.intensity=v; });
  mkSl('xrvu-s-light','xrvu-s-light-val',v=>{ if(keyLight) keyLight.intensity=v; });
  mkSl('xrvu-s-opacity','xrvu-s-opacity-val',v=>{ CONFIG.material.opacity=v; if(threeModelGroup) applyMaterialConfig(threeModelGroup); });
  mkSl('xrvu-s-roughness','xrvu-s-roughness-val',v=>{ CONFIG.material.roughness=v; if(threeModelGroup) applyMaterialConfig(threeModelGroup); });
  mkSl('xrvu-s-metalness','xrvu-s-metalness-val',v=>{ CONFIG.material.metalness=v; if(threeModelGroup) applyMaterialConfig(threeModelGroup); });
}

function setupMediaPanel(){
  let videoProj='flat',imageProj='flat';
  function initProjBtns(groupId,onSelect){
    document.querySelectorAll('#'+groupId+' .xrvu-proj-btn').forEach(btn=>{
      btn.addEventListener('click',()=>{ document.querySelectorAll('#'+groupId+' .xrvu-proj-btn').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); onSelect(btn.dataset.proj); });
    });
  }
  initProjBtns('xrvu-mv-proj',p=>videoProj=p);
  initProjBtns('xrvu-mi-proj',p=>imageProj=p);
  const mvSz=$('xrvu-mv-size'),mvSzV=$('xrvu-mv-size-val'); if(mvSz) mvSz.addEventListener('input',()=>mvSzV.textContent=parseFloat(mvSz.value).toFixed(1));
  const miSz=$('xrvu-mi-size'),miSzV=$('xrvu-mi-size-val'); if(miSz) miSz.addEventListener('input',()=>miSzV.textContent=parseFloat(miSz.value).toFixed(1));
  const maVol=$('xrvu-ma-vol'),maVolV=$('xrvu-ma-vol-val'); if(maVol) maVol.addEventListener('input',()=>{ maVolV.textContent=parseFloat(maVol.value).toFixed(2); if(audioEl) audioEl.volume=parseFloat(maVol.value); });
  $('xrvu-mv-apply').addEventListener('click',()=>{ const url=$('xrvu-mv-url').value.trim(); if(!url){ toast('Entrez une URL video'); return; } if(!threeScene) initThree(); addVideoProjection(url,videoProj,parseFloat($('xrvu-mv-size').value)); $('xrvu-mv-remove').style.display='block'; toast('Video chargee'); });
  $('xrvu-mv-remove').addEventListener('click',()=>{ clearMediaMeshes(); $('xrvu-mv-remove').style.display='none'; toast('Video retiree'); });
  $('xrvu-mi-apply').addEventListener('click',()=>{ const url=$('xrvu-mi-url').value.trim(); if(!url){ toast('Entrez une URL image'); return; } if(!threeScene) initThree(); addImageProjection(url,imageProj,parseFloat($('xrvu-mi-size').value)); $('xrvu-mi-remove').style.display='block'; toast('Image chargee'); });
  $('xrvu-mi-remove').addEventListener('click',()=>{ clearMediaMeshes(); $('xrvu-mi-remove').style.display='none'; toast('Image retiree'); });
  $('xrvu-ma-apply').addEventListener('click',()=>{ const url=$('xrvu-ma-url').value.trim(); if(!url){ toast('Entrez une URL audio'); return; } CONFIG.media.sound=url; CONFIG.media.soundAutoplay=true; initAudio(url); toast('Audio charge'); });
}

function setupTriggersPanel(){
  const gpsR=$('xrvu-gps-radius'),gpsRV=$('xrvu-gps-radius-val'); if(gpsR) gpsR.addEventListener('input',()=>{ gpsRV.textContent=gpsR.value; CONFIG.triggers.gps.radius=parseInt(gpsR.value); });
  $('xrvu-gps-watch').addEventListener('click',startGPSWatch);
  $('xrvu-gps-add').addEventListener('click',()=>{
    if(!navigator.geolocation){ toast('GPS indisponible'); return; }
    navigator.geolocation.getCurrentPosition(pos=>{ const name=$('xrvu-gps-zone-name').value.trim(),url=$('xrvu-gps-zone-url').value.trim(); addGPSZone(pos.coords.latitude,pos.coords.longitude,name,url,CONFIG.triggers.gps.radius); toast('Zone GPS ajoutee : '+(name||'Zone')); });
  });
  const itW=$('xrvu-it-width'),itWV=$('xrvu-it-width-val'); if(itW) itW.addEventListener('input',()=>itWV.textContent=parseFloat(itW.value).toFixed(2));
  $('xrvu-it-add').addEventListener('click',()=>{
    const imgUrl=$('xrvu-it-url').value.trim(),contentUrl=$('xrvu-it-content').value.trim();
    if(!imgUrl){ toast('Entrez une URL image cible'); return; }
    const w=parseFloat($('xrvu-it-width').value);
    CONFIG.triggers.imageTracker.targets.push({image:imgUrl,widthInMeters:w,contentUrl:contentUrl||null});
    toast('Tracker image ajoute');
    const list=$('xrvu-it-list'); if(list) list.innerHTML+=`<div style="padding:2px 0;">${imgUrl.split('/').pop()} (${w}m)</div>`;
  });
}

/* ═══════════════════════════════════════════════
   INIT UI
   ═══════════════════════════════════════════════ */
function initUI(){
  $('xrvu-bar').style.display=CONFIG.commands.display?'flex':'none';
  applyBackground();
  if(CONFIG.overlay.logo||CONFIG.overlay.title){ const br=$('xrvu-brand'); br.style.display='flex'; if(CONFIG.overlay.logo) $('xrvu-brand-logo').src=CONFIG.overlay.logo; if(CONFIG.overlay.title){ $('xrvu-brand-title').textContent=CONFIG.overlay.title; $('xrvu-brand-title').style.color=CONFIG.overlay.titleColor; } }
  const sync=(id,vid,v)=>{ const el=$(id); if(!el) return; el.value=v; const vel=$(vid); if(vel) vel.textContent=parseFloat(v).toFixed(2); };
  sync('xrvu-tx','xrvu-tx-val',CONFIG.transform.x); sync('xrvu-ty','xrvu-ty-val',CONFIG.transform.y); sync('xrvu-tz','xrvu-tz-val',CONFIG.transform.z);
  $('xrvu-tscale').value=Math.log10(Math.max(0.0001,CONFIG.transform.scale));
  $('xrvu-tscale-num').value=(CONFIG.transform.scale||1).toFixed(4);
  sync('xrvu-s-ambient','xrvu-s-ambient-val',CONFIG.lights.ambient); sync('xrvu-s-light','xrvu-s-light-val',CONFIG.lights.directional);
  sync('xrvu-s-opacity','xrvu-s-opacity-val',CONFIG.material.opacity); sync('xrvu-s-roughness','xrvu-s-roughness-val',CONFIG.material.roughness); sync('xrvu-s-metalness','xrvu-s-metalness-val',CONFIG.material.metalness);
  const animBtn=$('xrvu-btn-anim'),animMenu=$('xrvu-anim-menu'); let _menuOpen=false;
  const openM=()=>{ animMenu.classList.add('open'); _menuOpen=true; animBtn.classList.add('active'); };
  const closeM=()=>{ animMenu.classList.remove('open'); _menuOpen=false; animBtn.classList.remove('active'); };
  animBtn.addEventListener('click',e=>{ e.stopPropagation(); _menuOpen?closeM():openM(); });
  animMenu.addEventListener('click',e=>{ const item=e.target.closest('.xrvu-anim-item'); if(!item) return; setAnimation(item.dataset.mode); closeM(); });
  document.addEventListener('click',e=>{ if(!$('xrvu-root').contains(e.target)||(!animBtn.contains(e.target)&&!animMenu.contains(e.target))) closeM(); });
  const allPanelIds=['xrvu-panel-transform','xrvu-panel-settings','xrvu-panel-media','xrvu-panel-triggers'];
  const allBtnIds=['xrvu-btn-transform','xrvu-btn-settings','xrvu-btn-media','xrvu-btn-triggers'];
  function closeAllPanels(){ allPanelIds.forEach(id=>$(id)?.classList.remove('open')); allBtnIds.forEach(id=>$(id)?.classList.remove('active')); }
  function togglePanel(pId,bId){ const open=!$(pId).classList.contains('open'); closeAllPanels(); $(pId).classList.toggle('open',open); $(bId).classList.toggle('active',open); }
  $('xrvu-btn-transform').addEventListener('click',e=>{ e.stopPropagation(); togglePanel('xrvu-panel-transform','xrvu-btn-transform'); });
  $('xrvu-btn-settings').addEventListener('click',e=>{ e.stopPropagation(); togglePanel('xrvu-panel-settings','xrvu-btn-settings'); });
  $('xrvu-btn-media').addEventListener('click',e=>{ e.stopPropagation(); togglePanel('xrvu-panel-media','xrvu-btn-media'); });
  $('xrvu-btn-triggers').addEventListener('click',e=>{ e.stopPropagation(); togglePanel('xrvu-panel-triggers','xrvu-btn-triggers'); });
  $('xrvu-root').addEventListener('click',closeAllPanels);
  allPanelIds.forEach(id=>$(id)?.addEventListener('click',e=>e.stopPropagation()));
  $('xrvu-btn-qr').addEventListener('click',()=>{ const panel=$('xrvu-qr-panel'); const visible=panel.style.display==='block'; if(visible){ panel.style.display='none'; $('xrvu-btn-qr').classList.remove('active'); return; } panel.style.display='block'; $('xrvu-btn-qr').classList.add('active'); $('xrvu-qr-code').innerHTML=''; const url=CONFIG.model.url?location.href.split('?')[0]+'?model='+encodeURIComponent(CONFIG.model.url):location.href; if(window.QRCode) new window.QRCode($('xrvu-qr-code'),{text:url,width:120,height:120,colorDark:'#0010ef',colorLight:'#ffffff',correctLevel:window.QRCode.CorrectLevel.H}); });
  $('xrvu-btn-fs').addEventListener('click',()=>{ const el=$('xrvu-root'); if(!document.fullscreenElement) el.requestFullscreen?.(); else document.exitFullscreen?.(); });
  document.addEventListener('fullscreenchange',()=>{ $('xrvu-btn-fs').innerHTML=document.fullscreenElement?ICONS.compress:ICONS.expand; });
  $('xrvu-root').addEventListener('dblclick',()=>{ threeControls?.reset(); if(threeCamera) threeCamera.position.set(0,0.5,2.5); if(gsCamera){ gsCamera.position.set(0,gsSceneRadius*0.3,gsSceneRadius*3.0); if(gsCamera.lookAt) gsCamera.lookAt(0,0,0); gsCamInitialized=true; } });
  const params=new URLSearchParams(location.search); if(params.has('model')) CONFIG.model.url=params.get('model');
  setupTransformPanel(); setupSettingsPanel(); setupMediaPanel(); setupTriggersPanel();
  if(CONFIG.hud.autoHide) _hudTimer=setTimeout(()=>$('xrvu-bar').classList.add('hud-hidden'),CONFIG.hud.autoHideDelay*2);
}

/* ═══════════════════════════════════════════════
   BOUCLE + INIT
   ═══════════════════════════════════════════════ */
function threeLoop(t,frame){
  if(frame&&CONFIG.xr.hitTest) updateHitTest(frame);
  if(threeRenderer.xr.isPresenting) updateVRInteractions();
  tickAnimations(animClock.getDelta());
  threeControls?.update();
  threeRenderer.render(threeScene,threeCamera);
}

async function init(){
  try {
    initThree(); initUI(); setupGyroscope(); setupMedia();
    if(CONFIG.triggers.gps.enabled&&CONFIG.triggers.gps.zones.length) startGPSWatch();
    if(CONFIG.model.url){
      const fmt=detectFormat(CONFIG.model.url,CONFIG.model.format);
      if(fmt==='spz'||fmt==='ply'||fmt==='splat') initGS().then(()=>loadModel(CONFIG.model.url,CONFIG.model.format)).catch(e=>err('GS: '+(e.message||e)));
      else { loadModel(CONFIG.model.url,CONFIG.model.format); initGS(); }
    } else { lbl('Aucun modele configure'); initGS(); }
  } catch(e){ err('Init: '+(e.message||String(e))); }
}
init();
</script>
				</div>
				</div>
				</div>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/spz/">spz</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>Labs</title>
		<link>https://presentcomposedesign.fr/labs/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Mon, 16 Mar 2026 15:19:07 +0000</pubDate>
				<category><![CDATA[Design produit]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=35979</guid>

					<description><![CDATA[<p>PCdlabs · Présent Composé design Applications &#38; expériences Carrousel Grille Liste Faites glisser pour tourner · Touchez la carte de face pour l&#8217;ouvrir © PCdlabs · Présent Composé design .dev presentcomposedesign.fr Mode développeur Code d&#8217;accès Annuler Entrer Mode développeur Réglages Appliqués tout de suite et enregistrés dans ce navigateur, sur cet appareil seulement. Pour qu&#8217;ils valent pour tous les visiteurs, copiez la configuration en bas dans le bloc Elementor. Sélecteur d&#8217;affichageBoutons Carrousel, Grille, Liste Affichage par défaut Carrousel Grille Liste ThèmeAuto suit le réglage du navigateur Auto Sombre Clair Défilement latéral à la moletteOrdinateur : la molette fait tourner le carrousel au lieu de faire défiler la page Taille du carré 58 % Rotation automatiqueDélai sans interaction avant que le carrousel tourne seul (salon) 20 s Clic sur la carte Application Article Liens dans un nouvel onglet Configuration pour le bloc ElementorÀ remplacer dans la balise &#60;section &#8230;&#62; Copier Quitter le mode développeur Réinitialiser Terminé</p>
<p>Cet article <a href="https://presentcomposedesign.fr/labs/">Labs</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="35979" class="elementor elementor-35979">
				<div class="elementor-element elementor-element-baf8fad 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="baf8fad" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-a00977d elementor-widget elementor-widget-html" data-id="a00977d" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					<!--
// ║  Tourniquet PCdlabs, © All rights reserved                                     ║
// ║  2026 · PCdlabs ·  Present Composé Design                                ║
// ║  Alban DESBARAX                                                          ║
// ║  360° Design & Creative Technologist - Toulouse, FRANCE                  ║
// ║                                                                          ║
// ║  · Expertise & Support 2D, 3D, AR, VR, XR, AI                            ║
// ║  · Product Design & Industrial Design                                    ║
// ║  · Digital communication                                                 ║
// ║  · Immersive experiences  -ᯅ- ✔️                                        ║
//                                                                            
//  · 🡺 https://presentcomposedesign.fr/      
//  · 🡺 https://pcdlabs.presentcomposedesign.fr/

// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#*++==------===**%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#++-::::::::::::::::::::::-=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+-::::::::::::::::::::::::::::::::=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@#+::::::::::::::::::::::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@%+:::::::::::::::::=++***+=-::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@#=::::::::::::::::+%@@@@@@@@@@%+-:::::::::::::::::::=#@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@#-::::::::::::::::#@@@@@@@@@@@@@@@@%-:::::::::::::::::::=#@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@%=::::::::::::::::*@@@@@@**==-=+##@@@@@*-:::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@=:::::::::::::::::%@@@@@+::::::::::-*@@@@#-::::::::::::::::::::=@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@#::::::::::::::::::%@@@@=::::::::::::::-%@@@#-:::::::::::::::::::::#@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@=::::::::::::::::::*@@@@=::::::::::::::::-%@@@*::::::::::::::::::::::=@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@%-::::::::::::::::::-@@@@=::::::::::::::::::-%@@@-::::::::::::::::::::::-%@@@@@@@@@@@@@@
// @@@@@@@@@@@@#::::::::::::::::::::*@@@#::::::::::::::::::::=@@@*::::::::::::::::::::::::*@@@@@@@@@@@@@
// @@@@@@@@@@@#:::::::::::::::::::::%@@@+::::::::::::::::::::-@@@%:::::::::::::::::::::::::*@@@@@@@@@@@@
// @@@@@@@@@@+::::::::::::::::::::::@@@@-:::::::::::::::::::::%@@@::::::::::::::::::::::::::*@@@@@@@@@@@
// @@@@@@@@@*:::::::::::::::::::::::@@@@=:::::::::::::::::::::@@@@:::::::::::::::::::::::::::*@@@@@@@@@@
// @@@@@@@@@%::::::::::::::::::::::::@@@@*:::::::::::::::::::::@@@%::::::::::::::::::::::::::::*@@@@@@@@
// @@@@@@@%:::::::::::::::::::::::::@@@@%::::::::::::::::::::+@@@*:::::::::::::::::::::::::::::%@@@@@@@@
// @@@@@@@::::::::::::::::::::::::::@@@@@*::::::::::::::::::-@@@@::::::::::::::::::::::::::::::-@@@@@@@@
// @@@@@@=::::::::::::::::::::::::::@@@@@@*::::::::::::::::-@@@@+:::::::::::::::::::::::::::::::*@@@@@@@
// @@@@@%:::::::::::::::::::::::::::@@@@@@@*::::::::::::::-%@@@#:::::::::::::::::::::::::::::::::*@@@@@@
// @@@@@::::::::::::::::::::::::::::@@@@@@@@@*::::::::::+@@@@@*::::::::::::::::::::::::::::::::::-@@@@@@
// @@@@*::::::::::::::::::::::::::::@@@#=@@@@@@%#*+=**%@@@@@@=::::::::::::::::::::::::::::::::::::*@@@@@
// @@@@:::::::::::::::::::::::::::::@@@#::*@@@@@@@@@@@@@@@@*:::::::::::::::::::::::::::::::::::::::@@@@@
// @@@*:::::::::::::::::::::::::::::@@@#::::=*@@@@@@@@@%#=:::::::::::::::::::::::::::::::::::::::::#@@@@
// @@@=:::::::::::::::::::::::::::::@@@#::::::::-==+=-:::::::::::::::::::::::::::::::::::::::::::::=@@@@
// @@@::::::::::::::::::::::::::::::@@@#:::::::::::::::::::::::::::::::::::::::::::::::::--:::::::::@@@@
// @@*::::::::::::::::::::::::::::::@@@#:::::::::+######*:::::::::::::::::::::::::::::::+@=:::::::::#@@@
// @@*::::::::::::::::::::::::::::::@@@#:::--::::===+*+==:::::=-:::::::=-::::::-::::::::+@=:::::::::+@@@
// @@:::::::::::::::::::::::::::::::@@@#::+@#@@::-#@@%@%=::=@@@@+:::+@@@@@+-::@@@@%*-::#@@%#:::::::::@@@
// @@:::::::::::::::::+*##%%#*=-::::@@@#::+@@=::-%@:::-*@=:%@-:=:::*@*:::-@*::@@-:=@%::=*@*=:::::::::@@@
// @@::::::::::::::+%@@@@@@@@@@@%*-:@@@#::+@*:::*@@@@@@@@#:+@%*-:::@@@@@@@@@-:@%:::#@:::+@=::::::::::@@@
// @%::::::::::::*@@@@@@@@@@@@@@@@@#+@@#::+@+:::+@=-------:::=%@*::@%-------::@%:::#@:::+@=::::::::::%@@
// @%::::::::::-@@@@@@++-::::-=#@@@@@+*#::+@+:::-@%=-:+%%-:-=::#@::*@*-::+%=::@%:::#@:::+@=::::::::::#@@
// @%:::::::::=@@@@%+::::::::::::#@@@@**::+@+::::-#@@@@%=::#@%%@+:::+@@@@@*:::@%:::#@:::+@=::::::::::%@@
// @%::::::::-@@@@+:::::::::::::::-@@@@+:::-::::::::--:::::::=-:::::::-=-:::::--:::-=----=-::::::::::%@@
// @@:::::::-@@@@*:::::::::::::::::-%%%*:::::::::::::::::::::::::::::::::::::::::::+%%%%%%#::::::::::@@@
// @@:::::::*@@@#:::::::+#@%*-::::::=+*#*=+#*=-:::::+#%%#-::::::=#%%#=::::-#@@#-::::=##%#+-::::::::::@@@
// @@-::::::@@@@-:::::-%@#+*%@*:::::%@#+#@@+*@*::::%@+==#@*::::%@#+=%@*-::%@-=*=:::*@*+=+@%-::::::::-@@@
// @@+:::::-@@@@::::::#@=::::#@=::::%@::=@*::#@-::*@=::::-@+::*@*::::*@*::*@*-::::-@@####%@#::::::::*@@@
// @@#:::::-@@@*::::::@@:::::=@*::::%@::=@*::#@-::%@::::::@*::%@-::::-@%:::=%@%-::*@#******+::::::::*@@@
// @@@-::::-@@@%::::::#@+::::%@-::::%@::=@*::#@-::@@-::::=@-::*@*::::*@*:::-::@%::=@*::::-=-::::::::@@@@
// @@@+:::::@@@@-::::::%@%*#@@+:::::%@::=@*::#@-::@@@#=+#@+::::*@%*+@@*:::#@*+@*:::*@%*+%@%::::::::-@@@@
// @@@#:::::#@@@+:::::::=*##+-::::::+*::-*=::+*:::@%=*%#*-::::::=*%#*-:::::=*#+:::::-*##*=--=::::::*@@@@
// @@@@-::::=@@@@-::::::::::::::::::=**+::::::::::@%:::::::::::::::::::::::-==++**##%%@@@@@@@-::::-@@@@@
// @@@@*:::::*@@@@-::::::::::::::::+@@@#::::::::::::::::::::-==++**##=-#@@@@@@@@@@@@@@@@@@@@@=::::*@@@@@
// @@@@@-:::::#@@@@+-:::::::::::::*@@@@::::::-==++--##%@@@@@@@@@#*#@@%%@@@@@@@@@@@#@@@@@*#@@@+:::-@@@@@@
// @@@@@%::::::*@@@@@+-:::::::::+@@@@%:=@@@@@@@@@@==@@@**%@@@@*-#@*-@@*@@@*-+**@@@:%@@==@++@@*:::%@@@@@@
// @@@@@@*::::::=@@@@@@@%%**##@@@@@@=::=@@@@#-:--=-=@@=+@+=@@@:%@@@=+@-*@*-@@@+:@@=#@:*@@%-@@#::+@@@@@@@
// @@@@@@@-:::::::+%@@@@@@@@@@@@@@#-::::@@@+-%@@@*--@@:@@=*@@@%=*%@@@@*=@-*@@@*:+@=+=#@@@%:@@%:-@@@@@@@@
// @@@@@@@@-::::::::=*%@@@@@@@##-:::::::@@@:#@@@@@*:@@:%==@@@@@@%+-%@@#-@*+@@@:+:@*:*@@@@@:%@@-@@@@@@@@@
// @@@@@@@@#-:::::::::::::-:::::::::::::@@%:%@@@@@*-@@-#@@@#-@*%@@@-#@@:@@#=+:%@-#@:@@@@@@*=@@%@@@@@@@@@
// @@@@@@@@@*:::::::::::::::::::::::::::#@@--@@@@%:*@@%*+*=*@@=+@@@=+@@-#@@@@@@@=#@#@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@*-:::::::::::::::::::::::::#@@@+-==-:#@@@@@@@@@@@@#++*#@@@@@@@#@@@%:@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@*-::::::::::::::::::::::::+@@@@@%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@-=-:+**++==--::%@@@@@@@@@@@@
// @@@@@@@@@@@@#-:::::::::::::::::::::::=@@@@@@@@@@@@@@@@@@@@@%%##**++=---::::::::::::::::%@@@@@@@@@@@@@
// @@@@@@@@@@@@@@-::::::::::::::::::::::-@@@@@@%###**++=--::::::::::::::::::::::::::::::-%@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@+::::::::::::::::::::::--::::::::::::::::::::::::::::::::::::::::::::+@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@%-::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::-%@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@+-:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::+@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@%=::::::::::::::::::::::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@%=::::::::::::::::::::::::::::::::::::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@%=-:::::::::::::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@*-:::::::::::::::::::::::::::::::::::::::::::::#@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@%+-:::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%*=-:::::::::::::::::::::::::::::::+#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%**=-:::::::::::::::::::::=+#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%**++=====+++*##@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
-->
<!--
  PCdlabs showcase carousel ("tourniquet"), ready for an Elementor "HTML" widget.

  HOW TO USE
  1. Upload the "media" folder next to your apps, e.g. with FileZilla to
     https://pcdlabs.presentcomposedesign.fr/showcase/media/
     then keep data-media-base below pointed at that folder (trailing slash).
  2. Paste this whole file into an Elementor "HTML" widget. Nothing else to load.
  3. Edit the PROJECTS list in the script to add, remove or reorder cards.

  OPTIONS (attributes on the <section class="pcdc"> element)
  - data-media-base : folder that relative media paths are resolved against.
  - data-view       : default view, "carousel" | "grid" | "list".
  - data-card-link  : what a click on the card body opens when a project has
                      both links, "app" (default) or "article".
  - data-new-tab    : "true" opens links in a new tab, "false" in the same tab.
  - data-attract    : seconds of inactivity before the carousel starts turning
                      on its own (handy on a trade-show stand), "0" to disable.
  - data-theme      : "dark" | "light" | "auto" (follows the browser setting).
  - data-switch     : "false" hides the Carrousel / Grille / Liste switch.
  - data-wheel      : "lateral" makes the vertical mouse wheel turn the carousel
                      (desktop); "off" leaves the wheel to the page.
  - data-tile       : size of the glass square, in % of the card width (40 to 66).
  A URL parameter can also force the view: ?vue=carrousel | grille | liste

  DEVELOPER MODE
  The discreet ".dev" text in the footer asks for an access code, then opens a
  settings panel for every option above. Those settings are saved in THIS
  browser only (e.g. the trade-show tablet); the panel also gives the matching
  attributes to paste here so they become the default for every visitor.
  Only a SHA-256 fingerprint of the code is stored below: the code keeps the
  panel out of visitors' way, it is not a security barrier (nothing on the
  server can be changed from it).
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600;700;800&display=swap">

<section class="pcdc"
  data-media-base="https://pcdlabs.presentcomposedesign.fr/showcase/media/"
  data-view="carousel"
  data-card-link="app"
  data-new-tab="true"
  data-attract="20"
  data-theme="dark"
  data-switch="true"
  data-wheel="off"
  data-tile="58"
  aria-label="Applications et expériences PCdlabs">

<style>
/* ------------------------------------------------------------------
   Scope: every rule starts with .pcdc so the block never leaks into the
   theme, and the few element resets below shield it from Elementor's
   global button / link / image styles.
   ------------------------------------------------------------------ */
/* Theme tokens. Dark is the default; light comes from data-theme="light",
   or from data-theme="auto" when the browser prefers a light scheme. */
.pcdc {
  --pcdc-blue: #1001ef;
  --pcdc-blue-2: #4a5bff;
  --pcdc-ink: #f4f6ff;
  --pcdc-title: #fff;
  --pcdc-muted: rgba(244, 246, 255, .62);
  --pcdc-line: rgba(255, 255, 255, .12);
  --pcdc-bg: #06070f;
  --pcdc-halo: rgba(16, 1, 239, .42);
  --pcdc-halo-2: rgba(124, 139, 255, .14);
  --pcdc-glass: rgba(255, 255, 255, .07);
  --pcdc-glass-line: rgba(255, 255, 255, .16);
  --pcdc-glass-hover: rgba(255, 255, 255, .16);
  --pcdc-dot: rgba(255, 255, 255, .28);
  --pcdc-card-bg-1: #151a36;
  --pcdc-card-bg-2: #06070f;
  --pcdc-shade: 5, 6, 14;
  --pcdc-card-ink: #fff;
  --pcdc-card-text: rgba(244, 246, 255, .84);
  --pcdc-chip: rgba(255, 255, 255, .09);
  --pcdc-chip-line: rgba(255, 255, 255, .14);
  --pcdc-pill: rgba(8, 10, 22, .5);
  --pcdc-dim-color: #04050c;
  --pcdc-tile-shadow: rgba(0, 0, 0, .6);
  --pcdc-row: rgba(255, 255, 255, .045);
  --pcdc-row-hover: rgba(255, 255, 255, .08);
  --pcdc-floor: rgba(16, 1, 239, .55);
  --pcdc-tile-w: .58;              /* glass square width, share of the card (data-tile) */
  --pcdc-tile-cy: .46;             /* its vertical centre, between the pills and the title */
  --pcdc-pad: clamp(16px, 3.2vw, 44px);
  --pcdc-size: 320px;              /* carousel card size, set by the script */
  --pcdc-persp: 760px;             /* carousel camera distance, set by the script */
  --pcdc-d: calc(var(--pcdc-size) * .028);   /* card thickness */
  --pcdc-view-i: 0;
  container: pcdc / inline-size;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: var(--pcdc-pad);
  border-radius: clamp(18px, 2.4vw, 32px);
  color: var(--pcdc-ink);
  font-family: "Inter Tight", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: 16px;
  line-height: 1.4;
  text-align: left;
  -webkit-font-smoothing: antialiased;
  background:
    radial-gradient(1100px 520px at 50% -12%, var(--pcdc-halo), transparent 62%),
    radial-gradient(700px 380px at 105% 105%, var(--pcdc-halo-2), transparent 70%),
    var(--pcdc-bg);
  transition: background-color .4s, color .4s;
}
.pcdc[data-theme="light"] {
  --pcdc-ink: #0b0e1c;
  --pcdc-title: #0b0e1c;
  --pcdc-muted: rgba(11, 14, 28, .6);
  --pcdc-line: rgba(11, 14, 28, .12);
  --pcdc-bg: #f3f5fc;
  --pcdc-halo: rgba(16, 1, 239, .16);
  --pcdc-halo-2: rgba(124, 139, 255, .16);
  --pcdc-glass: rgba(255, 255, 255, .75);
  --pcdc-glass-line: rgba(11, 14, 28, .1);
  --pcdc-glass-hover: #fff;
  --pcdc-dot: rgba(11, 14, 28, .2);
  --pcdc-card-bg-1: #ffffff;
  --pcdc-card-bg-2: #e6ebfa;
  --pcdc-shade: 246, 248, 255;
  --pcdc-card-ink: #0b0e1c;
  --pcdc-card-text: rgba(11, 14, 28, .78);
  --pcdc-chip: rgba(11, 14, 28, .06);
  --pcdc-chip-line: rgba(11, 14, 28, .12);
  --pcdc-pill: rgba(255, 255, 255, .72);
  --pcdc-dim-color: #eef1fa;
  --pcdc-tile-shadow: rgba(20, 24, 60, .32);
  --pcdc-row: rgba(255, 255, 255, .7);
  --pcdc-row-hover: #fff;
  --pcdc-floor: rgba(16, 1, 239, .22);
}
@media (prefers-color-scheme: light) {
  .pcdc[data-theme="auto"] {
    --pcdc-ink: #0b0e1c;
    --pcdc-title: #0b0e1c;
    --pcdc-muted: rgba(11, 14, 28, .6);
    --pcdc-line: rgba(11, 14, 28, .12);
    --pcdc-bg: #f3f5fc;
    --pcdc-halo: rgba(16, 1, 239, .16);
    --pcdc-halo-2: rgba(124, 139, 255, .16);
    --pcdc-glass: rgba(255, 255, 255, .75);
    --pcdc-glass-line: rgba(11, 14, 28, .1);
    --pcdc-glass-hover: #fff;
    --pcdc-dot: rgba(11, 14, 28, .2);
    --pcdc-card-bg-1: #ffffff;
    --pcdc-card-bg-2: #e6ebfa;
    --pcdc-shade: 246, 248, 255;
    --pcdc-card-ink: #0b0e1c;
    --pcdc-card-text: rgba(11, 14, 28, .78);
    --pcdc-chip: rgba(11, 14, 28, .06);
    --pcdc-chip-line: rgba(11, 14, 28, .12);
    --pcdc-pill: rgba(255, 255, 255, .72);
    --pcdc-dim-color: #eef1fa;
    --pcdc-tile-shadow: rgba(20, 24, 60, .32);
    --pcdc-row: rgba(255, 255, 255, .7);
    --pcdc-row-hover: #fff;
    --pcdc-floor: rgba(16, 1, 239, .22);
  }
}
/* Accent text: lightened accent on dark cards, darkened accent on light cards. */
.pcdc .pcdc-card { --pcdc-accent-ink: var(--pcdc-accent-light); }
.pcdc[data-theme="light"] .pcdc-card { --pcdc-accent-ink: var(--pcdc-accent-deep); }
@media (prefers-color-scheme: light) {
  .pcdc[data-theme="auto"] .pcdc-card { --pcdc-accent-ink: var(--pcdc-accent-deep); }
}
/* Doubled class: beats theme rules such as ".entry-content p" or ".elementor-kit-5 button". */
.pcdc *, .pcdc *::before, .pcdc *::after { box-sizing: border-box; }
.pcdc.pcdc :where(h2, h3, p, ul, li, figure) { margin: 0; padding: 0; font-family: inherit; }
.pcdc.pcdc :where(ul, li) { list-style: none; }
.pcdc.pcdc img, .pcdc.pcdc video {
  display: block; max-width: none; width: 100%; height: 100%;
  border: 0; border-radius: 0; box-shadow: none; margin: 0; padding: 0;
}
.pcdc.pcdc button {
  -webkit-appearance: none; appearance: none; margin: 0; padding: 0; border: 0; border-radius: 0; background: none;
  font: inherit; color: inherit; letter-spacing: inherit; text-transform: none;
  line-height: 1; min-height: 0; box-shadow: none; cursor: pointer;
}
.pcdc svg { display: block; flex: none; }
.pcdc .pcdc-sr {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

/* ---------------- Header + view switch ---------------- */
.pcdc .pcdc-head {
  display: flex; flex-wrap: wrap; align-items: flex-end;
  justify-content: space-between; gap: 18px 24px;
  margin-bottom: clamp(18px, 3vw, 34px);
}
.pcdc .pcdc-kicker {
  display: flex; align-items: center; gap: 10px;
  font-size: .75rem; font-weight: 700; letter-spacing: .16em;
  text-transform: uppercase; color: var(--pcdc-muted);
}
.pcdc .pcdc-kicker::before {
  content: ""; width: 8px; height: 8px; border-radius: 50%;
  background: var(--pcdc-blue-2); box-shadow: 0 0 12px var(--pcdc-blue-2);
}
.pcdc .pcdc-h {
  margin-top: 10px;
  font-size: clamp(1.5rem, 1rem + 2.2vw, 2.6rem);
  font-weight: 800; line-height: 1.02; letter-spacing: -.025em; color: var(--pcdc-title);
}
.pcdc .pcdc-switch {
  position: relative; display: grid; grid-template-columns: repeat(3, 1fr);
  padding: 4px; border-radius: 999px;
  background: var(--pcdc-glass);
  border: 1px solid var(--pcdc-glass-line);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12), 0 12px 30px -18px rgba(0, 0, 0, .6);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
}
.pcdc .pcdc-switch-thumb {
  position: absolute; top: 4px; bottom: 4px; left: 4px;
  width: calc((100% - 8px) / 3); border-radius: 999px;
  background: linear-gradient(135deg, var(--pcdc-blue), var(--pcdc-blue-2));
  box-shadow: 0 6px 20px -6px rgba(16, 1, 239, .9), inset 0 1px 0 rgba(255, 255, 255, .35);
  transform: translateX(calc(100% * var(--pcdc-view-i)));
  transition: transform .45s cubic-bezier(.2, .8, .2, 1);
}
.pcdc button.pcdc-switch-btn {
  position: relative; z-index: 1; display: inline-flex; align-items: center;
  justify-content: center; gap: 8px; min-height: 42px; padding: 0 16px;
  border-radius: 999px; background: transparent; color: var(--pcdc-muted);
  font-size: .875rem; font-weight: 600; transition: color .3s;
}
.pcdc button.pcdc-switch-btn:hover { background: transparent; color: var(--pcdc-title); }
.pcdc.pcdc button.pcdc-switch-btn[aria-pressed="true"]:hover { color: #fff; }
.pcdc button.pcdc-switch-btn[aria-pressed="true"] { color: #fff; }
.pcdc button.pcdc-switch-btn:focus-visible { outline: 2px solid var(--pcdc-title); outline-offset: 2px; }
@container pcdc (max-width: 560px) {
  .pcdc .pcdc-switch { width: 100%; }
  .pcdc .pcdc-switch-label { font-size: .8rem; }
}

/* ---------------- Card anatomy (shared by every view) ---------------- */
.pcdc .pcdc-card { position: relative; }
.pcdc .pcdc-face {
  position: absolute; inset: 0; overflow: hidden; isolation: isolate;
  border-radius: 7.5%;
  -webkit-backface-visibility: hidden; backface-visibility: hidden;
  container: pcdc-face / inline-size;
  background: var(--pcdc-card-bg-2);
}
/* Everything inside a face is sized in em, and 1em follows the card width,
   so a 1024 px card and a 240 px card look the same, only smaller. */
.pcdc .pcdc-in { position: absolute; inset: 0; font-size: max(9px, 4.6cqi); }

/* Iridescent rim, as on the BirdSignal cards. */
.pcdc .pcdc-face::before {
  content: ""; position: absolute; inset: 0; z-index: 8; pointer-events: none;
  border-radius: inherit; padding: 1.5px;
  background: linear-gradient(135deg, rgba(255,255,255,.55), rgba(180,196,255,.28) 50%, rgba(230,168,255,.4));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
}
/* Depth dimming (carousel): cards further back get darker. */
.pcdc .pcdc-in::after {
  content: ""; position: absolute; inset: 0; z-index: 7; pointer-events: none;
  background: var(--pcdc-dim-color); opacity: var(--pcdc-dim, 0);
}
/* Moving sheen: follows the card angle in the carousel, sweeps on hover elsewhere. */
.pcdc .pcdc-in::before {
  content: ""; position: absolute; inset: 0; z-index: 6; pointer-events: none;
  background: linear-gradient(112deg, transparent 32%, rgba(255,255,255,.13) 46%, rgba(255,255,255,.02) 54%, transparent 64%);
  background-size: 260% 100%;
  background-position: calc(var(--pcdc-sheen, -40) * 1%) 0;
  transition: background-position 1.1s cubic-bezier(.2, .8, .2, 1);
}

/* Featured media ("image à la une"): image, GIF or video, cropped square. */
.pcdc .pcdc-bg {
  position: absolute; inset: 0; overflow: hidden;
  background:
    radial-gradient(110% 85% at 18% 0%, var(--pcdc-accent-soft), transparent 62%),
    radial-gradient(90% 70% at 100% 100%, rgba(16, 1, 239, .35), transparent 70%),
    linear-gradient(160deg, var(--pcdc-card-bg-1), var(--pcdc-card-bg-2));
}
.pcdc .pcdc-bg-media { position: absolute; inset: 0; object-fit: cover; }
.pcdc .pcdc-bg.is-blurred .pcdc-bg-media { filter: blur(1.2em) saturate(1.35); transform: scale(1.3); opacity: .8; }
.pcdc .pcdc-bg.is-icon .pcdc-bg-media {
  object-fit: contain; filter: blur(2.4em) saturate(1.5);
  transform: scale(1.35) translateY(-14%); opacity: .5;
}
.pcdc .pcdc-shade {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(var(--pcdc-shade), .35) 0%, rgba(var(--pcdc-shade), 0) 26%, rgba(var(--pcdc-shade), .3) 60%, rgba(var(--pcdc-shade), .94) 100%);
}
.pcdc .pcdc-shade::after {
  content: ""; position: absolute; inset: 0; opacity: 0;
  background: linear-gradient(180deg, rgba(var(--pcdc-shade), .25), rgba(var(--pcdc-shade), .8) 55%, rgba(var(--pcdc-shade), .96));
  transition: opacity .45s;
}

/* Top row: index + kind of entry. */
.pcdc .pcdc-top {
  position: absolute; z-index: 3; top: 5.5%; left: 6%; right: 6%;
  display: flex; justify-content: space-between; align-items: center;
  pointer-events: none;
}
.pcdc .pcdc-pill {
  font-size: .5em; font-weight: 700; letter-spacing: .14em; text-transform: uppercase;
  padding: .55em .9em; border-radius: 99em; color: var(--pcdc-card-ink);
  background: var(--pcdc-pill); border: 1px solid var(--pcdc-chip-line);
}
.pcdc .pcdc-pill.is-app { color: #fff; background: rgba(16, 1, 239, .7); border-color: rgba(140, 150, 255, .5); }

/* The glass tile: app icon, or the demo GIF / video when there is no icon.
   No backdrop-filter here (unreliable inside 3D transforms): the frosted look
   comes from a blurred copy of the featured image, aligned under the tile. */
/* Position and size come from two numbers, so the square stays centred
   whatever its size: --pcdc-tile-w (share of the card width) and
   --pcdc-tile-cy (vertical centre, midway between the pills and the title). */
.pcdc .pcdc-tile {
  --pcdc-tl: calc((1 - var(--pcdc-tile-w)) / 2);
  --pcdc-tt: calc(var(--pcdc-tile-cy) - var(--pcdc-tile-w) / 2);
  position: absolute; z-index: 2; aspect-ratio: 1;
  left: calc(var(--pcdc-tl) * 100%); top: calc(var(--pcdc-tt) * 100%);
  width: calc(var(--pcdc-tile-w) * 100%);
  border-radius: 23%; overflow: hidden;
  transform-origin: 50% 0;
  box-shadow:
    0 1.1em 2.2em -.5em var(--pcdc-tile-shadow),
    0 .35em .7em -.2em rgba(0, 0, 0, .35),
    0 0 2.6em -.6em var(--pcdc-accent-glow);
  transition: transform .55s cubic-bezier(.2, .8, .2, 1);
}
.pcdc .pcdc-tile-blur {
  /* Blurred copy of the card background, sized and shifted to line up with it. */
  position: absolute;
  left: calc(-100% * var(--pcdc-tl) / var(--pcdc-tile-w));
  top: calc(-100% * var(--pcdc-tt) / var(--pcdc-tile-w));
  width: calc(100% / var(--pcdc-tile-w)); height: calc(100% / var(--pcdc-tile-w));
  background-image: var(--pcdc-still, none), radial-gradient(80% 70% at 30% 20%, var(--pcdc-accent-soft), transparent 70%);
  background-color: #1a1f3f;
  background-size: cover; background-position: center;
  filter: blur(.75em) saturate(1.6) brightness(1.08);
}
.pcdc .pcdc-tile::before {  /* frosted tint */
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(150deg, rgba(255,255,255,.38), rgba(255,255,255,.1) 42%, rgba(255,255,255,.06) 60%, rgba(255,255,255,.18));
}
.pcdc .pcdc-tile::after {   /* bevel, rim light and specular spot: the relief */
  content: ""; position: absolute; inset: 0; z-index: 3; pointer-events: none;
  border-radius: inherit;
  border: 1px solid rgba(255, 255, 255, .5);
  background: radial-gradient(60% 40% at 22% 8%, rgba(255,255,255,.55), transparent 70%);
  box-shadow:
    inset 0 .12em .1em rgba(255, 255, 255, .6),
    inset 0 -.25em .5em rgba(0, 0, 0, .28),
    inset .2em 0 .45em rgba(255, 255, 255, .12);
  mix-blend-mode: screen;
}
.pcdc .pcdc-tile-content { position: absolute; z-index: 2; }
.pcdc .pcdc-tile-content.is-icon { inset: 17%; filter: drop-shadow(0 .35em .45em rgba(0, 0, 0, .38)); }
.pcdc .pcdc-tile-content.is-icon img { object-fit: contain; border-radius: 18%; }
.pcdc .pcdc-tile-content.is-media {
  inset: 6.5%; border-radius: 18%; overflow: hidden;
  box-shadow: 0 .2em .6em rgba(0, 0, 0, .45);
}
.pcdc .pcdc-tile-content.is-media :is(img, video) { object-fit: cover; }
.pcdc .pcdc-tile-content.is-letter {
  inset: 0; display: grid; place-items: center;
  font-size: 3.2em; font-weight: 800; color: #fff;
}

/* Bottom stack: kind, title, then (on hover) description, tags and CTAs. */
.pcdc .pcdc-bottom {
  position: absolute; z-index: 4; left: 6.5%; right: 6.5%; bottom: 6%;
  display: flex; flex-direction: column; pointer-events: none;
}
.pcdc .pcdc-kind {
  font-size: .58em; font-weight: 700; letter-spacing: .15em; text-transform: uppercase;
  color: var(--pcdc-accent-ink);
}
.pcdc h3.pcdc-title {
  margin-top: .22em; font-family: inherit;
  font-size: 1.55em; font-weight: 800; line-height: 1.02; letter-spacing: -.02em;
  color: var(--pcdc-card-ink); text-wrap: balance;
}
.pcdc .pcdc-more, .pcdc .pcdc-actions-wrap {
  display: grid; grid-template-rows: 0fr; opacity: 0;
  transition: grid-template-rows .5s cubic-bezier(.2, .8, .2, 1), opacity .35s;
}
.pcdc .pcdc-more > div, .pcdc .pcdc-actions-wrap > div { min-height: 0; overflow: hidden; }
.pcdc .pcdc-desc {
  margin-top: .75em; font-size: max(10.5px, .7em); line-height: 1.42; color: var(--pcdc-card-text);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.pcdc .pcdc-tags { display: flex; flex-wrap: wrap; gap: .3em; margin-top: .6em; }
.pcdc .pcdc-tags li {
  font-size: .5em; font-weight: 600; letter-spacing: .02em;
  padding: .4em .75em; border-radius: 99em; color: var(--pcdc-card-text);
  background: var(--pcdc-chip); border: 1px solid var(--pcdc-chip-line);
}
.pcdc .pcdc-actions { display: flex; flex-wrap: wrap; gap: .4em; padding-top: .8em; }
.pcdc a.pcdc-cta, .pcdc a.pcdc-cta:visited {
  position: relative; z-index: 5; pointer-events: auto;
  display: inline-flex; align-items: center; gap: .5em;
  font-size: .66em; font-weight: 700; line-height: 1; letter-spacing: 0;
  padding: .85em 1.2em; border-radius: 99em; text-decoration: none; white-space: nowrap;
  transition: transform .25s, box-shadow .25s, background .25s;
}
.pcdc a.pcdc-cta svg { width: 1.05em; height: 1.05em; }
.pcdc a.pcdc-cta.is-primary, .pcdc a.pcdc-cta.is-primary:visited {
  color: #fff; background: linear-gradient(135deg, var(--pcdc-blue), var(--pcdc-blue-2));
  box-shadow: 0 .6em 1.4em -.4em rgba(16, 1, 239, .95), inset 0 1px 0 rgba(255, 255, 255, .35);
}
.pcdc a.pcdc-cta.is-secondary, .pcdc a.pcdc-cta.is-secondary:visited {
  color: var(--pcdc-card-ink); background: var(--pcdc-chip);
  border: 1px solid var(--pcdc-chip-line);
}
.pcdc a.pcdc-cta:hover, .pcdc a.pcdc-cta:focus-visible { transform: translateY(-1px); text-decoration: none; }
.pcdc a.pcdc-cta.is-primary:is(:hover, :focus-visible) { color: #fff; }
.pcdc a.pcdc-cta.is-secondary:is(:hover, :focus-visible) { color: var(--pcdc-card-ink); }
.pcdc a.pcdc-cta.is-primary:hover { box-shadow: 0 .8em 1.8em -.4em rgba(16, 1, 239, 1), inset 0 1px 0 rgba(255, 255, 255, .45); }
.pcdc a.pcdc-cta:focus-visible { outline: 2px solid var(--pcdc-card-ink); outline-offset: 2px; }

/* The whole card is a link (the "hit" layer); the CTAs sit above it. */
.pcdc a.pcdc-hit { position: absolute; inset: 0; z-index: 3; border-radius: inherit; }

/* Revealed state: mouse hover, keyboard focus, or a first tap on touch screens. */
/* Revealed: the square shrinks to 32 % of the card and moves up to 8 % from the top. */
.pcdc .pcdc-card:is(.is-hover, .is-open, :focus-within) .pcdc-tile {
  transform: translateY(calc((.08 - var(--pcdc-tt)) / var(--pcdc-tile-w) * 100%)) scale(calc(.32 / var(--pcdc-tile-w)));
}
.pcdc .pcdc-card:is(.is-hover, .is-open, :focus-within) .pcdc-shade::after { opacity: 1; }
.pcdc .pcdc-card:is(.is-hover, .is-open, :focus-within) :is(.pcdc-more, .pcdc-actions-wrap) { grid-template-rows: 1fr; opacity: 1; }
.pcdc:not([data-view="carousel"]) .pcdc-card:is(.is-hover, .is-open) { --pcdc-sheen: 140; }

/* Small cards: keep the essentials. */
@container pcdc-face (max-width: 300px) {
  .pcdc .pcdc-tags { display: none; }
  .pcdc .pcdc-desc { -webkit-line-clamp: 2; }
}

/* Back face and edges: only visible in the 3D carousel. */
.pcdc .pcdc-back {
  background:
    radial-gradient(100% 80% at 80% 0%, var(--pcdc-accent-soft), transparent 65%),
    linear-gradient(200deg, var(--pcdc-card-bg-1), var(--pcdc-card-bg-2));
}
.pcdc img.pcdc-back-mark {
  position: absolute; left: 20%; top: 16%; width: 60%; height: 60%;
  object-fit: contain; opacity: .22; filter: saturate(1.6) blur(.02em);
}
.pcdc img.pcdc-back-mark.is-photo { left: 0; top: 0; width: 100%; height: 100%; object-fit: cover; opacity: .28; filter: blur(.5em) saturate(1.4); }
.pcdc .pcdc-back-text { position: absolute; left: 7%; right: 7%; bottom: 7%; }
.pcdc .pcdc-back-text strong { display: block; font-size: 1.3em; font-weight: 800; color: var(--pcdc-card-ink); letter-spacing: -.02em; }
.pcdc .pcdc-back-text span { display: block; margin-top: .5em; font-size: .5em; font-weight: 700; letter-spacing: .16em; text-transform: uppercase; color: var(--pcdc-muted); }
.pcdc .pcdc-edge {
  position: absolute; display: block; pointer-events: none;
  background: linear-gradient(180deg, #eef1ff, #9aa6ff 35%, #d8aaff 66%, #ffd9a3);
  opacity: calc(1 - var(--pcdc-dim, 0) * 1.1);
}
.pcdc .pcdc-edge-l { top: 7%; bottom: 7%; left: calc(var(--pcdc-d) / -2); width: var(--pcdc-d); transform: rotateY(-90deg); filter: brightness(.7); }
.pcdc .pcdc-edge-r { top: 7%; bottom: 7%; right: calc(var(--pcdc-d) / -2); width: var(--pcdc-d); transform: rotateY(90deg); }
.pcdc .pcdc-edge-t { left: 7%; right: 7%; top: calc(var(--pcdc-d) / -2); height: var(--pcdc-d); transform: rotateX(90deg); filter: brightness(1.1); }
.pcdc:not([data-view="carousel"]) :is(.pcdc-back, .pcdc-edge) { display: none; }

/* ---------------- View 1: the 3D carousel ("tourniquet") ---------------- */
.pcdc[data-view="carousel"] .pcdc-stage {
  position: relative; height: calc(var(--pcdc-size) * 1.4);
  margin-inline: calc(var(--pcdc-pad) * -1);
  perspective: var(--pcdc-persp); perspective-origin: 50% 38%;
  overflow: hidden; touch-action: pan-y; cursor: grab;
  -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent;
  outline: none;
}
/* Keyboard focus only: a thin rounded ring inside the stage, never a frame on click. */
.pcdc[data-view="carousel"] .pcdc-stage:focus { outline: none; }
.pcdc[data-view="carousel"] .pcdc-stage:focus-visible { outline: 2px solid rgba(74, 91, 255, .6); outline-offset: -10px; border-radius: 28px; }
.pcdc[data-view="carousel"] .pcdc-stage::after {   /* soft glow on the "floor" */
  content: ""; position: absolute; left: 50%; bottom: 2%; width: calc(var(--pcdc-size) * 2.6); height: calc(var(--pcdc-size) * .34);
  transform: translateX(-50%); border-radius: 50%; pointer-events: none;
  background: radial-gradient(closest-side, var(--pcdc-floor), transparent);
  filter: blur(18px);
}
.pcdc.is-dragging .pcdc-stage { cursor: grabbing; }
.pcdc[data-view="carousel"] .pcdc-track {
  position: absolute; z-index: 1; left: 50%; top: 50%; width: 0; height: 0;
  transform-style: preserve-3d;
}
.pcdc[data-view="carousel"] .pcdc-card {
  position: absolute; width: var(--pcdc-size); height: var(--pcdc-size);
  left: calc(var(--pcdc-size) / -2); top: calc(var(--pcdc-size) / -2);
  transform-style: preserve-3d; will-change: transform; cursor: pointer;
}
.pcdc[data-view="carousel"] .pcdc-front { transform: translateZ(calc(var(--pcdc-d) / 2)); }
.pcdc[data-view="carousel"] .pcdc-back { transform: rotateY(180deg) translateZ(calc(var(--pcdc-d) / 2)); }
.pcdc[data-view="carousel"] .pcdc-card:not(.is-front) :is(.pcdc-cta) { pointer-events: none; }

.pcdc .pcdc-nav {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 16px;
  max-width: 560px; margin: clamp(8px, 2vw, 18px) auto 0;
}
.pcdc:not([data-view="carousel"]) :is(.pcdc-nav, .pcdc-dots, .pcdc-hint) { display: none; }
.pcdc button.pcdc-arrow {
  display: grid; place-items: center; width: 48px; height: 48px; border-radius: 50%;
  color: var(--pcdc-title); background: var(--pcdc-glass);
  border: 1px solid var(--pcdc-glass-line);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18), 0 10px 24px -14px rgba(0, 0, 0, .6);
  transition: background .25s, transform .25s;
}
.pcdc button.pcdc-arrow:hover { background: var(--pcdc-glass-hover); color: var(--pcdc-title); }
.pcdc button.pcdc-arrow:active { transform: scale(.94); }
.pcdc button.pcdc-arrow:focus-visible { outline: 2px solid var(--pcdc-title); outline-offset: 2px; }
.pcdc button.pcdc-arrow svg { width: 20px; height: 20px; }
.pcdc .pcdc-caption { text-align: center; min-width: 0; }
.pcdc .pcdc-count { font-size: .72rem; font-weight: 700; letter-spacing: .16em; color: var(--pcdc-muted); }
.pcdc .pcdc-cap-title { display: block; margin-top: 4px; font-size: 1.15rem; font-weight: 800; color: var(--pcdc-title); letter-spacing: -.01em; }
.pcdc .pcdc-cap-kind { display: block; margin-top: 2px; font-size: .82rem; color: var(--pcdc-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pcdc .pcdc-dots { display: flex; justify-content: center; flex-wrap: wrap; gap: 2px; margin-top: 12px; }
.pcdc button.pcdc-dot { width: 24px; height: 24px; display: grid; place-items: center; background: transparent; }
.pcdc button.pcdc-dot:hover { background: transparent; }
.pcdc button.pcdc-dot::before {
  content: ""; width: 7px; height: 7px; border-radius: 99px; background: var(--pcdc-dot);
  transition: width .35s, background .35s;
}
.pcdc button.pcdc-dot[aria-current="true"]::before { width: 20px; background: var(--pcdc-title); }
.pcdc button.pcdc-dot:focus-visible { outline: 2px solid var(--pcdc-title); outline-offset: 0; border-radius: 6px; }
.pcdc .pcdc-hint {
  margin-top: 10px; text-align: center; font-size: .78rem; color: var(--pcdc-muted);
  transition: opacity .6s;
}
.pcdc.has-interacted .pcdc-hint { opacity: 0; }

/* ---------------- View 2: grid ---------------- */
.pcdc[data-view="grid"] .pcdc-track {
  display: grid; gap: clamp(12px, 1.8vw, 22px);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 230px), 1fr));
}
.pcdc[data-view="grid"] .pcdc-card {
  aspect-ratio: 1; transition: transform .45s cubic-bezier(.2, .8, .2, 1);
}
.pcdc[data-view="grid"] .pcdc-card.is-hover { transform: translateY(-4px); }
.pcdc[data-view="grid"] .pcdc-face { box-shadow: 0 24px 50px -30px #000; }

/* ---------------- View 3: list (fastest access on a stand) ---------------- */
.pcdc[data-view="list"] .pcdc-track { display: flex; flex-direction: column; gap: 10px; }
.pcdc[data-view="list"] .pcdc-face {
  position: relative; inset: auto; border-radius: 18px; container-type: normal;
  background: var(--pcdc-row);
}
.pcdc[data-view="list"] .pcdc-face::before { padding: 1px; opacity: .6; }
.pcdc[data-view="list"] .pcdc-in {
  position: relative; inset: auto; font-size: 16px;
  display: grid; grid-template-columns: 68px 1fr; align-items: center; gap: 16px;
  padding: 12px 16px 12px 12px;
}
.pcdc[data-view="list"] :is(.pcdc-bg, .pcdc-shade, .pcdc-top) { display: none; }
.pcdc[data-view="list"] .pcdc-card.is-hover .pcdc-face { background: var(--pcdc-row-hover); }
.pcdc[data-view="list"] .pcdc-tile {
  position: relative; left: auto; top: auto; width: 68px; font-size: 7px;
  transform: none !important; border-radius: 17px;
}
.pcdc[data-view="list"] .pcdc-tile-blur { left: 0; top: 0; width: 100%; height: 100%; background-image: radial-gradient(80% 70% at 30% 20%, var(--pcdc-accent-soft), transparent 70%); }
.pcdc[data-view="list"] .pcdc-bottom {
  position: static; display: grid; grid-template-columns: 1fr; gap: 10px; align-items: center;
}
.pcdc[data-view="list"] .pcdc-kind { font-size: .68rem; }
.pcdc[data-view="list"] h3.pcdc-title { font-size: 1.2rem; margin-top: 2px; }
.pcdc[data-view="list"] :is(.pcdc-more, .pcdc-actions-wrap) { grid-template-rows: 1fr; opacity: 1; }
.pcdc[data-view="list"] .pcdc-desc { margin-top: 4px; font-size: .88rem; -webkit-line-clamp: 2; }
.pcdc[data-view="list"] .pcdc-tags { display: none; }
.pcdc[data-view="list"] .pcdc-actions { padding-top: 0; }
.pcdc[data-view="list"] a.pcdc-cta { font-size: .82rem; padding: 12px 16px; }
@container pcdc (min-width: 760px) {
  .pcdc[data-view="list"] .pcdc-bottom { grid-template-columns: 1fr auto; gap: 24px; }
  .pcdc[data-view="list"] .pcdc-tags { display: flex; }
  .pcdc[data-view="list"] .pcdc-tags li { font-size: .7rem; }
}

/* View change: short fade. */
.pcdc .pcdc-views.is-entering { animation: pcdc-fade .5s cubic-bezier(.2, .8, .2, 1); }
@keyframes pcdc-fade { from { opacity: 0; } to { opacity: 1; } }

.pcdc .pcdc-foot {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px;
  margin-top: clamp(20px, 3vw, 34px); padding-top: 14px;
  border-top: 1px solid var(--pcdc-line); font-size: .78rem; color: var(--pcdc-muted);
}
.pcdc .pcdc-foot a, .pcdc .pcdc-foot a:visited { color: var(--pcdc-muted); text-decoration: none; }
.pcdc .pcdc-foot a:hover { color: var(--pcdc-title); text-decoration: underline; }
.pcdc .pcdc-foot-left { display: inline-flex; align-items: center; gap: 10px; }

/* Settings: the view switch can be hidden from the developer panel. */
.pcdc[data-switch="false"] .pcdc-switch { display: none; }
.pcdc.pcdc [hidden] { display: none !important; }   /* [hidden] must win over any display rule */

/* ---------------- Developer mode ---------------- */
.pcdc button.pcdc-dev-link {
  font-family: ui-monospace, "SF Mono", Consolas, monospace; font-size: .74rem;
  color: var(--pcdc-muted); opacity: .45; padding: 4px 2px; background: transparent;
  transition: opacity .25s;
}
.pcdc button.pcdc-dev-link:hover, .pcdc button.pcdc-dev-link:focus-visible { opacity: 1; background: transparent; color: var(--pcdc-title); }
.pcdc button.pcdc-dev-link:focus-visible { outline: 1px dashed currentColor; outline-offset: 2px; }

.pcdc dialog.pcdc-dlg {
  width: min(460px, calc(100vw - 24px)); max-height: calc(100dvh - 24px); overflow: auto;
  margin: auto; padding: 0; border: 1px solid var(--pcdc-glass-line); border-radius: 22px;
  color: var(--pcdc-ink); background: var(--pcdc-bg);
  background-image: radial-gradient(420px 220px at 50% -20%, var(--pcdc-halo), transparent 70%);
  box-shadow: 0 40px 90px -30px rgba(0, 0, 0, .75);
  font: 400 15px/1.45 "Inter Tight", system-ui, sans-serif;
}
.pcdc dialog.pcdc-dlg::backdrop { background: rgba(4, 5, 12, .55); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); }
.pcdc .pcdc-dlg-body { padding: 22px 22px 20px; }
.pcdc .pcdc-dlg-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
.pcdc .pcdc-dlg-kicker { font-size: .7rem; font-weight: 700; letter-spacing: .16em; text-transform: uppercase; color: var(--pcdc-muted); }
.pcdc .pcdc-dlg-title { margin-top: 4px; font-size: 1.35rem; font-weight: 800; letter-spacing: -.02em; color: var(--pcdc-title); }
.pcdc .pcdc-dlg-note { font-size: .82rem; color: var(--pcdc-muted); margin-bottom: 14px; }
.pcdc button.pcdc-x {
  width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center; flex: none;
  color: var(--pcdc-title); background: var(--pcdc-glass); border: 1px solid var(--pcdc-glass-line);
}
.pcdc button.pcdc-x:hover { background: var(--pcdc-glass-hover); color: var(--pcdc-title); }
.pcdc button.pcdc-x svg { width: 16px; height: 16px; }
.pcdc .pcdc-row {
  display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 6px 16px;
  padding: 12px 0; border-top: 1px solid var(--pcdc-line);
}
.pcdc .pcdc-row > span { font-size: .9rem; font-weight: 600; color: var(--pcdc-title); }
.pcdc .pcdc-row small { display: block; font-size: .76rem; font-weight: 400; color: var(--pcdc-muted); }
.pcdc .pcdc-row.is-wide { grid-template-columns: 1fr; }
@container pcdc (max-width: 520px) {
  .pcdc .pcdc-row:has(.pcdc-seg) { grid-template-columns: 1fr; }
  .pcdc .pcdc-row .pcdc-seg { width: 100%; }
}

/* Text fields (access code, generated attributes). */
.pcdc.pcdc input.pcdc-field, .pcdc.pcdc textarea.pcdc-field {
  width: 100%; margin: 0; padding: 12px 14px; border-radius: 12px; box-shadow: none;
  font: 500 1rem/1.3 "Inter Tight", system-ui, sans-serif; color: var(--pcdc-title);
  background: var(--pcdc-glass); border: 1px solid var(--pcdc-glass-line);
}
.pcdc.pcdc textarea.pcdc-field { font: 500 .76rem/1.5 ui-monospace, Consolas, monospace; resize: vertical; min-height: 96px; }
.pcdc.pcdc .pcdc-field:focus { outline: 2px solid var(--pcdc-blue-2); outline-offset: 1px; }
.pcdc .pcdc-err { min-height: 1.3em; margin-top: 8px; font-size: .82rem; color: #ff5d6e; }
.pcdc .pcdc-lock.is-wrong .pcdc-field { animation: pcdc-shake .35s; }
@keyframes pcdc-shake { 25% { transform: translateX(-6px); } 50% { transform: translateX(6px); } 75% { transform: translateX(-3px); } }

/* On / off switch built on a checkbox. */
.pcdc.pcdc input.pcdc-toggle {
  -webkit-appearance: none; appearance: none; position: relative; margin: 0; padding: 0; cursor: pointer;
  width: 46px; height: 28px; border-radius: 99px; flex: none;
  background: var(--pcdc-dot); border: 1px solid var(--pcdc-glass-line); transition: background .25s;
}
.pcdc.pcdc input.pcdc-toggle::after {
  content: ""; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%;
  background: #fff; box-shadow: 0 2px 6px rgba(0, 0, 0, .35); transition: transform .25s cubic-bezier(.2, .8, .2, 1);
}
.pcdc.pcdc input.pcdc-toggle:checked { background: linear-gradient(135deg, var(--pcdc-blue), var(--pcdc-blue-2)); }
.pcdc.pcdc input.pcdc-toggle:checked::after { transform: translateX(18px); }
.pcdc.pcdc input.pcdc-toggle:focus-visible { outline: 2px solid var(--pcdc-blue-2); outline-offset: 2px; }

/* Segmented choice built on radio buttons. */
.pcdc .pcdc-seg {
  display: inline-grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 2px; padding: 3px;
  border-radius: 99px; background: var(--pcdc-glass); border: 1px solid var(--pcdc-glass-line);
}
.pcdc .pcdc-seg label { position: relative; margin: 0; cursor: pointer; }
.pcdc .pcdc-seg input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }
.pcdc .pcdc-seg span {
  display: block; padding: 7px 12px; border-radius: 99px; text-align: center; white-space: nowrap;
  font-size: .8rem; font-weight: 600; color: var(--pcdc-muted); transition: background .2s, color .2s;
}
.pcdc .pcdc-seg input:checked + span { color: #fff; background: linear-gradient(135deg, var(--pcdc-blue), var(--pcdc-blue-2)); }
.pcdc .pcdc-seg input:focus-visible + span { outline: 2px solid var(--pcdc-blue-2); outline-offset: 1px; }

/* Slider. */
.pcdc .pcdc-range { display: flex; align-items: center; gap: 12px; }
.pcdc.pcdc .pcdc-range input { flex: 1; margin: 0; accent-color: var(--pcdc-blue-2); }
.pcdc .pcdc-range output { min-width: 4.4em; text-align: right; font-size: .82rem; font-weight: 700; color: var(--pcdc-title); font-variant-numeric: tabular-nums; }

.pcdc .pcdc-dlg-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-top: 16px; }
.pcdc button.pcdc-btn {
  min-height: 42px; padding: 0 18px; border-radius: 99px; font-size: .86rem; font-weight: 700;
  color: var(--pcdc-title); background: var(--pcdc-glass); border: 1px solid var(--pcdc-glass-line);
}
.pcdc button.pcdc-btn:hover { background: var(--pcdc-glass-hover); color: var(--pcdc-title); }
.pcdc button.pcdc-btn.is-primary, .pcdc button.pcdc-btn.is-primary:hover {
  color: #fff; border-color: transparent; background: linear-gradient(135deg, var(--pcdc-blue), var(--pcdc-blue-2));
}
.pcdc button.pcdc-btn.is-quiet { margin-right: auto; color: var(--pcdc-muted); background: transparent; border-color: transparent; }
.pcdc button.pcdc-btn:focus-visible { outline: 2px solid var(--pcdc-blue-2); outline-offset: 2px; }
.pcdc .pcdc-copy-row { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-top: 8px; }
.pcdc .pcdc-copy-row small { font-size: .76rem; color: var(--pcdc-muted); }

@media (prefers-reduced-motion: reduce) {
  .pcdc *, .pcdc *::before, .pcdc *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
}
</style>

  <header class="pcdc-head">
    <div>
      <p class="pcdc-kicker">PCdlabs · Présent Composé design</p>
      <h2 class="pcdc-h">Applications &amp; expériences</h2>
    </div>
    <div class="pcdc-switch" role="group" aria-label="Mode d'affichage">
      <span class="pcdc-switch-thumb" aria-hidden="true"></span>
      <button type="button" class="pcdc-switch-btn" data-view="carousel" aria-pressed="true">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="5" width="8" height="12" rx="1.6"/><path d="M5 7.5 3 8.5v7l2 1M19 7.5l2 1v7l-2 1"/><path d="M6 20.5c3.8 1 8.2 1 12 0" stroke-linecap="round"/></svg>
        <span class="pcdc-switch-label">Carrousel</span>
      </button>
      <button type="button" class="pcdc-switch-btn" data-view="grid" aria-pressed="false">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><rect x="3.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="13.5" width="7" height="7" rx="1.6"/></svg>
        <span class="pcdc-switch-label">Grille</span>
      </button>
      <button type="button" class="pcdc-switch-btn" data-view="list" aria-pressed="false">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><rect x="3.5" y="4" width="5" height="5" rx="1.2"/><rect x="3.5" y="15" width="5" height="5" rx="1.2"/><path d="M12 6.5h8.5M12 17.5h8.5"/></svg>
        <span class="pcdc-switch-label">Liste</span>
      </button>
    </div>
  </header>

  <div class="pcdc-views">
    <div class="pcdc-stage" tabindex="0" role="region" aria-roledescription="carrousel" aria-label="Projets, utilisez les flèches gauche et droite">
      <div class="pcdc-track"></div>
    </div>
    <div class="pcdc-nav">
      <button type="button" class="pcdc-arrow" data-dir="-1" aria-label="Projet précédent">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg>
      </button>
      <div class="pcdc-caption" aria-live="polite">
        <span class="pcdc-count"></span>
        <strong class="pcdc-cap-title"></strong>
        <span class="pcdc-cap-kind"></span>
      </div>
      <button type="button" class="pcdc-arrow" data-dir="1" aria-label="Projet suivant">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 5l7 7-7 7"/></svg>
      </button>
    </div>
    <div class="pcdc-dots"></div>
    <p class="pcdc-hint">Faites glisser pour tourner · Touchez la carte de face pour l'ouvrir</p>
  </div>

  <footer class="pcdc-foot">
    <span class="pcdc-foot-left">
      <span>© PCdlabs · Présent Composé design</span>
      <button type="button" class="pcdc-dev-link" aria-haspopup="dialog" title="Mode développeur">.dev</button>
    </span>
    <a href="https://presentcomposedesign.fr/">presentcomposedesign.fr</a>
  </footer>

  <!-- Developer mode: access code first, then the settings panel. -->
  <dialog class="pcdc-dlg" aria-label="Mode développeur">
    <form class="pcdc-lock pcdc-dlg-body" autocomplete="off">
      <div class="pcdc-dlg-head">
        <div>
          <p class="pcdc-dlg-kicker">Mode développeur</p>
          <p class="pcdc-dlg-title">Code d'accès</p>
        </div>
        <button type="button" class="pcdc-x" data-close aria-label="Fermer">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg>
        </button>
      </div>
      <input class="pcdc-field" type="password" name="code" aria-label="Code d'accès" autocomplete="off" autocapitalize="off" spellcheck="false" required>
      <p class="pcdc-err" aria-live="assertive"></p>
      <div class="pcdc-dlg-actions">
        <button type="button" class="pcdc-btn" data-close>Annuler</button>
        <button type="submit" class="pcdc-btn is-primary">Entrer</button>
      </div>
    </form>

    <form class="pcdc-settings pcdc-dlg-body" hidden>
      <div class="pcdc-dlg-head">
        <div>
          <p class="pcdc-dlg-kicker">Mode développeur</p>
          <p class="pcdc-dlg-title">Réglages</p>
        </div>
        <button type="button" class="pcdc-x" data-close aria-label="Fermer">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg>
        </button>
      </div>
      <p class="pcdc-dlg-note">Appliqués tout de suite et enregistrés dans ce navigateur, sur cet appareil seulement. Pour qu'ils valent pour tous les visiteurs, copiez la configuration en bas dans le bloc Elementor.</p>

      <label class="pcdc-row">
        <span>Sélecteur d'affichage<small>Boutons Carrousel, Grille, Liste</small></span>
        <input class="pcdc-toggle" type="checkbox" name="switch">
      </label>
      <div class="pcdc-row">
        <span>Affichage par défaut</span>
        <div class="pcdc-seg" role="radiogroup" aria-label="Affichage par défaut">
          <label><input type="radio" name="view" value="carousel"><span>Carrousel</span></label>
          <label><input type="radio" name="view" value="grid"><span>Grille</span></label>
          <label><input type="radio" name="view" value="list"><span>Liste</span></label>
        </div>
      </div>
      <div class="pcdc-row">
        <span>Thème<small>Auto suit le réglage du navigateur</small></span>
        <div class="pcdc-seg" role="radiogroup" aria-label="Thème">
          <label><input type="radio" name="theme" value="auto"><span>Auto</span></label>
          <label><input type="radio" name="theme" value="dark"><span>Sombre</span></label>
          <label><input type="radio" name="theme" value="light"><span>Clair</span></label>
        </div>
      </div>
      <label class="pcdc-row">
        <span>Défilement latéral à la molette<small>Ordinateur : la molette fait tourner le carrousel au lieu de faire défiler la page</small></span>
        <input class="pcdc-toggle" type="checkbox" name="wheel">
      </label>
      <div class="pcdc-row is-wide">
        <span>Taille du carré</span>
        <div class="pcdc-range"><input type="range" name="tile" min="40" max="66" step="1" aria-label="Taille du carré"><output>58 %</output></div>
      </div>
      <div class="pcdc-row is-wide">
        <span>Rotation automatique<small>Délai sans interaction avant que le carrousel tourne seul (salon)</small></span>
        <div class="pcdc-range"><input type="range" name="attract" min="0" max="60" step="5" aria-label="Rotation automatique"><output>20 s</output></div>
      </div>
      <div class="pcdc-row">
        <span>Clic sur la carte</span>
        <div class="pcdc-seg" role="radiogroup" aria-label="Clic sur la carte">
          <label><input type="radio" name="cardLink" value="app"><span>Application</span></label>
          <label><input type="radio" name="cardLink" value="article"><span>Article</span></label>
        </div>
      </div>
      <label class="pcdc-row">
        <span>Liens dans un nouvel onglet</span>
        <input class="pcdc-toggle" type="checkbox" name="newTab">
      </label>

      <div class="pcdc-row is-wide">
        <span>Configuration pour le bloc Elementor<small>À remplacer dans la balise &lt;section class="pcdc" ...&gt;</small></span>
        <textarea class="pcdc-field pcdc-config" readonly rows="5" aria-label="Attributs à copier"></textarea>
        <div class="pcdc-copy-row">
          <small class="pcdc-copy-state" aria-live="polite"></small>
          <button type="button" class="pcdc-btn" data-copy>Copier</button>
        </div>
      </div>

      <div class="pcdc-dlg-actions">
        <button type="button" class="pcdc-btn is-quiet" data-logout>Quitter le mode développeur</button>
        <button type="button" class="pcdc-btn" data-reset>Réinitialiser</button>
        <button type="button" class="pcdc-btn is-primary" data-close>Terminé</button>
      </div>
    </form>
  </dialog>

<script>
(() => {
  'use strict';

  /* ==================================================================
     1. CONTENT: one entry per card. The order here is the order on screen.

     icon        App icon shown in the glass tile. Leave null when the project
                 has no icon: its media (GIF / video / image) fills the tile.
     media       The featured media ("image à la une"), square 1024 x 1024
                 recommended (other ratios are cropped to the square):
                   { type: 'image', src }            jpg / png / webp
                   { type: 'gif',   src, poster? }   animated GIF
                   { type: 'video', src, poster }    mp4 (H.264), muted loop
                 src can be relative to data-media-base, or any full URL
                 (WordPress media library, CDN...).
     appUrl      Link to the application itself (CTA "Ouvrir l'application").
     articleUrl  Link to the presentation article.
     accent      Card tint, taken from the icon or visual.
     ================================================================== */
  const PROJECTS = [
    {
      title: 'enginR',
      kind: 'Visualiseur 3D · AR · VR',
      description: 'Un fichier 3D, dix minutes, une page web immersive. Modèles, gaussian splatting, 360 et données, visibles en AR et en VR sans écrire une ligne de code.',
      tags: ['3D', 'Gaussian splatting', 'WebXR', 'QR code'],
      icon: 'icons/enginr.webp',
      media: { type: 'image', src: 'featured/enginr.webp' },
      appUrl: 'https://pcdlabs.presentcomposedesign.fr/tools/enginR',
      articleUrl: null,
      accent: '#1001ef',
    },
    {
      title: 'BirdSignal',
      kind: 'Bioacoustique',
      description: 'Écouter, identifier, collectionner et signaler les oiseaux qui vous entourent, depuis le micro ou un fichier audio. Cartes à collectionner et statut UICN.',
      tags: ['BirdNET', 'Audio', 'Collection', 'PWA'],
      icon: 'icons/birdsignal.webp',
      media: { type: 'video', src: 'featured/birdsignal.mp4', poster: 'featured/birdsignal.jpg' },
      appUrl: 'https://pcdlabs.presentcomposedesign.fr/tools/birdsignal/',
      articleUrl: 'https://presentcomposedesign.fr/birdsignal-infos/',
      accent: '#3a3cf0',
    },
    {
      title: 'PCd-1210',
      kind: 'Platine vinyle virtuelle',
      description: 'Une platine direct drive dans le navigateur : bibliothèque de vinyles, scratch et lecteur média, installable sur mobile et ordinateur.',
      tags: ['Audio', 'Scratch', '3D', 'PWA'],
      icon: 'icons/platine.webp',
      media: { type: 'image', src: 'featured/platine.webp' },
      appUrl: 'https://pcdlabs.presentcomposedesign.fr/experiences/platine-virtuelle/',
      articleUrl: 'https://presentcomposedesign.fr/pcd-1210_infos/',
      accent: '#ff6f91',
    },
    {
      title: 'CONVRT',
      kind: 'Convertisseur de fichiers',
      description: 'Convertir images, vidéos, sons et modèles 3D sans rien téléverser : tout se passe dans le navigateur.',
      tags: ['Images', 'Vidéo', 'Audio', '3D'],
      icon: 'icons/convrt.webp',
      media: null,
      appUrl: 'https://pcdlabs.presentcomposedesign.fr/tools/CONVRT/',
      articleUrl: null,
      accent: '#c8fe44',
    },
    {
      title: 'Le Prompteur',
      kind: 'Générateur de prompts',
      description: 'Des prompts optimisés pour Mistral, Claude, ChatGPT, Gemini, Suno et Rodin, construits en quelques choix.',
      tags: ['IA', 'Texte', 'Image', 'Musique'],
      icon: 'icons/leprompteur.webp',
      media: { type: 'image', src: 'featured/leprompteur.webp' },
      appUrl: 'https://pcdlabs.presentcomposedesign.fr/tools/leprompteur',
      articleUrl: null,
      accent: '#ff8a1e',
    },
    {
      title: 'Spotbaignade',
      kind: 'Carte des baignades',
      description: 'Lacs, rivières et bases de loisirs pour se baigner autour de vous, en Occitanie. Géolocalisé, gratuit et open source.',
      tags: ['Carte', 'Géolocalisation', 'Open source'],
      icon: 'icons/spotbaignade.webp',
      media: null,
      appUrl: 'https://pcdlabs.presentcomposedesign.fr/tools/Spotbaignade/',
      articleUrl: 'https://presentcomposedesign.fr/spotbaignade/',
      accent: '#1d7fe0',
    },
    {
      title: 'SplatGate',
      kind: 'Hub VR',
      description: 'Un portail de voyage virtuel pour explorer des paysages réalistes depuis un ordinateur, un mobile ou un casque XR.',
      tags: ['VR', 'Gaussian splatting', 'Voyage'],
      icon: null,
      media: { type: 'video', src: 'featured/splatgate.mp4', poster: 'featured/splatgate.jpg' },
      appUrl: null,
      articleUrl: 'https://presentcomposedesign.fr/splatgate/',
      accent: '#52b2f9',
    },
    {
      title: 'Paper Plane',
      kind: 'Avatar WebXR',
      description: 'Un avatar-avion en papier, simple et universel, pensé pour traverser les mondes immersifs avec légèreté.',
      tags: ['WebXR', 'Avatar', 'Arrival.space'],
      icon: null,
      media: { type: 'video', src: 'featured/paperplane.mp4', poster: 'featured/paperplane.jpg' },
      appUrl: null,
      articleUrl: 'https://presentcomposedesign.fr/paper-plane-mode_for_arrival-space/',
      accent: '#e79bb0',
    },
    {
      title: 'Curiosity',
      kind: 'IA culturelle',
      description: 'Un oracle culturel français qui invite le public à poser des questions et à explorer un savoir vivant.',
      tags: ['IA', 'Culture', 'Médiation'],
      icon: null,
      media: { type: 'image', src: 'featured/curiosity.webp' },
      appUrl: null,
      articleUrl: 'https://presentcomposedesign.fr/curiosity/',
      accent: '#7ecf73',
    },
    {
      title: 'RIZOM',
      kind: 'WebXR · hologrammes',
      description: 'Une exploration WebXR autour du passage de la vidéo vers des présences 3D et des hologrammes navigables.',
      tags: ['WebXR', 'Hologrammes', 'Vidéo volumétrique'],
      icon: null,
      media: { type: 'video', src: 'featured/rizom.mp4', poster: 'featured/rizom.jpg' },
      appUrl: null,
      articleUrl: 'https://presentcomposedesign.fr/rizom/',
      accent: '#4b63ff',
    },
    {
      title: 'Les MoliAires',
      kind: 'Exposition augmentée',
      description: 'Neuf expressions françaises transformées par l’IA en tableaux absurdes et poétiques, visitables en réalité augmentée.',
      tags: ['AR', 'IA', 'Exposition'],
      icon: null,
      media: { type: 'video', src: 'featured/moliaires.mp4', poster: 'featured/moliaires.jpg' },
      appUrl: 'https://pcdlabs.presentcomposedesign.fr/experiences/les-moliaires/',
      articleUrl: 'https://presentcomposedesign.fr/les-moliaires-exposition-augmentee/',
      accent: '#e2564f',
    },
  ];

  /* Interface wording (French site). */
  const UI = {
    openApp: 'Ouvrir l’application',
    discover: 'Découvrir le projet',
    readArticle: 'Lire l’article',
    badgeApp: 'Application',
    badgeProject: 'Projet',
    goTo: 'Afficher',
  };

  const ARROW_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7M9 7h8v8"/></svg>';
  const TAU = Math.PI * 2;
  const VIEWS = ['carousel', 'grid', 'list'];
  const VIEW_PARAM = { carrousel: 'carousel', grille: 'grid', liste: 'list', carousel: 'carousel', grid: 'grid', list: 'list' };
  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
  const noHover = window.matchMedia('(hover: none)');

  /* Browser storage only remembers the chosen view; it may be unavailable. */
  const store = {
    get(k) { try { return window.localStorage.getItem(k); } catch (e) { return null; } },
    set(k, v) { try { window.localStorage.setItem(k, v); } catch (e) { /* private mode */ } },
  };

  /* Tiny DOM helper: h('a', { class: 'x', href }, child, 'text'). */
  function h(tag, attrs, ...children) {
    const el = document.createElement(tag);
    for (const [k, v] of Object.entries(attrs || {})) {
      if (v === null || v === undefined || v === false) continue;
      el.setAttribute(k, v === true ? '' : String(v));
    }
    for (const c of children) if (c !== null && c !== undefined) el.append(c);
    return el;
  }

  function hexRgb(hex) {
    const m = /^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(hex || '');
    return m ? m.slice(1).map((x) => parseInt(x, 16)) : [16, 1, 239];
  }
  const rgba = (c, a) => `rgba(${c[0]}, ${c[1]}, ${c[2]}, ${a})`;
  const tint = (c, t) => `rgb(${c.map((v) => Math.round(v + (255 - v) * t)).join(', ')})`;
  const deepen = (c, t) => `rgb(${c.map((v) => Math.round(v * (1 - t))).join(', ')})`;

  /* Developer mode: SHA-256 fingerprint of the access code (the code itself
     is not written anywhere in this page). */
  const DEV_CODE_SHA256 = '4aec739611b5e38fad43e4e77a0349dfcef548e65d4daa51f304c6f624816909';
  const SETTINGS_KEY = 'pcdc-settings';
  const UNLOCK_KEY = 'pcdc-dev';
  const clampNum = (v, lo, hi, dflt) => { const x = Number(v); return Number.isFinite(x) ? Math.min(hi, Math.max(lo, x)) : dflt; };

  // Elementor may run this script again when the widget is edited: only
  // blocks that are not yet initialised get wired.
  document.querySelectorAll('.pcdc:not([data-pcdc-ready])').forEach(init);

  function init(root) {
    root.setAttribute('data-pcdc-ready', '');

    const base = root.dataset.mediaBase || '';

    /* Settings: the attributes of the block are the defaults for everyone;
       the developer panel may override them for this browser only. */
    const d = root.dataset;
    const DEFAULTS = {
      view: VIEWS.includes(d.view) ? d.view : 'carousel',
      theme: ['auto', 'dark', 'light'].includes(d.theme) ? d.theme : 'dark',
      switch: d.switch !== 'false',
      wheel: d.wheel === 'lateral',
      tile: clampNum(d.tile, 40, 66, 58),
      attract: clampNum(d.attract ?? 20, 0, 600, 20),
      cardLink: d.cardLink === 'article' ? 'article' : 'app',
      newTab: d.newTab !== 'false',
    };
    let cfg = { ...DEFAULTS };
    try {
      const saved = JSON.parse(store.get(SETTINGS_KEY) || 'null');
      if (saved && typeof saved === 'object') {
        for (const k of Object.keys(DEFAULTS)) if (typeof saved[k] === typeof DEFAULTS[k]) cfg[k] = saved[k];
      }
    } catch (e) { /* corrupted entry: keep the defaults */ }

    const stage = root.querySelector('.pcdc-stage');
    const track = root.querySelector('.pcdc-track');
    const views = root.querySelector('.pcdc-views');
    const switchBtns = [...root.querySelectorAll('.pcdc-switch-btn')];
    const dotsBox = root.querySelector('.pcdc-dots');
    const capCount = root.querySelector('.pcdc-count');
    const capTitle = root.querySelector('.pcdc-cap-title');
    const capKind = root.querySelector('.pcdc-cap-kind');
    const dlg = root.querySelector('.pcdc-dlg');

    /* Relative media paths are resolved against data-media-base. */
    const resolve = (p) => (!p ? '' : /^(https?:)?\/\/|^\/|^data:|^blob:/i.test(p) ? p : base + p);

    /* --------------------------- build cards --------------------------- */

    function mediaEl(m, cls, role) {
      if (m.type === 'video') {
        // No src yet: a video is only downloaded the first time it has to play.
        const v = h('video', {
          class: cls, muted: true, loop: true, playsinline: true, preload: 'none',
          poster: resolve(m.poster) || null, 'aria-hidden': 'true', disablepictureinpicture: true,
        });
        v.muted = true;
        v.dataset.src = resolve(m.src);
        v.dataset.role = role;
        return v;
      }
      return h('img', { class: cls, src: resolve(m.src), alt: '', decoding: 'async', draggable: 'false' });
    }

    const cards = PROJECTS.map((p, i) => {
      const rgb = hexRgb(p.accent);
      const primaryUrl = p.appUrl || p.articleUrl || '#';
      const still = p.media ? resolve(p.media.type === 'image' ? p.media.src : (p.media.poster || p.media.src)) : '';
      const num = String(i + 1).padStart(2, '0');

      const el = h('article', { class: 'pcdc-card', 'data-index': i });
      el.style.setProperty('--pcdc-accent', p.accent || '#1001ef');
      el.style.setProperty('--pcdc-accent-soft', rgba(rgb, .55));
      el.style.setProperty('--pcdc-accent-glow', rgba(rgb, .65));
      // Accent text: lightened on dark cards, darkened on light cards (see the theme rules).
      el.style.setProperty('--pcdc-accent-light', tint(rgb, .5));
      el.style.setProperty('--pcdc-accent-deep', deepen(rgb, .45));

      // Featured media behind everything.
      const bg = h('div', { class: 'pcdc-bg' });
      if (p.icon && p.media) {
        bg.append(mediaEl(p.media, 'pcdc-bg-media', 'bg'));
      } else if (!p.icon && still) {
        bg.classList.add('is-blurred');
        bg.append(h('img', { class: 'pcdc-bg-media', src: still, alt: '', decoding: 'async', draggable: 'false' }));
      } else if (p.icon) {
        bg.classList.add('is-icon');
        bg.append(h('img', { class: 'pcdc-bg-media', src: resolve(p.icon), alt: '', decoding: 'async', draggable: 'false' }));
      }

      // Glass tile.
      const tileBlur = h('div', { class: 'pcdc-tile-blur' });
      if (still) tileBlur.style.setProperty('--pcdc-still', `url("${still.replace(/"/g, '%22')}")`);
      let content;
      if (p.icon) {
        content = h('div', { class: 'pcdc-tile-content is-icon' },
          h('img', { src: resolve(p.icon), alt: '', decoding: 'async', draggable: 'false' }));
      } else if (p.media) {
        content = h('div', { class: 'pcdc-tile-content is-media' }, mediaEl(p.media, 'pcdc-tile-media', 'tile'));
      } else {
        content = h('div', { class: 'pcdc-tile-content is-letter' }, p.title.charAt(0));
      }
      const tile = h('div', { class: 'pcdc-tile' }, tileBlur, content);

      const top = h('div', { class: 'pcdc-top' },
        h('span', { class: 'pcdc-pill' }, num),
        h('span', { class: 'pcdc-pill' + (p.appUrl ? ' is-app' : '') }, p.appUrl ? UI.badgeApp : UI.badgeProject));

      // CTAs: the app first, the article as a second choice.
      const actions = h('div', { class: 'pcdc-actions' });
      const primary = h('a', { class: 'pcdc-cta is-primary', href: primaryUrl,
        'aria-label': `${p.appUrl ? UI.openApp : UI.discover} : ${p.title}` });
      primary.innerHTML = `<span>${p.appUrl ? UI.openApp : UI.discover}</span>${ARROW_SVG}`;
      actions.append(primary);
      if (p.appUrl && p.articleUrl) {
        actions.append(h('a', { class: 'pcdc-cta is-secondary', href: p.articleUrl,
          'aria-label': `${UI.readArticle} : ${p.title}` }, UI.readArticle));
      }

      const tags = h('ul', { class: 'pcdc-tags' }, ...(p.tags || []).map((t) => h('li', null, t)));
      const bottom = h('div', { class: 'pcdc-bottom' },
        h('div', { class: 'pcdc-info' },
          h('span', { class: 'pcdc-kind' }, p.kind || ''),
          h('h3', { class: 'pcdc-title' }, p.title),
          h('div', { class: 'pcdc-more' }, h('div', null, h('p', { class: 'pcdc-desc' }, p.description || ''), tags))),
        h('div', { class: 'pcdc-actions-wrap' }, h('div', null, actions)));

      // The card body itself is a link (kept out of the tab order: the CTA is the focus target).
      // Its href and target are set by applyLinks(), since the settings can change them.
      const hit = h('a', { class: 'pcdc-hit', href: primaryUrl, tabindex: '-1', 'aria-hidden': 'true' });

      const front = h('div', { class: 'pcdc-face pcdc-front' },
        h('div', { class: 'pcdc-in' }, bg, h('div', { class: 'pcdc-shade' }), top, tile, hit, bottom));

      // Back face: in a tourniquet some cards are always seen from behind.
      const backMark = p.icon
        ? h('img', { class: 'pcdc-back-mark', src: resolve(p.icon), alt: '', decoding: 'async', draggable: 'false' })
        : still ? h('img', { class: 'pcdc-back-mark is-photo', src: still, alt: '', decoding: 'async', draggable: 'false' }) : null;
      const back = h('div', { class: 'pcdc-face pcdc-back', 'aria-hidden': 'true' },
        h('div', { class: 'pcdc-in' }, backMark,
          h('div', { class: 'pcdc-back-text' }, h('strong', null, p.title), h('span', null, 'PCdlabs · ' + num))));

      el.append(front, back,
        h('i', { class: 'pcdc-edge pcdc-edge-l' }), h('i', { class: 'pcdc-edge pcdc-edge-r' }), h('i', { class: 'pcdc-edge pcdc-edge-t' }));
      track.append(el);

      return { el, p, hit, primaryUrl, videos: [...el.querySelectorAll('video')], links: [...el.querySelectorAll('a.pcdc-cta')], visible: false };
    });

    // Card link and tab behaviour, re-applied whenever the settings change.
    function applyLinks() {
      cards.forEach((c) => {
        c.hit.href = cfg.cardLink === 'article' && c.p.articleUrl ? c.p.articleUrl : c.primaryUrl;
        c.el.querySelectorAll('a').forEach((a) => {
          if (cfg.newTab) { a.target = '_blank'; a.rel = 'noopener'; } else { a.removeAttribute('target'); a.removeAttribute('rel'); }
        });
      });
    }
    applyLinks();

    const n = cards.length;
    if (!n) return;
    const step = TAU / n;

    const dots = PROJECTS.map((p, i) => {
      const b = h('button', { type: 'button', class: 'pcdc-dot', 'aria-label': `${UI.goTo} ${p.title}` });
      b.addEventListener('click', () => { userActed(); goTo(i); });
      dotsBox.append(b);
      return b;
    });

    /* ------------------------ carousel engine ------------------------
       Same logic as the BirdSignal favourites tourniquet: cards stand on a
       ring, EDGE-ON, and only turn to face you once they reach the front
       (turn = cos^6, so exactly one card faces at a time). The front card
       grows and rises, the others dim with depth. Here the ring is drawn
       with CSS 3D instead of WebGL, so GIFs, videos, links and hover
       panels stay native HTML. */
    let S = 320;           // card size in px
    let R = 880;           // ring radius in px
    let target = 0;        // ring angle we are heading to
    let current = 0;       // ring angle on screen
    let hovered = -1;
    let frontIdx = -1;
    let tiltX = 0, tiltZ = 0, tiltTX = 0, tiltTZ = 0;
    let raf = 0;
    let view = 'carousel';

    function layout() {
      const W = root.clientWidth || 800;
      const vh = window.innerHeight || 800;
      // Wide screens show the front card plus neighbours; phones show one card.
      let s = W >= 900 ? W / 3.1 : W >= 600 ? W / 2.45 : W * 0.66;
      s = Math.max(170, Math.min(s, 460, vh * 0.56));
      S = Math.round(s);
      // The arc between two cards must exceed a card width, or they interpenetrate.
      R = Math.max(1.6 * S, (1.55 * S) / (2 * Math.sin(Math.PI / Math.max(3, n))));
      root.style.setProperty('--pcdc-size', S + 'px');
      root.style.setProperty('--pcdc-persp', Math.round(S * 2.5) + 'px');
    }

    const frontOf = (angle) => ((Math.round(-angle / step) % n) + n) % n;

    function frame() {
      raf = 0;
      if (view !== 'carousel') return;
      const k = reduceMotion.matches ? 0.4 : 0.12;
      current += (target - current) * k;
      if (Math.abs(target - current) < 1e-4) current = target;
      tiltX += (tiltTX - tiltX) * 0.08;
      tiltZ += (tiltTZ - tiltZ) * 0.08;

      // Slight high-angle view: gives the ring its depth without drawing a stand.
      // Tilting the ring about its centre would push the front card down by
      // R.sin(pitch); the leading translateY puts it back in the middle.
      const pitch = ((-6 + tiltX) * Math.PI) / 180;
      track.style.transform =
        `translateY(${(R * Math.sin(pitch)).toFixed(1)}px) translateZ(${(-R).toFixed(1)}px) rotateX(${pitch.toFixed(4)}rad) rotateZ(${tiltZ.toFixed(2)}deg) rotateY(${current.toFixed(4)}rad)`;

      for (let i = 0; i < n; i++) {
        const a = i * step + current;
        const front = Math.cos(a);                 // 1 = right in front, -1 = behind
        const turn = Math.pow(Math.max(0, front), 6);
        // Neighbours stand edge-on on BOTH sides, like cards in an index box:
        // the quarter turn follows the side of the ring (smoothly, so a card
        // passing behind does not flip).
        const side = Math.max(-1, Math.min(1, Math.sin(a) * 4));
        const edge = (Math.PI / 2) * (1 - turn) * side;
        const hot = i === hovered ? 1 : 0;
        const scale = (0.82 + 0.32 * turn + 0.09 * hot) / 1.14;
        const lift = -(0.07 * turn + 0.04 * hot) * S;
        const c = cards[i].el;
        c.style.transform =
          `rotateY(${(i * step).toFixed(4)}rad) translateZ(${R.toFixed(1)}px) rotateY(${edge.toFixed(4)}rad) translateY(${lift.toFixed(1)}px) scale(${scale.toFixed(4)})`;
        c.style.setProperty('--pcdc-dim', ((1 - (front * 0.5 + 0.5)) * 0.62).toFixed(3));
        c.style.setProperty('--pcdc-sheen', (50 + Math.sin(a) * 90).toFixed(1));
      }

      const f = frontOf(target);
      if (f !== frontIdx) setFront(f);

      const settling = current !== target || Math.abs(tiltTX - tiltX) > 0.01 || Math.abs(tiltTZ - tiltZ) > 0.01;
      if (settling) kick();                        // the loop sleeps once everything is still
    }
    const kick = () => { if (!raf && view === 'carousel') raf = requestAnimationFrame(frame); };

    function setFront(f) {
      frontIdx = f;
      const p = PROJECTS[f];
      capCount.textContent = `${String(f + 1).padStart(2, '0')} / ${String(n).padStart(2, '0')}`;
      capTitle.textContent = p.title;
      capKind.textContent = p.kind || '';
      dots.forEach((d, i) => d.setAttribute('aria-current', String(i === f)));
      cards.forEach((c, i) => {
        c.el.classList.toggle('is-front', i === f);
        // No hover on a tablet: the front card shows its details straight away,
        // so one tap on it opens the app.
        c.el.classList.toggle('is-open', i === f && noHover.matches);
        if (i !== f) c.el.classList.remove('is-hover');
        else if (i === hovered) c.el.classList.add('is-hover');
      });
      syncTabindex();
      syncMedia();
    }

    function goTo(i) {                             // shortest way round
      const aim = -i * step;
      target = aim + Math.round((target - aim) / TAU) * TAU;
      kick();
    }
    const nudge = (d) => { target = Math.round(target / step) * step - d * step; kick(); };
    const snap = () => { target = Math.round(target / step) * step; kick(); };

    /* ------------------------------ views ------------------------------ */
    function setView(v, persist) {
      if (!VIEWS.includes(v)) v = 'carousel';
      view = v;
      root.dataset.view = v;
      root.style.setProperty('--pcdc-view-i', VIEWS.indexOf(v));
      switchBtns.forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.view === v)));
      cards.forEach((c) => c.el.classList.remove('is-open', 'is-hover'));
      if (v === 'carousel') {
        layout();
        frontIdx = -1;                             // force caption / classes refresh
        kick();
      } else {
        cancelAnimationFrame(raf); raf = 0;
        track.style.transform = '';
        cards.forEach((c) => {
          c.el.style.transform = '';
          c.el.style.removeProperty('--pcdc-dim');
          c.el.style.removeProperty('--pcdc-sheen');
          c.el.classList.remove('is-front');
        });
      }
      syncTabindex();
      syncMedia();
      if (persist) store.set('pcdc-view', v);
      views.classList.remove('is-entering');
      void views.offsetWidth;
      views.classList.add('is-entering');
    }
    switchBtns.forEach((b) => b.addEventListener('click', () => { userActed(); setView(b.dataset.view, true); }));

    // Only the front card's buttons are reachable with Tab in the carousel.
    function syncTabindex() {
      cards.forEach((c, i) => {
        const reachable = view !== 'carousel' || i === frontIdx;
        c.links.forEach((a) => (reachable ? a.removeAttribute('tabindex') : a.setAttribute('tabindex', '-1')));
      });
    }

    /* ------------------------------ media ------------------------------
       Videos play only where they are seen: the front card of the carousel,
       or cards on screen in grid / list. Everything pauses when the block is
       scrolled away or the tab is hidden: a tablet on a stand runs all day. */
    let rootVisible = true;
    function syncMedia() {
      const on = rootVisible && !document.hidden && !reduceMotion.matches;
      cards.forEach((c, i) => {
        const seen = view === 'carousel' ? i === frontIdx : c.visible;
        c.videos.forEach((v) => {
          const want = on && seen && !(view === 'list' && v.dataset.role === 'bg');
          if (want) {
            if (!v.getAttribute('src')) v.src = v.dataset.src;
            if (v.paused) { const pr = v.play(); if (pr && pr.catch) pr.catch(() => { /* poster stays */ }); }
          } else if (!v.paused) {
            v.pause();
          }
        });
      });
    }
    if ('IntersectionObserver' in window) {
      new IntersectionObserver((entries) => {
        rootVisible = entries[0].isIntersecting;
        syncMedia();
      }).observe(root);
      const cardIO = new IntersectionObserver((entries) => {
        entries.forEach((e) => { cards[+e.target.dataset.index].visible = e.isIntersecting; });
        syncMedia();
      }, { threshold: 0.2 });
      cards.forEach((c) => cardIO.observe(c.el));
    } else {
      cards.forEach((c) => { c.visible = true; });
    }
    document.addEventListener('visibilitychange', syncMedia);

    /* --------------------------- interaction --------------------------- */
    let lastAct = performance.now();
    let lastAttract = 0;
    function userActed() {
      lastAct = performance.now();
      root.classList.add('has-interacted');
    }

    // Drag / swipe. The pointer is only captured once the gesture is clearly
    // horizontal, so a plain tap still reaches the links, and a vertical swipe
    // still scrolls the page on a tablet.
    let drag = null;
    let suppressClick = false;
    stage.addEventListener('pointerdown', (e) => {
      if (view !== 'carousel' || e.button > 0) return;
      suppressClick = false;
      drag = { id: e.pointerId, x0: e.clientX, y0: e.clientY, x: e.clientX, active: false };
      userActed();
    });
    stage.addEventListener('pointermove', (e) => {
      if (view !== 'carousel') return;
      if (e.pointerType === 'mouse' && !drag) {    // subtle parallax, as the gyroscope did on phones
        const r = stage.getBoundingClientRect();
        tiltTZ = ((e.clientX - r.left) / r.width - 0.5) * 3;
        tiltTX = ((e.clientY - r.top) / r.height - 0.5) * -4;
        userActed();
        kick();
      }
      if (!drag || e.pointerId !== drag.id) return;
      const dx = e.clientX - drag.x0;
      const dy = e.clientY - drag.y0;
      if (!drag.active) {
        if (Math.hypot(dx, dy) < 8) return;
        if (Math.abs(dy) > Math.abs(dx)) { drag = null; return; }
        drag.active = true;
        try { stage.setPointerCapture(e.pointerId); } catch (err) { /* already released */ }
        root.classList.add('is-dragging');
      }
      target += (e.clientX - drag.x) / (R * 0.8);  // the front card follows the finger
      drag.x = e.clientX;
      kick();
    });
    const endDrag = (e) => {
      if (!drag || e.pointerId !== drag.id) return;
      if (drag.active) {
        suppressClick = true;                      // the release of a drag is not a click
        setTimeout(() => { suppressClick = false; }, 350);
        snap();
      }
      drag = null;
      root.classList.remove('is-dragging');
    };
    stage.addEventListener('pointerup', endDrag);
    stage.addEventListener('pointercancel', endDrag);
    // Links and images are natively draggable: that would cancel the swipe.
    stage.addEventListener('dragstart', (e) => e.preventDefault());
    stage.addEventListener('pointerleave', () => { tiltTX = 0; tiltTZ = 0; kick(); });

    // Horizontal wheel / trackpad (or Shift + wheel) always turns the ring. The
    // vertical wheel is left to the page, unless "lateral scrolling" is on in
    // the settings: then it turns the ring too while the pointer is over it.
    let wheelTimer = 0;
    stage.addEventListener('wheel', (e) => {
      if (view !== 'carousel') return;
      const unit = e.deltaMode === 1 ? 16 : 1;
      const d = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : (e.shiftKey || cfg.wheel) ? e.deltaY : 0;
      if (!d) return;
      e.preventDefault();
      userActed();
      target -= d * unit * 0.0022;
      clearTimeout(wheelTimer);
      wheelTimer = setTimeout(snap, 140);
      kick();
    }, { passive: false });

    stage.addEventListener('keydown', (e) => {
      if (view !== 'carousel') return;
      if (e.key === 'ArrowRight') nudge(1);
      else if (e.key === 'ArrowLeft') nudge(-1);
      else if (e.key === 'Home') goTo(0);
      else if (e.key === 'End') goTo(n - 1);
      else if ((e.key === 'Enter' || e.key === ' ') && e.target === stage) cards[frontIdx].el.querySelector('.pcdc-hit').click();
      else return;
      e.preventDefault();
      userActed();
    });
    root.querySelectorAll('.pcdc-arrow').forEach((b) =>
      b.addEventListener('click', () => { userActed(); nudge(Number(b.dataset.dir)); }));

    // Hover (mouse only): lifts the card and shows its details.
    track.addEventListener('pointerover', (e) => {
      if (e.pointerType !== 'mouse') return;
      const card = e.target.closest('.pcdc-card');
      setHover(card ? +card.dataset.index : -1);
    });
    track.addEventListener('pointerleave', () => setHover(-1));
    function setHover(i) {
      if (i === hovered) return;
      hovered = i;
      cards.forEach((c, j) => c.el.classList.toggle('is-hover', j === i && (view !== 'carousel' || j === frontIdx)));
      kick();
    }

    // Clicks are decided here, in the capture phase, before any link reacts.
    root.addEventListener('click', (e) => {
      if (suppressClick) { e.preventDefault(); e.stopPropagation(); suppressClick = false; return; }
      const card = e.target.closest('.pcdc-card');
      if (!card) {
        if (!e.target.closest('button')) cards.forEach((c) => view !== 'carousel' && c.el.classList.remove('is-open'));
        return;
      }
      const i = +card.dataset.index;
      userActed();
      if (view === 'carousel') {
        // A card on the side first comes round to the front; the front card opens.
        if (i !== frontIdx) { e.preventDefault(); goTo(i); }
        return;
      }
      // Grid on a touch screen: first tap shows the details, second tap opens.
      if (view === 'grid' && noHover.matches && !card.classList.contains('is-open') && !e.target.closest('.pcdc-cta')) {
        e.preventDefault();
        cards.forEach((c) => c.el.classList.toggle('is-open', c.el === card));
      }
    }, true);

    // Attract mode: after a while without anyone touching the tablet, the
    // carousel turns on its own; the first touch stops it.
    setInterval(() => {
      if (!cfg.attract || view !== 'carousel' || drag || dlg.open || document.hidden || !rootVisible || reduceMotion.matches) return;
      const now = performance.now();
      if (now - lastAct > cfg.attract * 1000 && now - lastAttract > 4500) { lastAttract = now; nudge(1); }
    }, 500);

    if ('ResizeObserver' in window) {
      new ResizeObserver(() => { if (view === 'carousel') { layout(); kick(); } }).observe(root);
    } else {
      window.addEventListener('resize', () => { if (view === 'carousel') { layout(); kick(); } });
    }

    /* ----------------------------- settings ----------------------------- */
    function applySettings() {
      root.dataset.theme = cfg.theme;
      root.dataset.switch = String(cfg.switch);
      root.style.setProperty('--pcdc-tile-w', String(cfg.tile / 100));
      applyLinks();
    }

    /* -------------------------- developer mode --------------------------
       ".dev" in the footer -> access code -> settings panel. The unlock lasts
       for the browser session; the settings themselves stay in this browser
       until "Réinitialiser". */
    const lockForm = dlg.querySelector('.pcdc-lock');
    const setForm = dlg.querySelector('.pcdc-settings');
    const codeInput = lockForm.elements.code;
    const codeErr = lockForm.querySelector('.pcdc-err');
    const configBox = setForm.querySelector('.pcdc-config');
    const copyState = setForm.querySelector('.pcdc-copy-state');
    const session = {
      get() { try { return window.sessionStorage.getItem(UNLOCK_KEY) === '1'; } catch (e) { return false; } },
      set(on) { try { if (on) window.sessionStorage.setItem(UNLOCK_KEY, '1'); else window.sessionStorage.removeItem(UNLOCK_KEY); } catch (e) { /* ignore */ } },
    };

    async function sha256(text) {
      const buf = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
      return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, '0')).join('');
    }

    function showPanel(unlocked) {
      lockForm.hidden = unlocked;
      setForm.hidden = !unlocked;
      if (unlocked) fillSettings();
      else { codeInput.value = ''; codeErr.textContent = ''; }
      (unlocked ? setForm.querySelector('.pcdc-x') : codeInput).focus();
    }

    root.querySelector('.pcdc-dev-link').addEventListener('click', () => {
      if (typeof dlg.showModal === 'function') dlg.showModal(); else dlg.setAttribute('open', '');
      showPanel(session.get());
    });
    dlg.querySelectorAll('[data-close]').forEach((b) => b.addEventListener('click', () => dlg.close()));
    dlg.addEventListener('click', (e) => { if (e.target === dlg) dlg.close(); });   // click on the backdrop

    lockForm.addEventListener('submit', async (e) => {
      e.preventDefault();
      if (!window.crypto || !window.crypto.subtle) {
        codeErr.textContent = 'Vérification impossible ici : la page doit être servie en HTTPS.';
        return;
      }
      if ((await sha256(codeInput.value)) === DEV_CODE_SHA256) {
        session.set(true);
        showPanel(true);
      } else {
        codeErr.textContent = 'Code incorrect.';
        codeInput.value = '';
        lockForm.classList.remove('is-wrong'); void lockForm.offsetWidth; lockForm.classList.add('is-wrong');
        codeInput.focus();
      }
    });

    function configText() {
      return [
        `data-view="${cfg.view}"`,
        `data-theme="${cfg.theme}"`,
        `data-switch="${cfg.switch}"`,
        `data-wheel="${cfg.wheel ? 'lateral' : 'off'}"`,
        `data-tile="${cfg.tile}"`,
        `data-attract="${cfg.attract}"`,
        `data-card-link="${cfg.cardLink}"`,
        `data-new-tab="${cfg.newTab}"`,
      ].join('\n');
    }
    function syncOutputs() {
      setForm.elements.tile.nextElementSibling.textContent = cfg.tile + ' %';
      setForm.elements.attract.nextElementSibling.textContent = cfg.attract ? cfg.attract + ' s' : 'désactivée';
      configBox.value = configText();
    }
    function fillSettings() {
      const f = setForm.elements;
      f.switch.checked = cfg.switch;
      f.view.value = cfg.view;
      f.theme.value = cfg.theme;
      f.wheel.checked = cfg.wheel;
      f.tile.value = cfg.tile;
      f.attract.value = Math.min(60, cfg.attract);
      f.cardLink.value = cfg.cardLink;
      f.newTab.checked = cfg.newTab;
      copyState.textContent = '';
      syncOutputs();
    }
    function readSettings(e) {
      const f = setForm.elements;
      const before = cfg.view;
      cfg = {
        view: f.view.value, theme: f.theme.value, switch: f.switch.checked, wheel: f.wheel.checked,
        tile: clampNum(f.tile.value, 40, 66, 58), attract: clampNum(f.attract.value, 0, 600, 20),
        cardLink: f.cardLink.value, newTab: f.newTab.checked,
      };
      store.set(SETTINGS_KEY, JSON.stringify(cfg));
      applySettings();
      // A new default view is shown at once and replaces the visitor's last choice.
      if (e && e.target.name === 'view' && cfg.view !== before) { store.set('pcdc-view', ''); setView(cfg.view, false); }
      // Hiding the switch brings the display back to the default view.
      if (e && e.target.name === 'switch' && !cfg.switch && view !== cfg.view) setView(cfg.view, false);
      copyState.textContent = '';
      syncOutputs();
    }
    setForm.addEventListener('input', readSettings);
    setForm.addEventListener('change', readSettings);
    setForm.addEventListener('submit', (e) => e.preventDefault());

    setForm.querySelector('[data-copy]').addEventListener('click', async () => {
      try {
        await navigator.clipboard.writeText(configBox.value);
        copyState.textContent = 'Copié.';
      } catch (err) {
        configBox.select();                        // no clipboard access: select it for a manual copy
        copyState.textContent = 'Sélectionné : Ctrl+C pour copier.';
      }
    });
    setForm.querySelector('[data-reset]').addEventListener('click', () => {
      try { window.localStorage.removeItem(SETTINGS_KEY); } catch (e) { /* ignore */ }
      cfg = { ...DEFAULTS };
      applySettings();
      setView(cfg.view, false);
      fillSettings();
      copyState.textContent = 'Réglages du bloc rétablis.';
    });
    setForm.querySelector('[data-logout]').addEventListener('click', () => {
      session.set(false);
      dlg.close();
    });

    /* ------------------------------ start ------------------------------ */
    applySettings();
    let startView = cfg.view;
    const saved = store.get('pcdc-view');
    if (saved && cfg.switch) startView = saved;     // the visitor's own choice, if the switch is shown
    try {
      const q = new URLSearchParams(window.location.search).get('vue');
      if (q && VIEW_PARAM[q.toLowerCase()]) startView = VIEW_PARAM[q.toLowerCase()];
    } catch (e) { /* old browser */ }
    layout();
    setView(startView, false);
    views.classList.remove('is-entering');
  }
})();
</script>
</section>
				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-62eb6fc 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="62eb6fc" data-element_type="container" data-e-type="container" data-settings="{&quot;background_background&quot;:&quot;classic&quot;}">
		<div class="elementor-element elementor-element-7f78631 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="7f78631" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-ee4d6be elementor-widget elementor-widget-html" data-id="ee4d6be" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					
<!-- enginR · Video-game-theme -->
<iframe
  src="https://pcdlabs.presentcomposedesign.fr/tools/enginR/viewer.html?s=7aa2tspyq232&amp;embed=1"
  title="Video-game-theme - visualiseur 3D / XR"
  style="width:100%;height:560px;border:0;border-radius:14px;background:transparent;"
  allow="xr-spatial-tracking; fullscreen; camera; gyroscope; accelerometer; magnetometer; autoplay"
  allowfullscreen
  loading="lazy"
  referrerpolicy="strict-origin-when-cross-origin"></iframe>
				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-bb6d16b 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="bb6d16b" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-cb45e6d elementor-widget elementor-widget-html" data-id="cb45e6d" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					
<!-- enginR · Démo · gaussian splatting -->
<iframe
  src="https://pcdlabs.presentcomposedesign.fr/tools/enginR/viewer.html?s=demosplat&amp;embed=1"
  title="Démo · gaussian splatting - visualiseur 3D / XR"
  style="width:100%;height:560px;border:0;border-radius:14px;background:#0a0a12;"
  allow="xr-spatial-tracking; fullscreen; camera; gyroscope; accelerometer; magnetometer; autoplay"
  allowfullscreen
  loading="lazy"
  referrerpolicy="strict-origin-when-cross-origin"></iframe>
				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-9c1ce8c 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="9c1ce8c" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-8ec8ffe elementor-widget elementor-widget-html" data-id="8ec8ffe" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					
<!-- enginR · Alice-test -->
<iframe
  src="https://pcdlabs.presentcomposedesign.fr/tools/enginR/viewer.html?s=t2n2eoq7b7wn&amp;embed=1"
  title="Alice-test - visualiseur 3D / XR"
  style="width:100%;height:560px;border:0;border-radius:14px;background:transparent;"
  allow="xr-spatial-tracking; fullscreen; camera; gyroscope; accelerometer; magnetometer; autoplay"
  allowfullscreen
  loading="lazy"
  referrerpolicy="strict-origin-when-cross-origin"></iframe>
				</div>
				</div>
				</div>
		<div class="elementor-element elementor-element-5822496 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="5822496" data-element_type="container" data-e-type="container">
				</div>
		<div class="elementor-element elementor-element-088e2d4 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="088e2d4" data-element_type="container" data-e-type="container">
				</div>
		<div class="elementor-element elementor-element-2560a49 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="2560a49" data-element_type="container" data-e-type="container">
				</div>
				</div>
		<div class="elementor-element elementor-element-720b5ed 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="720b5ed" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-0f34b21 elementor-widget__width-inherit elementor-widget elementor-widget-html" data-id="0f34b21" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					
<!-- enginR · Ma scène test -->
<iframe
  src="https://pcdlabs.presentcomposedesign.fr/tools/enginR/viewer.html?s=f3fndparqrvu&amp;embed=1"
  title="Ma scène test - visualiseur 3D / XR"
  style="width:100%;height:560px;border:0;border-radius:14px;background:#0a0a12;"
  allow="xr-spatial-tracking; fullscreen; camera; gyroscope; accelerometer; magnetometer; autoplay"
  allowfullscreen
  loading="lazy"
  referrerpolicy="strict-origin-when-cross-origin"></iframe>
				</div>
				</div>
				</div>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/labs/">Labs</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Formations</title>
		<link>https://presentcomposedesign.fr/formations/</link>
		
		<dc:creator><![CDATA[admin]]></dc:creator>
		<pubDate>Sat, 14 Mar 2026 12:46:14 +0000</pubDate>
				<category><![CDATA[Design produit]]></category>
		<guid isPermaLink="false">https://presentcomposedesign.fr/?p=35941</guid>

					<description><![CDATA[<p>Formations / Présent Composé Design ✓ Guide débloqué ! ✕🎨 Formation Découverte IA Débloquez le guide Entrez vos coordonnées pour accéder au guide sélectionné. PrénomNomEmailTéléphone (optionnel)Vous êtes· Choisissez ·Étudiant·ePro du design / créatifPro (autre secteur)Entrepreneur·eCurieux·se / DécouverteSecteur d&#8217;activitéCode d&#8217;accès (fourni par le formateur)Accéder au guide → 🔒 Données utilisées uniquement pour cette formation. Formations Présent Composé Design / Alban Desbarax Formations Présent Composé Design / Alban Desbarax Ateliers &#038; Ressources 🎨 TOP 10 Applications IA Génératives 2026 Les meilleures apps gratuites (téléphone &#038; desktop) pour générer des images et vidéos facilement avec l&#8217;IA. Guide interactif avec démos vidéo. GRATUIT · Guide interactif · 11 apps testées 💻 TOP 25 Applications Open Source à installer en 2026 La sélection essentielle des logiciels gratuits &#038; open source pour la création 3D, audio, vidéo, design, impression 3D, code créatif et productivité. GRATUIT · 25 logiciels · 7 catégories · Liens de téléchargement 🎬 Storyboard &#38; virtual production IA TOOLBOX 20 outils IA et 100 termes techniques pour la création de storyboards. Glossaire interactif, prompts prêts à l&#8217;emploi, liens et demos. GRATUIT · 20 outils · 100 termes · 2 onglets ⚡ Claude Vibecoding — Guide complet Maîtriser Claude dans le navigateur et dans VS Code. Workflows, prompts-clés, pièges à éviter, méthodes d&#8217;itération rapide pour créatifs qui codent. GRATUIT · 15 fiches · Navigateur · VS Code · Prompts à copier ← Retour aux formations ★ Incontournable / Assistant IA quotidien ★Mistral AI (Le Chat)Web · iOS · Android Assistant IA français polyvalent : texte, recherche, résumé, analyse. Idéal pour la productivité quotidienne. TexteRechercheAnalyse▶ DemoAccéder → 🖼️ Images / Génération &#038; édition 1ChatGPT + DALL·EWeb · iOS · Android Génération d&#8217;images réalistes via GPT-4o. ~3 images/jour en gratuit. Texte lisible dans les images. ImagesTexte▶ DemoAccéder → 2Google Gemini + Veo 3.1Web · iOS · Android IA multimodale Google. Images (Nano Banana) et vidéos HD avec audio natif via Flow. Gratuit et puissant. ImagesVidéosAudio▶ DemoAccéder → 3Canva IAWeb · iOS · Android Suite de design avec images (Média Magique) et vidéos IA (Veo-3). Intuitive, idéale pour débutants. ImagesVidéosDesign▶ DemoAccéder → 4Adobe FireflyWeb · Desktop Générateur images/vidéos premium d&#8217;Adobe. 25 crédits gratuits/mois. Remplissage génératif et retouche. ImagesVidéosRetouche▶ DemoAccéder → 🎬 Vidéos / Génération &#038; animation 5Luma AI (Dream Machine)Web · iOS · Android Génération vidéo réaliste à partir de texte ou d&#8217;images. Capture 3D photoréaliste avec téléphone. Vidéos3D▶ DemoAccéder → 6Runway (Gen-4)Web · iOS Outil cinématographique. Vidéo à partir de texte, images ou clips. Productions réelles. VidéosCinéma▶ DemoAccéder → 7Leonardo AIWeb · iOS · Android Générateur d&#8217;images avec nombreux modèles. 150 tokens/jour gratuits. Assets visuels. ImagesAssets▶ DemoAccéder → 8IdeogramWeb · iOS · Android Maîtrise du texte dans les images. ~40 images/semaine gratuits. Résultats créatifs. ImagesTypographie▶ DemoAccéder → 9Microsoft CopilotWeb · iOS · Android DALL·E 3 gratuit et illimité via Microsoft. Aucune inscription nécessaire. ImagesIllimité▶ DemoAccéder → 10PikaWeb · iOS Vidéos courtes créatives pour réseaux sociaux. Interface simple. VidéosSocial▶ DemoAccéder → ← Retour aux formations 💻 TOP 25 Applications Open Source &#038; Gratuites · PC 2026 🎮 3D · Moteurs · VR / XR Blender Suite 3D complète : modélisation, animation, rendu, compositing. Le standard open source. 3DAnimationRendu▶ DemoTélécharger ↓ Roblox Studio Créez vos propres jeux et expériences 3D. Plateforme massive de création interactive. Jeux3D▶ DemoTélécharger ↓ Unity / Unreal Engine Les deux moteurs de jeu de référence. Unity (C#) et Unreal (Blueprints/C++). Gratuits pour débuter. Jeux3DXR▶ DemoTélécharger ↓ Babylon.js Moteur 3D web open source par Microsoft. Créez des expériences 3D/WebXR dans le navigateur. Web3DWebXR▶ DemoTélécharger ↓ SideQuest Plateforme de sideloading pour Meta Quest. Accédez à des apps VR non officielles. VRQuest▶ DemoTélécharger ↓ 🎵 Audio · Musique · Son Reaper DAW professionnel ultra-léger. Licence d&#8217;évaluation illimitée. Plugins VST, MIDI, multitrack. DAWAudio▶ DemoTélécharger ↓ Ableton Live Lite DAW référence pour la musique électronique et le live. Version Lite gratuite avec instruments. DAWLive▶ DemoTélécharger ↓ Max MSP Environnement de programmation visuelle pour audio, MIDI, vidéo et médias interactifs. AudioVisuel▶ DemoTélécharger ↓ 🎬 Vidéo · Montage · Compositing DaVinci Resolve Suite pro complète : montage, étalonnage, VFX, mixage audio. Gratuit et très puissant. MontageVFXColor▶ DemoTélécharger ↓ CapCut Montage vidéo simple et efficace. Templates, effets, sous-titres auto. Idéal réseaux sociaux. MontageSocial▶ DemoTélécharger ↓ Natron Compositing node-based open source. Alternative à After Effects/Nuke pour le VFX. VFXCompositing▶ DemoTélécharger ↓ VLC Lecteur multimédia universel. Lit tous les formats, convertit, streame. Incontournable. LecteurConversion▶ DemoTélécharger ↓ 🎨 Design · Image · UI Figma Design d&#8217;interfaces, prototypage, collaboration temps réel. Version gratuite très généreuse. UI/UXPrototypage▶ DemoTélécharger ↓ Affinity Suite créative (Photo, Designer, Publisher). Alternative premium à Adobe, licence perpétuelle. DesignPhoto▶ DemoTélécharger ↓ XnView Visionneuse et convertisseur d&#8217;images. Supporte 500+ formats. Batch processing puissant. ImagesBatch▶ DemoTélécharger ↓ 🖨️ Impression 3D · Fabrication Bambu Studio Slicer officiel Bambu Lab. Profils optimisés, multi-couleur, interface moderne. SlicerFDM▶ DemoTélécharger ↓ Orca Slicer Fork communautaire de Bambu Studio. Compatible toutes imprimantes, très personnalisable. SlicerOpen Source▶ DemoTélécharger ↓ Cura (UltiMaker) Slicer universel le plus utilisé au monde. Marketplace de plugins, profils communautaires. SlicerUniversel▶ DemoTélécharger ↓ ⚡ Code créatif · IA générative TouchDesigner Programmation visuelle temps réel pour installations interactives, VJing, mapping. InteractifTemps réel▶ DemoTélécharger ↓ Processing Langage de programmation créative. Idéal pour apprendre le code par le visuel. CodeArt▶ DemoTélécharger ↓ Arduino Plateforme électronique open source. IDE + cartes pour prototypage interactif. ÉlectroniqueIoT▶ DemoTélécharger ↓ ComfyUI Interface node-based pour Stable Diffusion. Workflows visuels pour la génération d&#8217;images IA. IAStable Diffusion▶ DemoTélécharger ↓ 🔧 Outils · Productivité · Communication Visual Studio Code Éditeur de code le plus populaire. Extensions, Git, terminal intégré, IA (Copilot). CodeIDE▶ DemoTélécharger ↓ OpenOffice Suite bureautique complète et gratuite. Writer, Calc, Impress. Alternative à Microsoft Office. BureauDocs▶ DemoTélécharger ↓ Discord Plateforme de communication. Salons vocaux/texte, partage d&#8217;écran, bots, communautés. ChatCommunauté▶ DemoTélécharger ↓ ← Retour aux formations 🎬 STORYBOARD &#038; VIRTUAL PRODUCTION IA TOOLBOX e.classList.remove(&#8216;active&#8217;));this.classList.add(&#8216;active&#8217;);document.getElementById(&#8216;sb-outils&#8217;).classList.add(&#8216;active&#8217;) »>🛠 Outils (20) e.classList.remove(&#8216;active&#8217;));this.classList.add(&#8216;active&#8217;);document.getElementById(&#8216;sb-glossaire&#8217;).classList.add(&#8216;active&#8217;) »>📖 Glossaire (100) 🖼️ Génération d&#8217;images 1ComfyUIGratuit Interface node-based pour Stable Diffusion et Flux. Workflows visuels puissants pour la génération d&#8217;images IA. Personnalisation totale, ControlNet, IP-Adapter, cohérence entre panels. Workflow visuelStable DiffusionControlNetCohérence▶ DemoAccéder →this.style.outline= »,1000) » data-p= »ControlNet pose + IP-Adapter style: Generate storyboard panel,</p>
<p>Cet article <a href="https://presentcomposedesign.fr/formations/">Formations</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="35941" class="elementor elementor-35941">
				<div class="elementor-element elementor-element-25c13e0 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="25c13e0" data-element_type="container" data-e-type="container">
				<div class="elementor-element elementor-element-4ebd579 elementor-widget__width-inherit elementor-widget elementor-widget-html" data-id="4ebd579" data-element_type="widget" data-e-type="widget" data-widget_type="html.default">
				<div class="elementor-widget-container">
					
<!--
╔═══════════════════════════════════════════════════════════════╗
║ Formations / Présent Composé Design, © 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/                         ║
╚═══════════════════════════════════════════════════════════════╝

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#*++==------===**%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#++-::::::::::::::::::::::-=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+-::::::::::::::::::::::::::::::::=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+::::::::::::::::::::::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@%+:::::::::::::::::=++***+=-::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@#=::::::::::::::::+%@@@@@@@@@@%+-:::::::::::::::::::=#@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@#-::::::::::::::::#@@@@@@@@@@@@@@@@%-:::::::::::::::::::=#@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@%=::::::::::::::::*@@@@@@**==-=+##@@@@@*-:::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@=:::::::::::::::::%@@@@@+::::::::::-*@@@@#-::::::::::::::::::::=@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@#::::::::::::::::::%@@@@=::::::::::::::-%@@@#-:::::::::::::::::::::#@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@=::::::::::::::::::*@@@@=::::::::::::::::-%@@@*::::::::::::::::::::::=@@@@@@@@@@@@@@@
@@@@@@@@@@@@@%-::::::::::::::::::-@@@@=::::::::::::::::::-%@@@-::::::::::::::::::::::-%@@@@@@@@@@@@@
@@@@@@@@@@@@#::::::::::::::::::::*@@@#::::::::::::::::::::=@@@*::::::::::::::::::::::::*@@@@@@@@@@@@
@@@@@@@@@@@#:::::::::::::::::::::%@@@+::::::::::::::::::::-@@@%:::::::::::::::::::::::::*@@@@@@@@@@@
@@@@@@@@@@+::::::::::::::::::::::@@@@-:::::::::::::::::::::%@@@::::::::::::::::::::::::::*@@@@@@@@@@
@@@@@@@@@*:::::::::::::::::::::::@@@@=:::::::::::::::::::::@@@@:::::::::::::::::::::::::::*@@@@@@@@@
@@@@@@@@%::::::::::::::::::::::::@@@@*:::::::::::::::::::::@@@%::::::::::::::::::::::::::::*@@@@@@@@
@@@@@@@%:::::::::::::::::::::::::@@@@%::::::::::::::::::::+@@@*:::::::::::::::::::::::::::::%@@@@@@@
@@@@@@@::::::::::::::::::::::::::@@@@@*::::::::::::::::::-@@@@::::::::::::::::::::::::::::::-@@@@@@@
@@@@@@=::::::::::::::::::::::::::@@@@@@*::::::::::::::::-@@@@+:::::::::::::::::::::::::::::::*@@@@@@
@@@@@%:::::::::::::::::::::::::::@@@@@@@*::::::::::::::-%@@@#:::::::::::::::::::::::::::::::::*@@@@@
@@@@@::::::::::::::::::::::::::::@@@@@@@@@*::::::::::+@@@@@*::::::::::::::::::::::::::::::::::-@@@@@
@@@@*::::::::::::::::::::::::::::@@@#=@@@@@@%#*+=**%@@@@@@=::::::::::::::::::::::::::::::::::::*@@@@
@@@@:::::::::::::::::::::::::::::@@@#::*@@@@@@@@@@@@@@@@*:::::::::::::::::::::::::::::::::::::::@@@@
@@@*:::::::::::::::::::::::::::::@@@#::::=*@@@@@@@@@%#=:::::::::::::::::::::::::::::::::::::::::#@@@
@@@=:::::::::::::::::::::::::::::@@@#::::::::-==+=-:::::::::::::::::::::::::::::::::::::::::::::=@@@
@@@::::::::::::::::::::::::::::::@@@#:::::::::::::::::::::::::::::::::::::::::::::::::--:::::::::@@@
@@*::::::::::::::::::::::::::::::@@@#:::::::::+######*:::::::::::::::::::::::::::::::+@=:::::::::#@@
@@*::::::::::::::::::::::::::::::@@@#:::--::::===+*+==:::::=-:::::::=-::::::-::::::::+@=:::::::::+@@
@@:::::::::::::::::::::::::::::::@@@#::+@#@@::-#@@%@%=::=@@@@+:::+@@@@@+-::@@@@%*-::#@@%#:::::::::@@
@@:::::::::::::::::+*##%%#*=-::::@@@#::+@@=::-%@:::-*@=:%@-:=:::*@*:::-@*::@@-:=@%::=*@*=:::::::::@@
@@::::::::::::::+%@@@@@@@@@@@%*-:@@@#::+@*:::*@@@@@@@@#:+@%*-:::@@@@@@@@@-:@%:::#@:::+@=::::::::::@@
@%::::::::::::*@@@@@@@@@@@@@@@@@#+@@#::+@+:::+@=-------:::=%@*::@%-------::@%:::#@:::+@=::::::::::%@
@%::::::::::-@@@@@@++-::::-=#@@@@@+*#::+@+:::-@%=-:+%%-:-=::#@::*@*-::+%=::@%:::#@:::+@=::::::::::#@
@%:::::::::=@@@@%+::::::::::::#@@@@**::+@+::::-#@@@@%=::#@%%@+:::+@@@@@*:::@%:::#@:::+@=::::::::::%@
@%::::::::-@@@@+:::::::::::::::-@@@@+:::-::::::::--:::::::=-:::::::-=-:::::--:::-=----=-::::::::::%@
@@:::::::-@@@@*:::::::::::::::::-%%%*:::::::::::::::::::::::::::::::::::::::::::+%%%%%%#::::::::::@@
@@:::::::*@@@#:::::::+#@%*-::::::=+*#*=+#*=-:::::+#%%#-::::::=#%%#=::::-#@@#-::::=##%#+-::::::::::@@
@@-::::::@@@@-:::::-%@#+*%@*:::::%@#+#@@+*@*::::%@+==#@*::::%@#+=%@*-::%@-=*=:::*@*+=+@%-::::::::-@@
@@+:::::-@@@@::::::#@=::::#@=::::%@::=@*::#@-::*@=::::-@+::*@*::::*@*::*@*-::::-@@####%@#::::::::*@@
@@#:::::-@@@*::::::@@:::::=@*::::%@::=@*::#@-::%@::::::@*::%@-::::-@%:::=%@%-::*@#******+::::::::*@@
@@@-::::-@@@%::::::#@+::::%@-::::%@::=@*::#@-::@@-::::=@-::*@*::::*@*:::-::@%::=@*::::-=-::::::::@@@
@@@+:::::@@@@-::::::%@%*#@@+:::::%@::=@*::#@-::@@@#=+#@+::::*@%*+@@*:::#@*+@*:::*@%*+%@%::::::::-@@@
@@@#:::::#@@@+:::::::=*##+-::::::+*::-*=::+*:::@%=*%#*-::::::=*%#*-:::::=*#+:::::-*##*=--=::::::*@@@
@@@@-::::=@@@@-::::::::::::::::::=**+::::::::::@%:::::::::::::::::::::::-==++**##%%@@@@@@@-::::-@@@@
@@@@*:::::*@@@@-::::::::::::::::+@@@#::::::::::::::::::::-==++**##=-#@@@@@@@@@@@@@@@@@@@@@=::::*@@@@
@@@@@-:::::#@@@@+-:::::::::::::*@@@@::::::-==++--##%@@@@@@@@@#*#@@%%@@@@@@@@@@@@@@@@@@@@@+:::-@@@@@
@@@@@%::::::*@@@@@+-:::::::::+@@@@%:=@@@@@@@@@@==@@@**%@@@@*-#@*-@@*@@@*-+**@@@:%@@==@++@@*:::%@@@@@
@@@@@@*::::::=@@@@@@@%%**##@@@@@@=::=@@@@#-:--=-=@@=+@+=@@@:%@@@=+@-*@*-@@@+:@@=#@:*@@%-@@#::+@@@@@@
@@@@@@@-:::::::+%@@@@@@@@@@@@@@#-::::@@@+-%@@@*--@@:@@=*@@@%=*%@@@@*=@-*@@@*:+@=+=#@@@%:@@%:-@@@@@@@
@@@@@@@@-::::::::=*%@@@@@@@##-:::::::@@@:#@@@@@*:@@:%==@@@@@@%+-%@@#-@*+@@@:+:@*:*@@@@@:%@@-@@@@@@@@
@@@@@@@@#-:::::::::::::-:::::::::::::@@%:%@@@@@*-@@-#@@@#-@*%@@@-#@@:@@#=+:%@-#@:@@@@@@*=@@%@@@@@@@@
@@@@@@@@@*:::::::::::::::::::::::::::#@@--@@@@%:*@@%*+*=*@@=+@@@=+@@-#@@@@@@@=#@#@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@*-:::::::::::::::::::::::::#@@@+-==-:#@@@@@@@@@@@@#++*#@@@@@@@#@@@%:@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@*-::::::::::::::::::::::::+@@@@@%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@-=-:+**++==--::%@@@@@@@@@@@
@@@@@@@@@@@@#-:::::::::::::::::::::::=@@@@@@@@@@@@@@@@@@@@@%%##**++=---::::::::::::::::%@@@@@@@@@@@@
@@@@@@@@@@@@@@-::::::::::::::::::::::-@@@@@@%###**++=--::::::::::::::::::::::::::::::-%@@@@@@@@@@@@@
@@@@@@@@@@@@@@@+::::::::::::::::::::::--::::::::::::::::::::::::::::::::::::::::::::+@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@%-::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::-%@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@+-:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::+@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@%=::::::::::::::::::::::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@%=::::::::::::::::::::::::::::::::::::::::::::::::::::::=%@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@%=-:::::::::::::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@*-:::::::::::::::::::::::::::::::::::::::::::::#@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@%+-:::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%*=-:::::::::::::::::::::::::::::::+#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%**=-:::::::::::::::::::::=+#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%**++=====+++*##@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#****+++++****##%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@**+-::::::::::::::::::::==*#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*+-::::::::::::::::::::::::::::::==#%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+-::::::::::::::::::::::::::::::::::::::=*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@*-::::::::::::::::::::::::::::::::::::::::::::=*%@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@#+:::::::::::::::++****%%@@@@@%%##**=--:::::::::::::=#@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@*=:::::::::::::+*%@@@@@@@@@@@@@@@@@@@@@@@#*=-:::::::::::=#@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@#=:::::::::::+*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#=-::::::::::=#@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@%=::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*=::::::::::=%@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@*::::::::::*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#+-:::::::::*%@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@%=::::::::=*@@@@@@@@@@@@@@%#*********#%%@@@@@@@@@@@@@@@@@@@%+-::::::::=%@@@@@@@@@@@@@@
@@@@@@@@@@@@@#:::::::::#@@@@@@@@@@@**+-::::::::::::::::-+#%@%@@@@@@@@@@@@@@%+-::::::::*@@@@@@@@@@@@@
@@@@@@@@@@@@+::::::::+@@@@@@@@@@*-::::::::::::::::::::::#@@@##%%@@@@@@@@@@@@@@+::::::::*%@@@@@@@@@@@
@@@@@@@@@@@=:::::::=#@@@@@@@@@+::::::::::::::::::::::::-*@@@@@@#%%@@@@@@@@@@@@@*-:::::::=%@@@@@@@@@@
@@@@@@@@@@=:::::::*@@@@@@@@@%=::::::::::::::::::::::::::*@@@@@@@@##%@@@@@@@@@@@@%+:::::::=%@@@@@@@@@
@@@@@@@@%=:::::::#@@@@@@@@@%:::::::::::::::::::::::::::+@@@@@@@@@@@%%@@@@@@@@@@@@@*-::::::-%@@@@@@@@
@@@@@@@@=::::::-#@@@@@@@@@%-:::::::::::::::::::::::::==#@@@@@@@@@@@@%#@@@@@@@@@@@@@#-::::::=%@@@@@@@
@@@@@@@=::::::-%@@@@@@@@@@=:::::::::::::::::::::::::*=#%@@@@@@@@@@@@@%#@@@@@@@@@@@@@#-::::::*@@@@@@@
@@@@@@*::::::-#@@@@@@@@@@*::::::::::::::::::::::::*=#+%@@@%@@@@@@@@@@@%%@@@@@@@@@@@@@%-::::::*@@@@@@
@@@@@%:::::::#@@@@@@@@@@%:::::::::::::::::::---:::-:-+*@%%@@@@@@@@@@@@@*%@@@@@@@@@@@@@%-::::::%@@@@@
@@@@@-::::::#@@@@@@@@@@@-::::::::::::::::::::+=#-=#*#=+%%@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@#-:::::-%@@@@
@@@@*::::::*@@@@@@@@@@@#:::::::::::::::::::::-++=#==#+-%%%@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@*::::::*@@@@
@@@%::::::-@@@@@@@@@@@@=::::::::::::::::::::::::+=++#%=%*@@@@@@@@@@@@@@@@*@@@@@@@@@@@@@@@-::::::%@@@
@@@*::::::#@@@@@@@@@@@%:::::::::::::::::::::::::+#:+@%+%@@@@@@@@@@@@@@@@@@#@@@@@@@@@@@@@@%-:::::*@@@
@@%::::::=@@@@@@@@@@@@*::::::::::::::::::::::::-#*-#+=@@%@%@@@@@@@@@@@@@@@#@@@@@@@@@@@@@@@*::::::%@@
@@*::::::%@@@@@@@@@@@@=:::::::::::::::::::::::+=#@++=+%@@@@@@@@@@@@@@#@@@+-%@@@@@@@@@@@@@@@-:::::*@@
@@::::::*@@@@@@@@@@@@@-:::::::::::::::::::::::-**+@+*=%@%@@@@@@@@@@@#@@%=::*@@@@@@@@@@@@@@@*::::::%@
@%::::::#@@@@@@@@@@@@@::::::::::::::::::::::::::==#@%+@%+*%%@@@@@@@#==+::=-#@@@@@@@@@@@@@@@#::::::*@
@*:::::-@@@@@@@@@@@@@@::::::::::::::::::::::::::::***%-@*@%%@@@@@@@%%=+*%%%@@@@@@@@@@@@@@@@@-:::::+@
@=:::::*@@@@@@@@@@@@@@-::::::::::::::::::::::::::-+@+=+*%*%@@@@@@@%@@@%*=:*@@@@@@@@@@@@@@@@@+:::::=@
%::::::*@@@@@@@@@@@@@@*::::::::::::::::::::::-=-=+#**#=%+@@*@@@@@@@@@@@+=**%@@@@@@@@@@@@@@@@#::::::%
%::::::%@@@@@@@@@@@@@@#:::::::::::::::::::+#@++@%@@#*+*+#=@@@@@@@@@@@@@@*%@#%@@@@@@@@@@@@@@@#::::::#
*::::::@@@@@@@@@@@@@@@@-:::::::::::::::::*@@@:::%@@@@*---**@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@%::::::*
*::::::@@@@@@@@@@@@@@@@+:::::::::::::::::@@@@%::*@@@@@+-:=%+@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@::::::*
*:::::-@@@@@@@@@@@@@@@@#:::::::::::::::::%=%@@-:+@@@@@*::%*@@@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@-:::::*
*:::::=@@@@@@@@@@@@@@@@@+::::::::::::::::*:-@*::*-*@@@@-:-=%@%@@@@@@@@@@@@@@@@@#@@@@@@@@@@@@@::::::*
*:::::-@@@@@@@@@@@@@@@@@%-:::::::::::::::*+:%@@:::-@@@@-:=+%%@%@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@::::::*
*::::::%@@@@@@@@@@@@@@@@@*::::::::::::::::*=-@*::::%@@@+:+*@@@%@@@@@@@@@@@%@@%#@@@@@@@@@@@@@%::::::*
#::::::%@@@@@@@@@@@@@@@@@%-::::::::::::::::#+**:::::%@@*:-=%@*@@@@@@@@@@@#**%@@@@@@@@@@@@@@@%::::::#
@::::::*@@@@@@@@@@@@@@@@@@*::::::::::::::::-%@@@@@**@@%=::=+@@@@@%@@@@@@@@@%@@@@@@@@@@@@@@@@*::::::@
@-:::::*@@@@@@@@@@@@@@@@@@@=:::::::::::::::::#@@@@@@@@%:::+=%@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@*:::::-@
@+:::::=@@@@@@@@@@@@@@@@@@@%-:::::::::::::::::*%@@@*%@-:::-*=*%@@@@@@@@@@@@@%@@@@@@@@@@@@@@@::::::*@
@*::::::%@@@@@@@@@@@@@@@@@@@*:::::::::::::::::::=%%+#@=:::=*++@@@@@@@@@@@@@#@@@@@@@@@@@@@@@%::::::*@
@@-:::::*@@@@@@@@@@@@@@@@@@@#::::::::::::::::----:=###::::=**%%@@@@@@@@@@@+%@@@@@@@@@@@@@@@+:::::-@@
@@+:::::-%@@@@@@@@@@@@@@@@@@@-:::::::::::::=@@@@@+::::::::-*#%%%@@@@@@@@@%*%@@@@@@@@@@@@@@%::::::*@@
@@#::::::*@@@@@@@@@@@@@@@@@@@+::::::::::::::%@@@@@-:::::::+%@%%%@@@@@@@@@%@@@@@@@@@@@@@@@@+::::::%@@
@@@+::::::%@@@@@@@@@@@@@@@@@@*::::::::::::::*@@@@@-:::::::-+#@%%@%@@@@@@@%@@@@@@@@@@@@@@@%::::::=@@@
@@@%-:::::*@@@@@@@@@@@@@@@@@@*::::::::::::::#@@@@@::::::::::*@%**%@@@@@@@#@@@@@@@@@@@@@@@=::::::%@@@
@@@@*::::::%@@@@@@@@@@@@@@@@@*::::::::::::::*@@@@@#-::::::::===+-%%@@@@@@%%@@@@@@@@@@@@@*::::::*@@@@
@@@@@-:::::-%@@@@@@@@@@@@@@@@+::::::::::::::=#%@@@@*--#=::::::--:=@@%%%@@%@@@@@@@@@@@@@#::::::-@@@@@
@@@@@#-:::::-%@@@@@@@@@@@@@@@=::::::::::::::==@%@%%%#+%%*:::::::-=#%%*%@#*@@@@@@@@@@@@%:::::::#@@@@@
@@@@@@*::::::*%@@@@@@@@@@**+-:::::::::::::::@#@@%@@@%#%@*::::-::::==::%%*@@@@@@@@@@@@%=::::::*@@@@@@
@@@@@@@+::::::*@@@@@@@@*::::::::::::::::::-##%%@#@@@@%%%*-==*%:-::==+**@@@@@@@@@@@@@%=::::::=@@@@@@@
@@@@@@@@-::::::*%@@@@*::::::::::::::::::::::%%%%%@@@@@@@#%@@%%@@@@@@@@@@@@@@@@@@@@@%=::::::=@@@@@@@@
@@@@@@@@%-::::::=%@@%:::::::::::::::::::::#@%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#:::::::-@@@@@@@@@
@@@@@@@@@@-::::::-%@#::::::::::::::::::::+@@@@@%@@@@@@@@@@@%%@@@@@@@@@@@@@@@@@@@@*:::::::=@@@@@@@@@@
@@@@@@@@@@@-:::::::*@-:::::::::::::::::::-%@@@@@@@@@@@@@@@@*@@@@@@@@@@@@@@@@@@@@=:::::::=@@@@@@@@@@@
@@@@@@@@@@@@+:::::::==:::::::::::::::::::-%@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@#-:::::::+@@@@@@@@@@@@
@@@@@@@@@@@@@*-:::::::::::::::::::::::::::=@@@@@@@@@@@@@@%#@@@@@@@@@@@@@@@@@%=::::::::#@@@@@@@@@@@@@
@@@@@@@@@@@@@@#-:::::::::::::::::::::::::::*@@@@@@@@@@@@#@@@@@@@@@@@@@@@@@@+::::::::=#@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@*-::::::::::::::::::::::::::*%@@@@@@@@@@*@@@@@@@@@@@@@@@@+:::::::::*@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@%=:::::::::::::::::::::::::::*%@@@@@@@@#@@@@@@@@@@@@@*=:::::::::=%@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@#-:::::::::::::::::::::::::::*%@@@@@@@%#@@@@@@@@*+-:::::::::=#@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@#-:::::::::::::::::::::::::::*%@@@@@@@%@@@@*+:::::::::::=#@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@#=-::::::::::::::::::::::::::+%@@@@%**--::::::::::::+#@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@%*-:::::::::::::::::::::::::::::::::::::::::::::*%@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@#*-:::::::::::::::::::::::::::::::::::::::+%@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@##=-:::::::::::::::::::::::::::::::+*%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@##*=-::::::::::::::::::::-+**%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%##****+++++****%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
-->
<!DOCTYPE html><html lang="fr"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Formations / Présent Composé Design</title><link rel="icon" href="https://presentcomposedesign.fr/wp-content/uploads/2024/03/cropped-logo_PCd_2024-32x32.png" sizes="32x32"><link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet"><script type="module" src="https://unpkg.com/@splinetool/viewer@1.12.69/build/spline-viewer.js"></script>
<style>
:root{--pcd:#1001EF;--pcd-dk:#0C01B8;--pcd-lt:#E8E5FF;--pcd-glow:rgba(16,1,239,.15);--pcd-or:#FF6B35;--pcd-or-lt:#FFF3ED;--pcd-or-bd:#FFD0B5;--pcd-bg:#F4F3FA;--pcd-w:#FFF;--pcd-tx:#1A1A2E;--pcd-txm:#4A4A66;--pcd-txl:#8888AA;--pcd-bd:#DDDDE8;--pcd-ok:#10B981;--pcd-r:14px}
#pcd,#pcd *,#pcd *::before,#pcd *::after{margin:0;padding:0;box-sizing:border-box;border:none;outline:none;text-decoration:none;font-style:normal;list-style:none;background:none;letter-spacing:normal;text-transform:none;text-shadow:none;box-shadow:none;float:none;max-width:none;min-height:0}
#pcd{font-family:'DM Sans',sans-serif!important;background:var(--pcd-bg)!important;color:var(--pcd-tx)!important;line-height:1.6!important;font-size:16px!important;text-align:left!important;min-height:100vh;overflow-x:hidden}
#pcd .H{position:relative;overflow:hidden;min-height:480px}
#pcd .H spline-viewer{position:absolute;inset:0;width:100%;height:100%;z-index:0;pointer-events:auto}
#pcd .H::after{content:'';position:absolute;bottom:0;left:0;right:0;height:4px;background:linear-gradient(90deg,var(--pcd-or),#FFD700,var(--pcd-or));z-index:3}
#pcd .Hi{max-width:900px;margin:0 auto;padding:24px 32px;display:flex;align-items:center;justify-content:space-between;gap:24px;position:relative;z-index:2}
#pcd .Hl{display:flex;align-items:center;gap:14px}
/* Logos: hidden on screen by default, shown on print */
#pcd .Lp{width:64px;height:64px;flex-shrink:0;border-radius:10px;overflow:hidden;box-shadow:0 2px 10px rgba(0,0,0,.3);background:#fff;display:none}
#pcd .Lp.show-screen{display:block}
#pcd .Lp img{width:100%;height:100%;object-fit:contain;display:block}
#pcd .Ht h1{color:#FFF!important;font-size:34px!important;font-weight:700!important;line-height:1.2!important;margin:0!important;text-shadow:0 2px 16px rgba(0,0,0,.4)}
#pcd .Ht p{color:rgba(255,255,255,.8)!important;font-size:14px!important;margin:6px 0 0!important;text-shadow:0 1px 8px rgba(0,0,0,.4)}
#pcd .Ll{width:72px;height:72px;background:rgba(255,255,255,.15);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.2)!important;border-radius:12px;display:none;align-items:center;justify-content:center;flex-shrink:0;overflow:hidden}
#pcd .Ll.show-screen{display:flex}
#pcd .Ll img{width:100%;height:100%;object-fit:contain;padding:5px;display:block}
#pcd .land{max-width:900px;margin:0 auto;padding:40px 24px}
#pcd .land-title{font-size:14px!important;font-weight:700!important;text-transform:uppercase;letter-spacing:1.5px;color:var(--pcd)!important;margin-bottom:20px}
#pcd .fcard{background:var(--pcd-w);border:1.5px solid var(--pcd-bd)!important;border-radius:var(--pcd-r);padding:28px 70px 28px 24px;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s;position:relative;overflow:hidden}
#pcd .fcard:hover{transform:translateY(-3px);box-shadow:0 12px 35px rgba(16,1,239,.1)!important;border-color:var(--pcd)!important}
#pcd .fcard::before{content:'';position:absolute;top:0;left:0;right:0;height:4px;background:linear-gradient(90deg,var(--pcd),var(--pcd-or))}
#pcd .fcard h2{font-size:22px!important;font-weight:700!important;color:var(--pcd-tx)!important;margin-bottom:6px!important}
#pcd .fcard .fdesc{font-size:14.5px!important;color:var(--pcd-txm)!important;line-height:1.5!important;margin-bottom:12px!important}
#pcd .fcard .fbadge{display:inline-block;background:var(--pcd-lt);color:var(--pcd)!important;font-size:11px!important;font-weight:600!important;padding:4px 12px;border-radius:6px}
#pcd .fcard .farrow{position:absolute;right:24px;top:50%;transform:translateY(-50%);width:32px;height:32px;background:var(--pcd)!important;border-radius:50%;display:flex;align-items:center;justify-content:center}
#pcd .fcard .farrow svg{width:14px;height:14px;fill:none;stroke:#fff;stroke-width:2.5}
#pcd .fo{position:fixed;inset:0;background:rgba(10,10,26,.88);backdrop-filter:blur(14px);z-index:99999;display:none;align-items:center;justify-content:center;padding:20px;animation:pF .4s}
#pcd .fo.on{display:flex}
@keyframes pF{from{opacity:0}to{opacity:1}}
#pcd .fc{background:#fff;border-radius:22px;padding:36px 32px;max-width:440px;width:100%;box-shadow:0 30px 80px rgba(0,0,0,.5);animation:pU .5s;position:relative;overflow:hidden}
#pcd .fc::before{content:'';position:absolute;top:0;left:0;right:0;height:4px;background:linear-gradient(90deg,var(--pcd),var(--pcd-or))}
@keyframes pU{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}
#pcd .fc h2{font-size:20px!important;font-weight:700!important;color:#0A0A1A!important;margin:4px 0!important}
#pcd .fs{color:var(--pcd-txm)!important;font-size:13px!important;margin-bottom:20px!important;line-height:1.5!important}
#pcd .fs strong{color:var(--pcd)!important}
#pcd .fg{margin-bottom:12px}
#pcd .fg label{display:block;font-size:11px!important;font-weight:600!important;color:var(--pcd-txm)!important;margin-bottom:3px!important;text-transform:uppercase;letter-spacing:.5px}
#pcd .fg label .op{font-weight:400!important;text-transform:none;color:var(--pcd-txl)!important;font-size:10px!important}
#pcd .fg input,#pcd .fg select{width:100%;padding:10px 12px;border:1.5px solid var(--pcd-bd)!important;border-radius:8px;font-family:'DM Sans',sans-serif!important;font-size:13.5px!important;color:var(--pcd-tx)!important;background:#FAFAFF!important;height:auto;-webkit-appearance:none}
#pcd .fg select{-webkit-appearance:menulist;appearance:menulist}
#pcd .fg input:focus,#pcd .fg select:focus{border-color:var(--pcd)!important;box-shadow:0 0 0 3px var(--pcd-glow)!important}
#pcd .fg input::placeholder{color:#BBB!important}
#pcd .fr{display:flex;gap:10px}
#pcd .fr .fg{flex:1}
#pcd .sf{max-height:0;overflow:hidden;transition:max-height .3s,opacity .3s;opacity:0}
#pcd .sf.v{max-height:70px;opacity:1}
#pcd .fb{display:inline-flex;align-items:center;gap:5px;background:var(--pcd-lt);border:1px solid rgba(16,1,239,.15)!important;border-radius:7px;padding:6px 12px;margin-bottom:16px;font-size:11px!important;color:var(--pcd)!important;font-weight:600!important}
#pcd .btn{width:100%;padding:13px;border:none!important;border-radius:9px;background:var(--pcd)!important;color:#fff!important;font-family:'DM Sans',sans-serif!important;font-size:14px!important;font-weight:600!important;cursor:pointer;margin-top:4px;display:block;text-align:center}
#pcd .btn:hover{transform:translateY(-1px);box-shadow:0 6px 24px rgba(16,1,239,.35)!important;background:var(--pcd-dk)!important}
#pcd .fn{text-align:center;font-size:10px!important;color:var(--pcd-txl)!important;margin-top:12px!important;line-height:1.5!important}
#pcd .fcl{position:absolute;top:14px;right:14px;width:28px;height:28px;border-radius:50%;background:rgba(0,0,0,.06)!important;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px!important;color:var(--pcd-txl)!important}
#pcd .guide{max-width:900px;margin:0 auto;padding:0 24px 40px;display:none}
#pcd .guide.on{display:block}
#pcd .sl{display:flex;align-items:center;gap:8px;padding:22px 0 10px;font-size:11px!important;font-weight:700!important;text-transform:uppercase;letter-spacing:1.2px;color:var(--pcd-or)!important}
#pcd .sl::after{content:'';flex:1;height:1.5px;background:linear-gradient(90deg,var(--pcd-or-bd),transparent)}
#pcd .st{display:flex;align-items:center;gap:8px;padding:24px 0 12px;font-size:11px!important;font-weight:700!important;text-transform:uppercase;letter-spacing:1.2px;color:var(--pcd)!important}
#pcd .st::after{content:'';flex:1;height:2px;background:linear-gradient(90deg,var(--pcd),transparent)}
#pcd .ac{background:var(--pcd-w);border:1px solid var(--pcd-bd)!important;border-radius:var(--pcd-r);padding:16px 18px;margin-bottom:8px;display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;transition:all .25s;position:relative}
#pcd .ac:hover{box-shadow:0 8px 28px rgba(16,1,239,.08)!important;border-color:rgba(16,1,239,.25)!important}
#pcd .ac.mc{background:var(--pcd-or-lt);border:1.5px solid var(--pcd-or-bd)!important}
#pcd .ac.mc:hover{border-color:var(--pcd-or)!important;box-shadow:0 8px 25px rgba(255,107,53,.12)!important}
#pcd .an{width:18px;flex-shrink:0;font-family:'Space Mono',monospace!important;font-size:11px!important;font-weight:700!important;color:var(--pcd-txl)!important;text-align:center;padding-top:10px}
#pcd .mc .an{color:var(--pcd-or)!important;font-size:14px!important}
#pcd .ai{width:44px;height:44px;border-radius:10px;flex-shrink:0;overflow:hidden;background:#F0F0F5;box-shadow:0 2px 6px rgba(0,0,0,.08);display:flex;align-items:center;justify-content:center}
#pcd .ai img{width:30px;height:30px;object-fit:contain;display:block}
#pcd .ab{flex:1;min-width:0}
#pcd .ah{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap;margin-bottom:2px}
#pcd .nm{font-size:15px!important;font-weight:700!important;color:var(--pcd-tx)!important}
#pcd .mc .nm{color:#B83800!important}
#pcd .pl{font-size:9.5px!important;color:var(--pcd-txl)!important;background:rgba(0,0,0,.04)!important;padding:1px 6px;border-radius:3px}
#pcd .ad{font-size:13.5px!important;color:var(--pcd-txm)!important;line-height:1.45!important;margin-bottom:6px!important}
#pcd .af{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:6px}
#pcd .at{display:flex;gap:4px;flex-wrap:wrap}
#pcd .tg{font-size:10.5px!important;font-weight:600!important;padding:2px 8px;border-radius:4px;display:inline-block}
#pcd .ctas{display:flex;gap:6px;align-items:center}
#pcd .cta-d,#pcd .cta-a{font-size:10.5px!important;font-weight:600!important;padding:4px 10px;border-radius:6px;cursor:pointer;display:inline-flex;align-items:center;gap:3px;transition:all .15s;text-decoration:none!important}
#pcd .cta-d{background:var(--pcd-lt)!important;color:var(--pcd)!important;border:1px solid rgba(16,1,239,.15)!important}
#pcd .cta-d:hover{background:var(--pcd)!important;color:#fff!important}
#pcd .cta-a{background:var(--pcd)!important;color:#fff!important}
#pcd .cta-a:hover{background:var(--pcd-dk)!important}
#pcd .mc .cta-d{background:var(--pcd-or-lt)!important;color:#CC4400!important;border:1px solid var(--pcd-or-bd)!important}
#pcd .mc .cta-d:hover{background:var(--pcd-or)!important;color:#fff!important}
#pcd .mc .cta-a{background:var(--pcd-or)!important}
#pcd .mc .cta-a:hover{background:#CC4400!important}
/* Video: full width inside card, smooth expand */

#pcd .vp{width:100%;flex-basis:100%;max-height:0;overflow:hidden;border-radius:10px;transition:max-height .4s ease,margin .4s ease;margin:0}
#pcd .vp.open{max-height:280px;margin-top:10px}
#pcd .vp iframe{width:100%;height:260px;display:block;border:none!important;border-radius:10px}

#pcd .sb-tabs{display:flex;gap:0;margin:0 0 20px;border-radius:12px;overflow:hidden;border:2px solid #1001EF}
#pcd .sb-tab{flex:1;padding:14px 20px;text-align:center;font-size:15px!important;font-weight:700!important;cursor:pointer;transition:all .2s;background:#fff;color:#1001EF}
#pcd .sb-tab.active{background:#1001EF;color:#fff}
#pcd .sb-tab:hover:not(.active){background:#f0f0ff}
#pcd .sb-panel{display:none}
#pcd .sb-panel.active{display:block}
#pcd .T{position:fixed;top:20px;right:20px;background:var(--pcd-ok)!important;color:#fff!important;padding:12px 20px;border-radius:10px;font-weight:600!important;font-size:13px!important;box-shadow:0 8px 30px rgba(16,185,129,.4)!important;z-index:100000;display:none}
#pcd .T.on{display:block;animation:pSI .4s}
@keyframes pSI{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:translateX(0)}}
#pcd .AP{max-width:900px;margin:30px auto 0;padding:0 24px 40px;display:none}
#pcd .AP.on{display:block}
#pcd .AC{background:#fff;border:1px solid var(--pcd-bd)!important;border-radius:var(--pcd-r);padding:20px}
#pcd .AC h3{font-size:13px!important;font-weight:700!important;color:var(--pcd)!important;margin-bottom:2px!important}
#pcd .AC .as{font-size:11px!important;color:var(--pcd-txl)!important;margin-bottom:14px!important}
#pcd .tb{width:100%;border-collapse:collapse;font-size:11px!important}
#pcd .tb th{text-align:left;padding:6px;border-bottom:2px solid var(--pcd-bd)!important;font-weight:600!important;color:var(--pcd-txm)!important;font-size:9px!important;text-transform:uppercase;background:none!important}
#pcd .tb td{padding:7px 6px;border-bottom:1px solid #F0F0F5!important;color:var(--pcd-tx)!important;background:none!important}
#pcd .tb tr:last-child td{border-bottom:none!important}
#pcd .bx{margin-top:12px;padding:8px 16px;border-radius:7px;font-family:'DM Sans',sans-serif!important;font-size:11px!important;font-weight:600!important;cursor:pointer;display:inline-block}
#pcd .bx-b{border:1.5px solid var(--pcd)!important;background:#fff!important;color:var(--pcd)!important;margin-right:6px}
#pcd .bx-b:hover{background:var(--pcd)!important;color:#fff!important}
#pcd .bx-g{border:1.5px solid #555!important;background:#fff!important;color:#555!important;margin-right:6px}
#pcd .bx-g:hover{background:#555!important;color:#fff!important}
#pcd .bx-r{border:1.5px solid #E55!important;background:#fff!important;color:#E55!important}
#pcd .bx-r:hover{background:#E55!important;color:#fff!important}
#pcd .cb{display:inline-block;background:var(--pcd-lt);color:var(--pcd)!important;font-size:10px!important;font-weight:700!important;padding:2px 8px;border-radius:20px;margin-left:5px}



@keyframes pSH{0%,100%{transform:translateX(0)}20%{transform:translateX(-6px)}40%{transform:translateX(6px)}60%{transform:translateX(-4px)}80%{transform:translateX(4px)}}
@media(max-width:1024px){
  #pcd .H{min-height:320px}
  #pcd .Ht h1{font-size:26px!important}
  #pcd .Ht p{font-size:12px!important}
}
@media(max-width:600px){
  #pcd .H{min-height:220px}
  #pcd .Hi{flex-wrap:wrap;padding:16px;gap:12px}
  #pcd .Ht h1{font-size:20px!important}
  #pcd .Ht p{font-size:11px!important}
  #pcd .ac{padding:12px 10px;gap:8px}
  #pcd .an{display:none}
  #pcd .fc{padding:24px 18px}
  #pcd .fr{flex-direction:column;gap:0}
  #pcd .fcard .farrow{display:none}
#pcd .vp.open{max-height:200px}
#pcd .vp iframe{height:180px}
}

#pcd .cat{display:flex;align-items:center;gap:8px;padding:18px 0 8px;font-size:11px!important;font-weight:700!important;text-transform:uppercase;letter-spacing:1px;color:var(--pcd)!important}
#pcd .cat::after{content:'';flex:1;height:1.5px;background:linear-gradient(90deg,var(--pcd-bd),transparent)}
#pcd .cat .cat-icon{font-size:14px;font-style:normal!important}
#pcd .guide-back{display:inline-flex;align-items:center;gap:4px;font-size:12px!important;font-weight:600!important;color:var(--pcd)!important;cursor:pointer;padding:16px 0 8px;text-decoration:none!important}
#pcd .guide-back:hover{color:var(--pcd-dk)!important}

#pcd .coffee{display:inline-flex;align-items:center;gap:6px;background:linear-gradient(135deg,#FF813F,#FF6B35);color:#fff!important;font-size:12px!important;font-weight:600!important;padding:8px 16px;border-radius:8px;text-decoration:none!important;margin-top:12px;box-shadow:0 4px 12px rgba(255,107,53,.3)}
#pcd .pc-cta{display:inline-flex;align-items:center;gap:4px;background:#00f7ad;color:#001028!important;border:none!important;padding:6px 14px;font-size:9px!important;font-weight:700!important;border-radius:6px;text-decoration:none!important}
#pcd .pc-thx{opacity:.8;font-size:7.5px!important;color:rgba(255,255,255,.7)!important}

#pcd .coffee:hover{transform:translateY(-1px);box-shadow:0 6px 20px rgba(255,107,53,.4)!important}


/* ── PRINT STYLES ── */

#pcd.spline-full{position:relative}
#pcd.spline-full>.H{position:fixed;inset:0;min-height:100vh!important;z-index:0}
#pcd.spline-full .land,#pcd.spline-full .guide,#pcd.spline-full .AP{position:relative;z-index:1}
#pcd.spline-full .fcard,#pcd.spline-full .ac,#pcd.spline-full .AC,#pcd.spline-full .fc{background:rgba(255,255,255,.92)!important;backdrop-filter:blur(12px)!important;-webkit-backdrop-filter:blur(12px)!important}
#pcd.spline-full .fo{z-index:99999}
#pcd .pc-li{width:28px!important;height:28px!important;border-radius:50%!important;display:inline-flex!important;align-items:center!important;justify-content:center!important;font-weight:700!important;font-size:12px!important;font-family:serif!important;padding:0!important;text-align:center!important}
#pcd .print-contact{display:none}

#pcd .print-hdr{display:none}

#pcd .print-footer{display:none}
@media print{
  @page{size:A4;margin:0;@bottom-center{content:counter(page)" / "counter(pages);font-family:"DM Sans",sans-serif;font-size:7pt;color:#bbb}}
  *{-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
  html,body{overflow:visible!important;width:100%!important;height:auto!important}
  ::-webkit-scrollbar{display:none!important}
  header:not(.H),footer:not(.print-footer),nav,.site-header,.site-footer,.site-navigation,.wp-block-navigation,.menu-toggle,#header-menu-toggle,.entry-footer,.post-navigation,.sidebar,#wpadminbar,.cookie-notice,.popup-overlay{display:none!important}
  #pcd{background:#fff!important;font-size:14px!important;width:100%!important;max-width:100%!important;margin:0!important;padding:0!important;float:none!important;position:static!important;overflow:visible!important}
  #pcd *{box-shadow:none!important;text-shadow:none!important;float:none!important}
  #pcd .H,#pcd .fo,#pcd .T,.bx,#pcd .AP,#pcd .guide-back,#pcd .vp,#pcd .sb-tabs{display:none!important}
  #pcd .land{display:none!important}
  #pcd .sb-panel{display:block!important}
  /* ── PRINT HEADER: full width gradient ── */
  #pcd .print-hdr{display:block!important;position:relative;width:100%;height:110px;overflow:hidden;margin:0 0 12px;-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
  #pcd .ph-bg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
  #pcd .ph-over{position:relative;z-index:1;display:flex!important;align-items:center;gap:14px;padding:20px 28px;height:100%}
  #pcd .ph-logo{width:48px;height:48px;border-radius:10px;object-fit:contain;padding:0;filter:brightness(0) invert(1)}
  #pcd .ph-title{color:#fff!important;font-size:20px!important;font-weight:700!important}
  #pcd .ph-sub{color:rgba(255,255,255,.85)!important;font-size:11px!important;margin-top:2px}
  /* ── GUIDES ── */
  #pcd .land{max-width:100%!important;padding:12px 20px!important}
  #pcd .fcard{break-inside:avoid;border:1px solid #ddd!important;margin-bottom:8px!important}
  #pcd .Lp,#pcd .Ll{display:none!important}
  #pcd .guide.on,[id="pcd-guide-os"][style*="block"],[id="pcd-guide-sb"][style*="block"]{display:block!important;max-width:100%!important;padding:0 20px!important}
  /* ── CARDS ── */
  #pcd .ac{break-inside:avoid;box-shadow:none!important;border:1px solid #E0E0EC!important;border-radius:10px!important;margin-bottom:6px!important;padding:12px 14px!important;background:#fff!important;display:flex!important;gap:10px!important}
  #pcd .ac:hover{transform:none!important}
  #pcd .mc{background:#FFF3ED!important;border:1.5px solid #FFD0B5!important}
  #pcd .an{width:18px!important;font-size:11px!important;padding-top:8px!important}
  #pcd .ai{width:38px!important;height:38px!important;min-width:38px!important;border-radius:10px!important;display:flex!important;align-items:center!important;justify-content:center!important;-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
  #pcd .ai img{width:26px!important;height:26px!important}
  #pcd .ab{flex:1!important;min-width:0!important}
  #pcd .nm{font-size:13px!important;font-weight:700!important;color:#1A1A2E!important}
  #pcd .mc .nm{color:#B83800!important}
  #pcd .pl{font-size:8px!important}
  #pcd .ad{font-size:10.5px!important;color:#4A4A66!important;line-height:1.4!important;margin-bottom:5px!important}
  #pcd .tg{font-size:8.5px!important;display:inline-block!important;padding:2px 7px!important;border-radius:4px!important;-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
  #pcd .af{display:flex!important;justify-content:space-between!important;align-items:center!important}
  #pcd .ctas{display:flex!important;gap:5px!important}
  #pcd .cta-d{display:inline-flex!important;font-size:8.5px!important;padding:3px 8px!important;border:1px solid #ddd!important;border-radius:5px!important;color:#555!important;background:#fff!important}
  #pcd .cta-a{display:inline-flex!important;font-size:8.5px!important;padding:3px 10px!important;background:#1001EF!important;color:#fff!important;border-radius:5px!important;text-decoration:none!important;-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
  /* ── CATEGORIES ── */
  #pcd .cat,#pcd .sl,#pcd .st{break-after:avoid;padding-top:14px!important;display:flex!important;font-size:10px!important}
  #pcd .cat::after,#pcd .sl::after,#pcd .st::after{-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
  /* ── CONTACT BANNER: full width ── */
  /* Contact banner (last page, full width, mirrors header) */
  #pcd .print-contact{display:block!important;break-inside:avoid;margin:20px 0 0;width:100%;position:relative;page-break-before:auto;overflow:hidden;z-index:999;page-break-inside:avoid;-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
  #pcd .pc-bg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
  #pcd .pc-inner{position:relative;z-index:1;background:rgba(26,26,46,.48)!important;padding:22px 28px 22px 32px!important;margin-bottom:-2mm;-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important;border-radius:0!important;padding:20px 28px;color:#fff!important;display:flex!important;align-items:center;gap:16px}
  #pcd .pc-logo{width:64px;height:64px;flex-shrink:0;border-radius:10px;overflow:hidden;border:none;display:block;text-decoration:none}
  #pcd .pc-logo img{width:100%;height:100%;object-fit:contain;filter:brightness(0) invert(1)}
  #pcd .pc-info{flex:1}
  #pcd .pc-name{font-size:13px!important;font-weight:700!important;color:#fff!important}
  #pcd .pc-role{font-size:9px!important;color:#99AABB!important;margin-top:2px!important}
  #pcd .pc-phrase{font-size:8.5px!important;color:#00f7ad!important;font-style:italic!important;margin-top:5px!important}
  #pcd .pc-links{display:flex!important;gap:6px;flex-wrap:wrap;margin-top:8px}
  #pcd .pc-links a{color:#fff!important;font-size:8px!important;max-width:200px!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;padding:3px 10px;background:rgba(255,255,255,.1)!important;border:1px solid rgba(255,255,255,.2)!important;border-radius:5px;text-decoration:none!important;-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
  #pcd .pc-coffee{background:rgba(0,247,173,.15)!important;color:#00f7ad!important;border-color:rgba(0,247,173,.3)!important}
  
  /* ── FOOTER: full width, stuck at bottom ── */
  /* Page footer (repeats every page) */
  #pcd .print-footer{display:block!important;position:fixed;bottom:5mm;left:0;right:0;text-align:center;font-family:'DM Sans',sans-serif;font-size:6.5pt;color:#888;border-top:1.5px solid #1001EF!important;padding:6px 20px 0;margin:0;background:#fff!important;-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
  #pcd .pf-b{color:#1001EF;font-weight:700;font-size:7.5pt}
  #pcd .pf-i{font-size:6.5pt;color:#aaa;margin-top:1px}
  /* ── LINKS ── */
  #pcd .eml{font-size:8px!important;display:inline!important}
  #pcd .pc-links .eml{font-size:8px!important}
  #pcd a{color:#1001EF!important}
  #pcd a[href]:after{content:none!important}
  #pcd .cta-a[href]:after,#pcd .cta-d[href]:after{content:none!important}
}

@media(max-width:600px){
  #pcd .tb{font-size:9px!important;display:block;overflow-x:auto;white-space:nowrap}
  #pcd .tb th,#pcd .tb td{padding:4px 3px!important;font-size:8px!important}
  #pcd .AC{padding:12px!important;overflow-x:auto}
  #pcd .cta-a,#pcd .cta-d{font-size:9px!important;padding:3px 6px!important}
  #pcd .af{flex-direction:column;align-items:flex-start!important;gap:6px!important}
}
#pcd .rulebox{background:#F3EEFF;border-left:3px solid #7C3AED;border-radius:0 8px 8px 0;padding:8px 12px;margin-top:8px;font-size:12px!important;color:#2D1B69!important;line-height:1.5!important}
#pcd .rulebox strong{color:#7C3AED!important;font-weight:700!important}
#pcd .rulebox ul{margin:4px 0 0 14px}
#pcd .rulebox ul li{list-style:disc;margin-bottom:3px}
#pcd .warnbox{background:#FFF7ED;border-left:3px solid var(--pcd-or);border-radius:0 8px 8px 0;padding:8px 12px;margin-top:8px;font-size:12px!important;color:#7C3004!important;line-height:1.5!important}
#pcd .warnbox strong{color:var(--pcd-or)!important;font-weight:700!important}
#pcd .warnbox ul{margin:4px 0 0 14px}
#pcd .warnbox ul li{list-style:disc;margin-bottom:3px}
#pcd .ac.star{background:#F3EEFF;border:1.5px solid #D8C5FF!important}
#pcd .ac.star:hover{border-color:#7C3AED!important;box-shadow:0 8px 25px rgba(124,58,237,.15)!important}
#pcd .ac.star .an{color:#7C3AED!important;font-size:14px!important}
#pcd .ac.star .nm{color:#4C1D95!important}
@media print{
  #pcd .rulebox,.warnbox{font-size:9.5px!important;padding:5px 8px!important;-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
  #pcd .ac.star{background:#F3EEFF!important;border:1.5px solid #D8C5FF!important;-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
}
</style></head><body><div id="pcd">

<!-- ╔═══════════════════════════════════════════════════════════════╗
     ║  🔧 CONFIG — TOUT SE MODIFIE ICI                            ║
     ╚═══════════════════════════════════════════════════════════════╝ -->
<script>
var C={
  LOGO_PCD:"https://presentcomposedesign.fr/wp-content/uploads/2024/03/cropped-logo_PCd_2024.png",
  LOGO_PCD_ON_SCREEN:false,  // true = visible à l'écran, false = print seulement

  LOGO_LIEU:"https://presentcomposedesign.fr/wp-content/uploads/2026/03/Logo_studio_m_wiki.png",
  LOGO_LIEU_ON_SCREEN:false, // true = visible à l'écran, false = print seulement

  TITRE:"Formations",
  CREDIT:"Présent Composé Design / Alban Desbarax",
  FAVICON:"https://presentcomposedesign.fr/wp-content/uploads/2024/03/cropped-logo_PCd_2024-32x32.png",
  SPLINE:"https://prod.spline.design/SsCdd-1WulmAbnAK/scene.splinecode",
  SPLINE_MODE:"header", // "header" | "fullscreen" | "header-footer"

  /* ── VIDEOS DEMO YOUTUBE ─────────────────────────────────────
   *  id = ID YouTube (ex: "UIZAiXYceBI"). Autoplay muet au hover.
   *       Clic sur la carte = active le son.
   *       Clic "▶ Demo" = ouvre YouTube plein écran avec son.
   *  q  = recherche YouTube (fallback si id est vide)
   *
   *  ▶ POUR TROUVER UN ID : ouvrir TROUVER_LES_IDS_YOUTUBE.html
   *    L'ID est le code après "v=" dans youtube.com/watch?v=XXXXX
   * ────────────────────────────────────────────────────────── */
  VIDEOS:{
    mistral: {id:"",q:"Mistral AI Le Chat demo tutoriel"},
    chatgpt: {id:"",q:"ChatGPT DALL-E image generation demo 2025"},
    gemini:  {id:"UIZAiXYceBI",q:"Google Gemini Veo 3 video demo"},
    canva:   {id:"",q:"Canva AI image video generator demo"},
    firefly: {id:"Sp6K3qpVFO0",q:"Adobe Firefly demo tutorial"},
    luma:    {id:"",q:"Luma AI Dream Machine video demo"},
    runway:  {id:"",q:"Runway Gen-4 AI video demo"},
    leonardo:{id:"",q:"Leonardo AI image generation tutorial"},
    ideogram:{id:"",q:"Ideogram AI text image demo"},
    copilot: {id:"S7xTBa93TX8",q:"Microsoft Copilot image generator demo"},
    pika:    {id:"",q:"Pika AI video generation demo"}
  }
};
document.addEventListener('DOMContentLoaded',function(){
  var lp=document.getElementById('pcd-lp'),ll=document.getElementById('pcd-ll');
  lp.querySelector('img').src=C.LOGO_PCD;
  ll.querySelector('img').src=C.LOGO_LIEU;
  if(C.LOGO_PCD_ON_SCREEN) lp.classList.add('show-screen');
  if(C.LOGO_LIEU_ON_SCREEN) ll.classList.add('show-screen');
  document.getElementById('pcd-t').textContent=C.TITRE;
  document.getElementById('pcd-c').textContent=C.CREDIT;
  var f=document.querySelector('link[rel="icon"]');if(f)f.href=C.FAVICON;
  document.getElementById('pcd-spline').setAttribute('url',C.SPLINE);

/* Apply Spline mode */
if(C.SPLINE_MODE==='fullscreen'){document.getElementById('pcd').classList.add('spline-full')}

});
</script>

<div class="T" id="pcd-toast">✓ Guide débloqué !</div>
<div class="fo" id="pcd-fo"><div class="fc"><div class="fcl" onclick="document.getElementById('pcd-fo').classList.remove('on')">✕</div><div class="fb" id="pcd-fo-badge">🎨 Formation Découverte IA</div><h2 id="pcd-fo-title">Débloquez le guide</h2><p class="fs" id="pcd-fo-desc">Entrez vos coordonnées pour accéder au <strong>guide sélectionné</strong>.</p><div class="fr"><div class="fg"><label>Prénom</label><input type="text" id="pcd-fn" placeholder="Votre prénom"></div><div class="fg"><label>Nom</label><input type="text" id="pcd-ln" placeholder="Votre nom"></div></div><div class="fg"><label>Email</label><input type="email" id="pcd-em" placeholder="votre@email.com"></div><div class="fg"><label>Téléphone <span class="op">(optionnel)</span></label><input type="tel" id="pcd-ph" placeholder="06 12 34 56 78"></div><div class="fg"><label>Vous êtes</label><select id="pcd-pr" onchange="var f=document.getElementById('pcd-sf');this.value==='pro_autre'?f.classList.add('v'):f.classList.remove('v')"><option value="">· Choisissez ·</option><option value="etudiant">Étudiant·e</option><option value="pro_design">Pro du design / créatif</option><option value="pro_autre">Pro (autre secteur)</option><option value="entrepreneur">Entrepreneur·e</option><option value="curieux">Curieux·se / Découverte</option></select></div><div class="fg sf" id="pcd-sf"><label>Secteur d'activité</label><input type="text" id="pcd-sc" placeholder="Ex : architecture, santé…"></div><div class="fg"><label>Code d'accès <span class="op">(fourni par le formateur)</span></label><input type="text" id="pcd-code" placeholder="1 2 3 4" maxlength="4" onkeydown="if(event.key==='Enter')pSub()" style="text-transform:uppercase;letter-spacing:4px;font-family:'Space Mono',monospace!important;font-size:18px!important;text-align:center"></div><button class="btn" id="pcd-sub" onclick="pSub()">Accéder au guide →</button><p class="fn">🔒 Données utilisées uniquement pour cette formation.</p></div></div>

<header class="H"><spline-viewer id="pcd-spline" loading="lazy" events-target="none"></spline-viewer><div class="Hi"><div class="Hl"><div class="Lp" id="pcd-lp"><img alt="PCD"></div><div class="Ht"><h1 id="pcd-t">Formations</h1><p id="pcd-c">Présent Composé Design / Alban Desbarax</p></div></div><div class="Ll" id="pcd-ll"><img alt=""></div></div></header>

<script>
var currentGuide='ia';
function openForm(g){
  currentGuide=g;
  var fo=document.getElementById('pcd-fo');
  var badge=document.getElementById('pcd-fo-badge');
  var title=document.getElementById('pcd-fo-title');
  var desc=document.getElementById('pcd-fo-desc');
  if(g==='ia'){
    badge.textContent='\u{1F3A8} Formation D\u00e9couverte IA';
    title.textContent='D\u00e9bloquez le guide IA';
    desc.innerHTML='Entrez vos coordonn\u00e9es pour acc\u00e9der au <strong>Top 10 des apps IA gratuites</strong>.';
  }else if(g==='sb'){
    badge.textContent='\u{1F3AC} Storyboard & virtual production IA TOOLBOX';
    title.textContent='D\u00e9bloquez la toolbox';
    desc.innerHTML='Acc\u00e9dez aux <strong>20 outils IA + 100 termes</strong> du storyboarder.';
  }else if(g==='vc'){
    badge.textContent='\u26A1 Claude Vibecoding';
    title.textContent='D\u00e9bloquez le guide Vibecoding';
    desc.innerHTML='Acc\u00e9dez au <strong>guide complet Claude Vibecoding</strong> — navigateur, VS Code, workflows et prompts.';
  }else{
    badge.textContent='\u{1F4BB} Applications Open Source';
    title.textContent='D\u00e9bloquez le guide';
    desc.innerHTML='Entrez vos coordonn\u00e9es pour acc\u00e9der au <strong>Top 25 des logiciels open source</strong>.';
  }
  fo.classList.add('on');
}
function showLanding(){
  document.getElementById('pcd-guide').classList.remove('on');
  var os=document.getElementById('pcd-guide-os');if(os)os.style.display='none';
  var sb=document.getElementById('pcd-guide-sb');if(sb)sb.style.display='none';
  var vc=document.getElementById('pcd-guide-vc');if(vc)vc.style.display='none';

  document.getElementById('pcd-land').style.display='block';
}


/* Fix Demo buttons: set real hrefs for PDF print */
document.querySelectorAll('#pcd-guide .cta-d').forEach(function(btn){
  var card=btn.closest('.ac[data-v]');
  if(!card)return;
  var key=card.getAttribute('data-v');
  var v=C.VIDEOS[key];
  if(!v)return;
  if(v.id){btn.href='https://www.youtube.com/watch?v='+v.id}
  else{btn.href='https://www.youtube.com/results?search_query='+encodeURIComponent(v.q)}
  btn.setAttribute('target','_blank');
});
/* OS guide Demo buttons already have real hrefs */

/* Video hover: autoplay for all cards */
document.querySelectorAll('.ac[data-v]').forEach(function(card){
  var key=card.getAttribute('data-v'),vp=card.querySelector('.vp');
  if(!vp)return;
  var v=C.VIDEOS?C.VIDEOS[key]:null;
  var demoBtn=card.querySelector('.cta-d');
  var demoHref=demoBtn?demoBtn.getAttribute('href'):'';
  var vid='';
  if(v&&v.id){vid=v.id}
  else if(demoHref){var m=demoHref.match(/watch\?v=([^&]+)/);if(m)vid=m[1]}
  var searchQ=(v&&v.q)?v.q:'';
  if(!searchQ&&demoHref){var sq=demoHref.match(/search_query=([^&]+)/);if(sq)searchQ=decodeURIComponent(sq[1]).replace(/\+/g,' ')}
  if(!vid)return;
  var timer=null;
  card.addEventListener('mouseenter',function(){
    timer=setTimeout(function(){
      var src='https://www.youtube.com/embed/'+vid+'?autoplay=1&mute=1&rel=0&modestbranding=1&controls=0';
      vp.innerHTML='<iframe src="'+src+'" allow="autoplay;encrypted-media" allowfullscreen loading="lazy"></iframe>';
      vp.classList.add('open');
    },300);
  });
  card.addEventListener('mouseleave',function(){
    clearTimeout(timer);
    vp.classList.remove('open');
    setTimeout(function(){vp.innerHTML=''},400);
  });
});
document.querySelectorAll('.eml').forEach(function(el){el.textContent=String.fromCharCode(99,111,110,116,97,99,116,64,112,114,101,115,101,110,116,99,111,109,112,111,115,101,100,101,115,105,103,110,46,102,114)});
</script>

<div class="print-hdr" id="pcd-print-hdr">
<img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/2026/03/abstract-gradient-background-Formation-page@1-2048x1111abstract-gradient-background-Formation_PresentComposedesign_header.png" class="ph-bg" alt="">
<div class="ph-over">
<a href="https://presentcomposedesign.fr" target="_blank"><img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/2025/07/Present-Compose-design_logo-image.svg" class="ph-logo" alt="PCD"></a>
<div class="ph-text">
<div class="ph-title">Formations</div>
<div class="ph-sub">Présent Composé Design / Alban Desbarax</div>
</div>
</div>
</div>
<section class="land" id="pcd-land"><div class="land-title">Ateliers & Ressources</div><div class="fcard" onclick="openForm('ia')"><h2>🎨 TOP 10 Applications IA Génératives 2026</h2><p class="fdesc">Les meilleures apps gratuites (téléphone & desktop) pour générer des images et vidéos facilement avec l'IA. Guide interactif avec démos vidéo.</p><span class="fbadge">GRATUIT · Guide interactif · 11 apps testées</span><div class="farrow"><svg viewBox="0 0 16 16"><path d="M5 11L11 5M11 5H6M11 5V10"/></svg></div></div><div class="fcard" onclick="openForm('os')" style="margin-top:12px"><h2>💻 TOP 25 Applications Open Source à installer en 2026</h2><p class="fdesc">La sélection essentielle des logiciels gratuits & open source pour la création 3D, audio, vidéo, design, impression 3D, code créatif et productivité.</p><span class="fbadge">GRATUIT · 25 logiciels · 7 catégories · Liens de téléchargement</span><div class="farrow"><svg viewBox="0 0 16 16"><path d="M5 11L11 5M11 5H6M11 5V10"/></svg></div></div><div class="fcard" onclick="openForm('sb')" style="margin-top:12px"><h2>🎬 Storyboard &amp; virtual production IA TOOLBOX</h2><p class="fdesc">20 outils IA et 100 termes techniques pour la création de storyboards. Glossaire interactif, prompts prêts à l'emploi, liens et demos.</p><span class="fbadge">GRATUIT · 20 outils · 100 termes · 2 onglets</span><div class="farrow"><svg viewBox="0 0 16 16"><path d="M5 11L11 5M11 5H6M11 5V10"/></svg></div></div><div class="fcard" onclick="openForm('vc')" style="margin-top:12px;border-color:#D8C5FF;position:relative;overflow:hidden"><div style="position:absolute;top:0;left:0;right:0;height:4px;background:linear-gradient(90deg,#7C3AED,#FF6B35)"></div><h2>⚡ Claude Vibecoding — Guide complet</h2><p class="fdesc">Maîtriser Claude dans le navigateur et dans VS Code. Workflows, prompts-clés, pièges à éviter, méthodes d'itération rapide pour créatifs qui codent.</p><span class="fbadge" style="background:#F3EEFF;color:#7C3AED">GRATUIT · 15 fiches · Navigateur · VS Code · Prompts à copier</span><div class="farrow" style="background:#7C3AED"><svg viewBox="0 0 16 16"><path d="M5 11L11 5M11 5H6M11 5V10"/></svg></div></div></section>

<section class="guide" id="pcd-guide">
<div class="guide-back" onclick="showLanding()">← Retour aux formations</div>
<div class="sl">★ Incontournable / Assistant IA quotidien</div>
<div class="ac mc" data-v="mistral"><div class="an">★</div><div class="ai" style="background:linear-gradient(135deg,#FF6B35,#FF8F5E);"><img decoding="async" src="https://www.google.com/s2/favicons?domain=mistral.ai&sz=64" alt="" onerror="this.style.display='none';this.parentElement.textContent='M'"></div><div class="ab"><div class="ah"><span class="nm">Mistral AI (Le Chat)</span><span class="pl">Web · iOS · Android</span></div><p class="ad">Assistant IA français polyvalent : texte, recherche, résumé, analyse. Idéal pour la productivité quotidienne.</p><div class="af"><div class="at"><span class="tg" style="background:#FFF3ED;color:#CC4400;">Texte</span><span class="tg" style="background:#FFF3ED;color:#CC4400;">Recherche</span><span class="tg" style="background:#FFF3ED;color:#CC4400;">Analyse</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Mistral+AI+Le+Chat+demo+tutoriel" target="_blank">▶ Demo</a><a class="cta-a" href="https://chat.mistral.ai" target="_blank">Accéder →</a></div></div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">🖼️</span> Images / Génération & édition</div>
<div class="ac" data-v="chatgpt"><div class="an">1</div><div class="ai" style="background:#E6F7F1;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=chatgpt.com&sz=64" alt="" onerror="this.parentElement.style.background='#10A37F';this.parentElement.textContent='G';this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">ChatGPT + DALL·E</span><span class="pl">Web · iOS · Android</span></div><p class="ad">Génération d'images réalistes via GPT-4o. ~3 images/jour en gratuit. Texte lisible dans les images.</p><div class="af"><div class="at"><span class="tg" style="background:#E6F7F1;color:#0D8C6C;">Images</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;">Texte</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=ChatGPT+DALL-E+image+generation+demo+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://chatgpt.com" target="_blank">Accéder →</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="gemini"><div class="an">2</div><div class="ai" style="background:#E8F0FE;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=gemini.google.com&sz=64" alt="" onerror="this.parentElement.style.background='#4285F4';this.parentElement.textContent='G';this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Google Gemini + Veo 3.1</span><span class="pl">Web · iOS · Android</span></div><p class="ad">IA multimodale Google. Images (Nano Banana) et vidéos HD avec audio natif via Flow. Gratuit et puissant.</p><div class="af"><div class="at"><span class="tg" style="background:#E8F0FE;color:#1A73E8;">Images</span><span class="tg" style="background:#E8F0FE;color:#1A73E8;">Vidéos</span><span class="tg" style="background:#E8F0FE;color:#1A73E8;">Audio</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/watch?v=UIZAiXYceBI" target="_blank">▶ Demo</a><a class="cta-a" href="https://gemini.google.com" target="_blank">Accéder →</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="canva"><div class="an">3</div><div class="ai" style="background:#E5F9FA;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=canva.com&sz=64" alt="" onerror="this.parentElement.style.background='#00C4CC';this.parentElement.textContent='C';this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Canva IA</span><span class="pl">Web · iOS · Android</span></div><p class="ad">Suite de design avec images (Média Magique) et vidéos IA (Veo-3). Intuitive, idéale pour débutants.</p><div class="af"><div class="at"><span class="tg" style="background:#E5F9FA;color:#00A5AB;">Images</span><span class="tg" style="background:#E5F9FA;color:#00A5AB;">Vidéos</span><span class="tg" style="background:#E5F9FA;color:#00A5AB;">Design</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Canva+AI+image+video+generator+demo" target="_blank">▶ Demo</a><a class="cta-a" href="https://www.canva.com" target="_blank">Accéder →</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="firefly"><div class="an">4</div><div class="ai" style="background:#FFF0EB;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=firefly.adobe.com&sz=64" alt="" onerror="this.parentElement.style.background='#FF4500';this.parentElement.textContent='A';this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Adobe Firefly</span><span class="pl">Web · Desktop</span></div><p class="ad">Générateur images/vidéos premium d'Adobe. 25 crédits gratuits/mois. Remplissage génératif et retouche.</p><div class="af"><div class="at"><span class="tg" style="background:#FFF0EB;color:#CC3700;">Images</span><span class="tg" style="background:#FFF0EB;color:#CC3700;">Vidéos</span><span class="tg" style="background:#FFF0EB;color:#CC3700;">Retouche</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/watch?v=Sp6K3qpVFO0" target="_blank">▶ Demo</a><a class="cta-a" href="https://firefly.adobe.com" target="_blank">Accéder →</a></div></div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">🎬</span> Vidéos / Génération & animation</div>
<div class="ac" data-v="luma"><div class="an">5</div><div class="ai" style="background:#F0EAFF;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=lumalabs.ai&sz=64" alt="" onerror="this.parentElement.style.background='#7C3AED';this.parentElement.textContent='L';this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Luma AI (Dream Machine)</span><span class="pl">Web · iOS · Android</span></div><p class="ad">Génération vidéo réaliste à partir de texte ou d'images. Capture 3D photoréaliste avec téléphone.</p><div class="af"><div class="at"><span class="tg" style="background:#F0EAFF;color:#6D28D9;">Vidéos</span><span class="tg" style="background:#F0EAFF;color:#6D28D9;">3D</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Luma+AI+Dream+Machine+video+demo" target="_blank">▶ Demo</a><a class="cta-a" href="https://lumalabs.ai" target="_blank">Accéder →</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="runway"><div class="an">6</div><div class="ai" style="background:#E6F0FF;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=runwayml.com&sz=64" alt="" onerror="this.parentElement.style.background='#0066FF';this.parentElement.textContent='R';this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Runway (Gen-4)</span><span class="pl">Web · iOS</span></div><p class="ad">Outil cinématographique. Vidéo à partir de texte, images ou clips. Productions réelles.</p><div class="af"><div class="at"><span class="tg" style="background:#E6F0FF;color:#0052CC;">Vidéos</span><span class="tg" style="background:#E6F0FF;color:#0052CC;">Cinéma</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Runway+Gen-4+AI+video+demo" target="_blank">▶ Demo</a><a class="cta-a" href="https://runwayml.com" target="_blank">Accéder →</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="leonardo"><div class="an">7</div><div class="ai" style="background:#F0EAFF;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=leonardo.ai&sz=64" alt="" onerror="this.parentElement.style.background='#8B5CF6';this.parentElement.textContent='L';this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Leonardo AI</span><span class="pl">Web · iOS · Android</span></div><p class="ad">Générateur d'images avec nombreux modèles. 150 tokens/jour gratuits. Assets visuels.</p><div class="af"><div class="at"><span class="tg" style="background:#F0EAFF;color:#7C3AED;">Images</span><span class="tg" style="background:#F0EAFF;color:#7C3AED;">Assets</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Leonardo+AI+image+generation+tutorial" target="_blank">▶ Demo</a><a class="cta-a" href="https://leonardo.ai" target="_blank">Accéder →</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="ideogram"><div class="an">8</div><div class="ai" style="background:#FFF0F3;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=ideogram.ai&sz=64" alt="" onerror="this.parentElement.style.background='#E11D48';this.parentElement.textContent='I';this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Ideogram</span><span class="pl">Web · iOS · Android</span></div><p class="ad">Maîtrise du texte dans les images. ~40 images/semaine gratuits. Résultats créatifs.</p><div class="af"><div class="at"><span class="tg" style="background:#FFF0F3;color:#BE123C;">Images</span><span class="tg" style="background:#FFF0F3;color:#BE123C;">Typographie</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Ideogram+AI+text+image+demo" target="_blank">▶ Demo</a><a class="cta-a" href="https://ideogram.ai" target="_blank">Accéder →</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="copilot"><div class="an">9</div><div class="ai" style="background:#E6F2FF;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=copilot.microsoft.com&sz=64" alt="" onerror="this.parentElement.style.background='#0078D4';this.parentElement.textContent='C';this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Microsoft Copilot</span><span class="pl">Web · iOS · Android</span></div><p class="ad">DALL·E 3 gratuit et illimité via Microsoft. Aucune inscription nécessaire.</p><div class="af"><div class="at"><span class="tg" style="background:#E6F2FF;color:#005EA6;">Images</span><span class="tg" style="background:#E6F2FF;color:#005EA6;">Illimité</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/watch?v=S7xTBa93TX8" target="_blank">▶ Demo</a><a class="cta-a" href="https://copilot.microsoft.com" target="_blank">Accéder →</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="pika"><div class="an">10</div><div class="ai" style="background:#FEF3C7;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=pika.art&sz=64" alt="" onerror="this.parentElement.style.background='#F59E0B';this.parentElement.textContent='P';this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Pika</span><span class="pl">Web · iOS</span></div><p class="ad">Vidéos courtes créatives pour réseaux sociaux. Interface simple.</p><div class="af"><div class="at"><span class="tg" style="background:#FEF3C7;color:#D97706;">Vidéos</span><span class="tg" style="background:#FEF3C7;color:#D97706;">Social</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Pika+AI+video+generation+demo" target="_blank">▶ Demo</a><a class="cta-a" href="https://pika.art" target="_blank">Accéder →</a></div></div></div><div class="vp"></div></div>
</section>


<section class="guide" id="pcd-guide-os" style="display:none">
<div class="guide-back" onclick="showLanding()">← Retour aux formations</div>
<div class="sl">💻 TOP 25 Applications Open Source & Gratuites · PC 2026</div>
<div class="cat"><span class="cat-icon">🎮</span> 3D · Moteurs · VR / XR</div>
<div class="ac" data-v="blender.org"><div class="ai" style="background:#E8E5FF;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=blender.org&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Blender</span></div><p class="ad">Suite 3D complète : modélisation, animation, rendu, compositing. Le standard open source.</p><div class="af"><div class="at"><span class="tg" style="background:#E8E5FF;color:#1001EF;">3D</span><span class="tg" style="background:#E8E5FF;color:#1001EF;">Animation</span><span class="tg" style="background:#E8E5FF;color:#1001EF;">Rendu</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Blender+3D+tutorial+d%C3%A9butant+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://www.blender.org/download/" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="roblox.com"><div class="ai" style="background:#E8E5FF;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=roblox.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Roblox Studio</span></div><p class="ad">Créez vos propres jeux et expériences 3D. Plateforme massive de création interactive.</p><div class="af"><div class="at"><span class="tg" style="background:#E8E5FF;color:#1001EF;">Jeux</span><span class="tg" style="background:#E8E5FF;color:#1001EF;">3D</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Roblox+Studio+tutorial+d%C3%A9butant+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://www.roblox.com/create" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="unity.com"><div class="ai" style="background:#E8E5FF;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=unity.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Unity / Unreal Engine</span></div><p class="ad">Les deux moteurs de jeu de référence. Unity (C#) et Unreal (Blueprints/C++). Gratuits pour débuter.</p><div class="af"><div class="at"><span class="tg" style="background:#E8E5FF;color:#1001EF;">Jeux</span><span class="tg" style="background:#E8E5FF;color:#1001EF;">3D</span><span class="tg" style="background:#E8E5FF;color:#1001EF;">XR</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Unity+vs+Unreal+Engine+tutorial+d%C3%A9butant+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://unity.com/download" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="babylonjs.com"><div class="ai" style="background:#E8E5FF;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=babylonjs.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Babylon.js</span></div><p class="ad">Moteur 3D web open source par Microsoft. Créez des expériences 3D/WebXR dans le navigateur.</p><div class="af"><div class="at"><span class="tg" style="background:#E8E5FF;color:#1001EF;">Web3D</span><span class="tg" style="background:#E8E5FF;color:#1001EF;">WebXR</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Babylon.js+tutorial+getting+started+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://www.babylonjs.com/" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="sidequestvr.com"><div class="ai" style="background:#E8E5FF;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=sidequestvr.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">SideQuest</span></div><p class="ad">Plateforme de sideloading pour Meta Quest. Accédez à des apps VR non officielles.</p><div class="af"><div class="at"><span class="tg" style="background:#E8E5FF;color:#1001EF;">VR</span><span class="tg" style="background:#E8E5FF;color:#1001EF;">Quest</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=SideQuest+Meta+Quest+tutorial+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://sidequestvr.com/" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">🎵</span> Audio · Musique · Son</div>
<div class="ac" data-v="reaper.fm"><div class="ai" style="background:#FFF3ED;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=reaper.fm&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Reaper</span></div><p class="ad">DAW professionnel ultra-léger. Licence d'évaluation illimitée. Plugins VST, MIDI, multitrack.</p><div class="af"><div class="at"><span class="tg" style="background:#FFF3ED;color:#CC4400;">DAW</span><span class="tg" style="background:#FFF3ED;color:#CC4400;">Audio</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Reaper+DAW+tutorial+d%C3%A9butant+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://www.reaper.fm/download.php" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="ableton.com"><div class="ai" style="background:#FFF3ED;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=ableton.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Ableton Live Lite</span></div><p class="ad">DAW référence pour la musique électronique et le live. Version Lite gratuite avec instruments.</p><div class="af"><div class="at"><span class="tg" style="background:#FFF3ED;color:#CC4400;">DAW</span><span class="tg" style="background:#FFF3ED;color:#CC4400;">Live</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Ableton+Live+Lite+tutorial+d%C3%A9butant+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://www.ableton.com/en/products/live-lite/" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="cycling74.com"><div class="ai" style="background:#FFF3ED;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=cycling74.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Max MSP</span></div><p class="ad">Environnement de programmation visuelle pour audio, MIDI, vidéo et médias interactifs.</p><div class="af"><div class="at"><span class="tg" style="background:#FFF3ED;color:#CC4400;">Audio</span><span class="tg" style="background:#FFF3ED;color:#CC4400;">Visuel</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Max+MSP+tutorial+d%C3%A9butant+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://cycling74.com/downloads" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">🎬</span> Vidéo · Montage · Compositing</div>
<div class="ac" data-v="blackmagicdesign.com"><div class="ai" style="background:#E6F7F1;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=blackmagicdesign.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">DaVinci Resolve</span></div><p class="ad">Suite pro complète : montage, étalonnage, VFX, mixage audio. Gratuit et très puissant.</p><div class="af"><div class="at"><span class="tg" style="background:#E6F7F1;color:#0D8C6C;">Montage</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;">VFX</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;">Color</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=DaVinci+Resolve+tutorial+d%C3%A9butant+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://www.blackmagicdesign.com/products/davinciresolve" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="capcut.com"><div class="ai" style="background:#E6F7F1;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=capcut.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">CapCut</span></div><p class="ad">Montage vidéo simple et efficace. Templates, effets, sous-titres auto. Idéal réseaux sociaux.</p><div class="af"><div class="at"><span class="tg" style="background:#E6F7F1;color:#0D8C6C;">Montage</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;">Social</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=CapCut+tutorial+montage+d%C3%A9butant+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://www.capcut.com/" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="natrongithub.github.io"><div class="ai" style="background:#E6F7F1;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=natrongithub.github.io&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Natron</span></div><p class="ad">Compositing node-based open source. Alternative à After Effects/Nuke pour le VFX.</p><div class="af"><div class="at"><span class="tg" style="background:#E6F7F1;color:#0D8C6C;">VFX</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;">Compositing</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Natron+compositing+tutorial+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://natrongithub.github.io/" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="videolan.org"><div class="ai" style="background:#E6F7F1;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=videolan.org&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">VLC</span></div><p class="ad">Lecteur multimédia universel. Lit tous les formats, convertit, streame. Incontournable.</p><div class="af"><div class="at"><span class="tg" style="background:#E6F7F1;color:#0D8C6C;">Lecteur</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;">Conversion</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=VLC+media+player+astuces+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://www.videolan.org/vlc/" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">🎨</span> Design · Image · UI</div>
<div class="ac" data-v="figma.com"><div class="ai" style="background:#F0EAFF;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=figma.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Figma</span></div><p class="ad">Design d'interfaces, prototypage, collaboration temps réel. Version gratuite très généreuse.</p><div class="af"><div class="at"><span class="tg" style="background:#F0EAFF;color:#7C3AED;">UI/UX</span><span class="tg" style="background:#F0EAFF;color:#7C3AED;">Prototypage</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Figma+tutorial+d%C3%A9butant+UI+design+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://www.figma.com/downloads/" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="affinity.serif.com"><div class="ai" style="background:#F0EAFF;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=affinity.serif.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Affinity</span></div><p class="ad">Suite créative (Photo, Designer, Publisher). Alternative premium à Adobe, licence perpétuelle.</p><div class="af"><div class="at"><span class="tg" style="background:#F0EAFF;color:#7C3AED;">Design</span><span class="tg" style="background:#F0EAFF;color:#7C3AED;">Photo</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Affinity+Designer+Photo+tutorial+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://affinity.serif.com/" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="xnview.com"><div class="ai" style="background:#F0EAFF;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=xnview.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">XnView</span></div><p class="ad">Visionneuse et convertisseur d'images. Supporte 500+ formats. Batch processing puissant.</p><div class="af"><div class="at"><span class="tg" style="background:#F0EAFF;color:#7C3AED;">Images</span><span class="tg" style="background:#F0EAFF;color:#7C3AED;">Batch</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=XnView+tutorial+batch+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://www.xnview.com/en/" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">🖨️</span> Impression 3D · Fabrication</div>
<div class="ac" data-v="bambulab.com"><div class="ai" style="background:#FEF3C7;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=bambulab.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Bambu Studio</span></div><p class="ad">Slicer officiel Bambu Lab. Profils optimisés, multi-couleur, interface moderne.</p><div class="af"><div class="at"><span class="tg" style="background:#FEF3C7;color:#D97706;">Slicer</span><span class="tg" style="background:#FEF3C7;color:#D97706;">FDM</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Bambu+Studio+slicer+tutorial+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://bambulab.com/en/download/studio" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="github.com"><div class="ai" style="background:#FEF3C7;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Orca Slicer</span></div><p class="ad">Fork communautaire de Bambu Studio. Compatible toutes imprimantes, très personnalisable.</p><div class="af"><div class="at"><span class="tg" style="background:#FEF3C7;color:#D97706;">Slicer</span><span class="tg" style="background:#FEF3C7;color:#D97706;">Open Source</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Orca+Slicer+tutorial+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://github.com/SoftFever/OrcaSlicer/releases" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="ultimaker.com"><div class="ai" style="background:#FEF3C7;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=ultimaker.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Cura (UltiMaker)</span></div><p class="ad">Slicer universel le plus utilisé au monde. Marketplace de plugins, profils communautaires.</p><div class="af"><div class="at"><span class="tg" style="background:#FEF3C7;color:#D97706;">Slicer</span><span class="tg" style="background:#FEF3C7;color:#D97706;">Universel</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Cura+slicer+tutorial+d%C3%A9butant+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://ultimaker.com/software/ultimaker-cura/" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">⚡</span> Code créatif · IA générative</div>
<div class="ac" data-v="derivative.ca"><div class="ai" style="background:#FFF0F3;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=derivative.ca&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">TouchDesigner</span></div><p class="ad">Programmation visuelle temps réel pour installations interactives, VJing, mapping.</p><div class="af"><div class="at"><span class="tg" style="background:#FFF0F3;color:#BE123C;">Interactif</span><span class="tg" style="background:#FFF0F3;color:#BE123C;">Temps réel</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=TouchDesigner+tutorial+d%C3%A9butant+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://derivative.ca/download" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="processing.org"><div class="ai" style="background:#FFF0F3;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=processing.org&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Processing</span></div><p class="ad">Langage de programmation créative. Idéal pour apprendre le code par le visuel.</p><div class="af"><div class="at"><span class="tg" style="background:#FFF0F3;color:#BE123C;">Code</span><span class="tg" style="background:#FFF0F3;color:#BE123C;">Art</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Processing+creative+coding+tutorial+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://processing.org/download" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="arduino.cc"><div class="ai" style="background:#FFF0F3;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=arduino.cc&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Arduino</span></div><p class="ad">Plateforme électronique open source. IDE + cartes pour prototypage interactif.</p><div class="af"><div class="at"><span class="tg" style="background:#FFF0F3;color:#BE123C;">Électronique</span><span class="tg" style="background:#FFF0F3;color:#BE123C;">IoT</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Arduino+tutorial+d%C3%A9butant+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://www.arduino.cc/en/software" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="comfy.org"><div class="ai" style="background:#FFF0F3;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=comfy.org&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">ComfyUI</span></div><p class="ad">Interface node-based pour Stable Diffusion. Workflows visuels pour la génération d'images IA.</p><div class="af"><div class="at"><span class="tg" style="background:#FFF0F3;color:#BE123C;">IA</span><span class="tg" style="background:#FFF0F3;color:#BE123C;">Stable Diffusion</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=ComfyUI+tutorial+d%C3%A9butant+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://github.com/comfyanonymous/ComfyUI" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">🔧</span> Outils · Productivité · Communication</div>
<div class="ac" data-v="code.visualstudio.com"><div class="ai" style="background:#E6F2FF;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=code.visualstudio.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Visual Studio Code</span></div><p class="ad">Éditeur de code le plus populaire. Extensions, Git, terminal intégré, IA (Copilot).</p><div class="af"><div class="at"><span class="tg" style="background:#E6F2FF;color:#005EA6;">Code</span><span class="tg" style="background:#E6F2FF;color:#005EA6;">IDE</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Visual+Studio+Code+tutorial+d%C3%A9butant+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://code.visualstudio.com/" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="openoffice.org"><div class="ai" style="background:#E6F2FF;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=openoffice.org&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">OpenOffice</span></div><p class="ad">Suite bureautique complète et gratuite. Writer, Calc, Impress. Alternative à Microsoft Office.</p><div class="af"><div class="at"><span class="tg" style="background:#E6F2FF;color:#005EA6;">Bureau</span><span class="tg" style="background:#E6F2FF;color:#005EA6;">Docs</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=OpenOffice+tutorial+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://www.openoffice.org/download/" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>
<div class="ac" data-v="discord.com"><div class="ai" style="background:#E6F2FF;"><img decoding="async" src="https://www.google.com/s2/favicons?domain=discord.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Discord</span></div><p class="ad">Plateforme de communication. Salons vocaux/texte, partage d'écran, bots, communautés.</p><div class="af"><div class="at"><span class="tg" style="background:#E6F2FF;color:#005EA6;">Chat</span><span class="tg" style="background:#E6F2FF;color:#005EA6;">Communauté</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Discord+tutorial+d%C3%A9butant+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://discord.com/download" target="_blank">Télécharger ↓</a></div></div></div><div class="vp"></div></div>

</section>






<section class="guide" id="pcd-guide-sb" style="display:none">
<div class="guide-back" onclick="showLanding()">← Retour aux formations</div>
<div class="sl">🎬 STORYBOARD & VIRTUAL PRODUCTION IA TOOLBOX</div>
<div class="sb-tabs">
<div class="sb-tab active" onclick="document.querySelectorAll('.sb-tab,.sb-panel').forEach(e=>e.classList.remove('active'));this.classList.add('active');document.getElementById('sb-outils').classList.add('active')">🛠 Outils (20)</div>
<div class="sb-tab" onclick="document.querySelectorAll('.sb-tab,.sb-panel').forEach(e=>e.classList.remove('active'));this.classList.add('active');document.getElementById('sb-glossaire').classList.add('active')">📖 Glossaire (100)</div>
</div>
<div class="sb-panel active" id="sb-outils">
<div class="cat"><span class="cat-icon">🖼️</span> Génération d'images</div>
<div class="ac" data-v="www.comfy.org"><div class="an">1</div><div class="ai" style="background:#E8F0FE"><img decoding="async" src="https://www.google.com/s2/favicons?domain=www.comfy.org&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">ComfyUI</span><span class="pl" style="background:#e8f5ed;color:#27ae60;padding:2px 6px;border-radius:3px;font-size:9px">Gratuit</span></div><p class="ad">Interface node-based pour Stable Diffusion et Flux. Workflows visuels puissants pour la génération d'images IA. Personnalisation totale, ControlNet, IP-Adapter, cohérence entre panels.</p><div class="af"><div class="at"><span class="tg" style="background:#E8F0FE;color:#1A73E8">Workflow visuel</span><span class="tg" style="background:#E8F0FE;color:#1A73E8">Stable Diffusion</span><span class="tg" style="background:#E8F0FE;color:#1A73E8">ControlNet</span><span class="tg" style="background:#E8F0FE;color:#1A73E8">Cohérence</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=ComfyUI" target="_blank">▶ Demo</a><a class="cta-a" href="https://www.comfy.org/" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="ControlNet pose + IP-Adapter style: Generate storyboard panel, plan rapproché, dutch angle 15°, hard side lighting, clair-obscur, background defocused urban décor, cinematic 16:9, high contrast B&amp;W storyboard sketch style" title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>ControlNet pose + IP-Adapter style: Generate storyboard panel, plan rapproché, dutch angle 15°, hard side lighting, clair-obscur, background defocused urban déc...</div></div><div class="vp"></div></div>
<div class="ac" data-v="labs.google"><div class="an">2</div><div class="ai" style="background:#E8F0FE"><img decoding="async" src="https://www.google.com/s2/favicons?domain=labs.google&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Google Flow</span><span class="pl" style="background:#e8f5ed;color:#27ae60;padding:2px 6px;border-radius:3px;font-size:9px">Gratuit</span></div><p class="ad">Outil Google pour créer des storyboards et moodboards visuels avec l'IA. Génération d'images à partir de prompts textuels, idéal pour la pré-production et la visualisation rapide de concepts narratifs.</p><div class="af"><div class="at"><span class="tg" style="background:#E8F0FE;color:#1A73E8">Storyboard</span><span class="tg" style="background:#E8F0FE;color:#1A73E8">Moodboard</span><span class="tg" style="background:#E8F0FE;color:#1A73E8">Google AI</span><span class="tg" style="background:#E8F0FE;color:#1A73E8">Pré-production</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Google+Flow" target="_blank">▶ Demo</a><a class="cta-a" href="https://labs.google/flow/about" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Cinematic storyboard panel, establishing shot, rain-soaked alley at night, low key lighting, hard light from neon creating clair-obscur, foreground bokeh, deep depth of field, 2.39:1 cinémascope" title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>Cinematic storyboard panel, establishing shot, rain-soaked alley at night, low key lighting, hard light from neon creating clair-obscur, foreground bokeh, deep ...</div></div><div class="vp"></div></div>
<div class="ac" data-v="stability.ai"><div class="an">3</div><div class="ai" style="background:#E8F0FE"><img decoding="async" src="https://www.google.com/s2/favicons?domain=stability.ai&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Stable Diffusion</span><span class="pl" style="background:#e8f5ed;color:#27ae60;padding:2px 6px;border-radius:3px;font-size:9px">Gratuit</span></div><p class="ad">Modèle open-source de génération d'images. Utilisable localement ou via ComfyUI. Parfait pour créer des planches cohérentes avec des LoRA personnalisés (personnages, univers visuels constants).</p><div class="af"><div class="at"><span class="tg" style="background:#E8F0FE;color:#1A73E8">Cohérence personnage</span><span class="tg" style="background:#E8F0FE;color:#1A73E8">Style graphique</span><span class="tg" style="background:#E8F0FE;color:#1A73E8">Batch génération</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Stable+Diffusion" target="_blank">▶ Demo</a><a class="cta-a" href="https://stability.ai" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Storyboard frame, plan américain shot, male detective character, over-the-shoulder framing, contre-plongée angle, rim light on shoulders, soft light fill, background slightly defocused, cinematic 16:9, desaturated color palette, pencil sketch style" title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>Storyboard frame, plan américain shot, male detective character, over-the-shoulder framing, contre-plongée angle, rim light on shoulders, soft light fill, backg...</div></div><div class="vp"></div></div>
<div class="ac" data-v="openai.com"><div class="an">4</div><div class="ai" style="background:#E8F0FE"><img decoding="async" src="https://www.google.com/s2/favicons?domain=openai.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">DALL·E 3</span><span class="pl" style="background:#fff8e8;color:#d4891a;padding:2px 6px;border-radius:3px;font-size:9px">Freemium</span></div><p class="ad">Accessible directement via ChatGPT. Excellente compréhension des instructions en langage naturel. Utile pour générer rapidement des thumbnails de séquences et tester des compositions.</p><div class="af"><div class="at"><span class="tg" style="background:#E8F0FE;color:#1A73E8">Prototypage rapide</span><span class="tg" style="background:#E8F0FE;color:#1A73E8">Composition</span><span class="tg" style="background:#E8F0FE;color:#1A73E8">Intégré ChatGPT</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=DALL·E+3" target="_blank">▶ Demo</a><a class="cta-a" href="https://openai.com/dall-e-3" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Storyboard thumbnail, plan d&#x27;ensemble of a forest clearing at golden hour, high key natural lighting, rule of thirds composition with protagonist in left third, symmetrical tree framing, cinematic flat 1.85:1 aspect ratio, warm color temperature, impressionist style" title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>Storyboard thumbnail, plan d&#x27;ensemble of a forest clearing at golden hour, high key natural lighting, rule of thirds composition with protagonist in left third,...</div></div><div class="vp"></div></div>
<div class="ac" data-v="blackforestlabs.ai"><div class="an">5</div><div class="ai" style="background:#E8F0FE"><img decoding="async" src="https://www.google.com/s2/favicons?domain=blackforestlabs.ai&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Flux (Black Forest Labs)</span><span class="pl" style="background:#fff8e8;color:#d4891a;padding:2px 6px;border-radius:3px;font-size:9px">Freemium</span></div><p class="ad">Modèle nouvelle génération aux rendus photographiques exceptionnels. Très précis sur les compositions complexes et le rendu des textures. Idéal pour des panneaux de storyboard ultra-détaillés.</p><div class="af"><div class="at"><span class="tg" style="background:#E8F0FE;color:#1A73E8">Rendu photoréaliste</span><span class="tg" style="background:#E8F0FE;color:#1A73E8">Texture & matière</span><span class="tg" style="background:#E8F0FE;color:#1A73E8">Composition précise</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Flux+(Black+Forest+Labs)" target="_blank">▶ Demo</a><a class="cta-a" href="https://blackforestlabs.ai" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Cinematic storyboard panel, bird&#x27;s eye view of a city intersection during golden hour, very long shot establishing geography, leading lines converging to center, silhouette figures, contre-jour sunlight, warm backlight, cinematic 2.35:1 scope format, photorealistic" title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>Cinematic storyboard panel, bird&#x27;s eye view of a city intersection during golden hour, very long shot establishing geography, leading lines converging to center...</div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">🎬</span> Génération vidéo</div>
<div class="ac" data-v="runwayml.com"><div class="an">6</div><div class="ai" style="background:#F0EAFF"><img decoding="async" src="https://www.google.com/s2/favicons?domain=runwayml.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Runway Gen-4</span><span class="pl" style="background:#fff8e8;color:#d4891a;padding:2px 6px;border-radius:3px;font-size:9px">Freemium</span></div><p class="ad">Transforme une image fixe en clip vidéo avec contrôle des mouvements de caméra. Permet de créer une animatique directement depuis ses panels de storyboard en quelques clics.</p><div class="af"><div class="at"><span class="tg" style="background:#F0EAFF;color:#7C3AED">Image-to-video</span><span class="tg" style="background:#F0EAFF;color:#7C3AED">Animatique</span><span class="tg" style="background:#F0EAFF;color:#7C3AED">Mouvement caméra</span><span class="tg" style="background:#F0EAFF;color:#7C3AED">Durée plan</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Runway+Gen-4" target="_blank">▶ Demo</a><a class="cta-a" href="https://runwayml.com" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Camera motion: slow dolly in toward subject, subtle tilt up, soft focus pull (rack focus) from foreground to background. Lighting remains constant high key. No jump cut. Smooth tracking movement, 4 seconds duration." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>Camera motion: slow dolly in toward subject, subtle tilt up, soft focus pull (rack focus) from foreground to background. Lighting remains constant high key. No ...</div></div><div class="vp"></div></div>
<div class="ac" data-v="sora.com"><div class="an">7</div><div class="ai" style="background:#F0EAFF"><img decoding="async" src="https://www.google.com/s2/favicons?domain=sora.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Sora (OpenAI)</span><span class="pl" style="background:#fce8e8;color:#c0392b;padding:2px 6px;border-radius:3px;font-size:9px">Payant</span></div><p class="ad">Génération vidéo longue durée depuis texte ou image. Excellent pour simuler des plans-séquences complexes, des mouvements fluides et des transitions entre scènes-clés d'une séquence narrative.</p><div class="af"><div class="at"><span class="tg" style="background:#F0EAFF;color:#7C3AED">Plan-séquence</span><span class="tg" style="background:#F0EAFF;color:#7C3AED">Text-to-video</span><span class="tg" style="background:#F0EAFF;color:#7C3AED">Simulation mouvement</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Sora+(OpenAI)" target="_blank">▶ Demo</a><a class="cta-a" href="https://sora.com" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="A continuous plan-séquence starting from a bird&#x27;s eye view of a rooftop, camera slowly arcs downward transitioning to eye level, following a woman walking, steadicam movement, dissolve to interior scene, low key dramatic lighting, cinematic 2.39:1 aspect ratio, golden hour ambiance" title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>A continuous plan-séquence starting from a bird&#x27;s eye view of a rooftop, camera slowly arcs downward transitioning to eye level, following a woman walking, stea...</div></div><div class="vp"></div></div>
<div class="ac" data-v="klingai.com"><div class="an">8</div><div class="ai" style="background:#F0EAFF"><img decoding="async" src="https://www.google.com/s2/favicons?domain=klingai.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Kling AI</span><span class="pl" style="background:#fff8e8;color:#d4891a;padding:2px 6px;border-radius:3px;font-size:9px">Freemium</span></div><p class="ad">Génération vidéo de haute qualité avec un excellent contrôle du mouvement des sujets. Idéal pour simuler les mouvements de personnages dans une scène, utile pour valider un blocking avant tournage.</p><div class="af"><div class="at"><span class="tg" style="background:#F0EAFF;color:#7C3AED">Blocking acteurs</span><span class="tg" style="background:#F0EAFF;color:#7C3AED">Mouvement sujet</span><span class="tg" style="background:#F0EAFF;color:#7C3AED">Image-to-video</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Kling+AI" target="_blank">▶ Demo</a><a class="cta-a" href="https://klingai.com" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Two-shot scene, blocking: character A enters frame from left foreground, character B remains static in background right, camera pans slightly left to follow action, maintaining over-the-shoulder framing, eye level angle, soft fill light, natural lighting interior" title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>Two-shot scene, blocking: character A enters frame from left foreground, character B remains static in background right, camera pans slightly left to follow act...</div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">📐</span> Storyboard dédié</div>
<div class="ac" data-v="wonderunit.com"><div class="an">9</div><div class="ai" style="background:#E6F7F1"><img decoding="async" src="https://www.google.com/s2/favicons?domain=wonderunit.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Storyboarder (Wonder Unit)</span><span class="pl" style="background:#e8f5ed;color:#27ae60;padding:2px 6px;border-radius:3px;font-size:9px">Gratuit</span></div><p class="ad">Logiciel gratuit dédié au storyboard avec intégration IA. Génère automatiquement des panels à partir d'un script, avec des champs pour action, dialogue, durée et notes de réalisation.</p><div class="af"><div class="at"><span class="tg" style="background:#E6F7F1;color:#0D8C6C">Script-to-storyboard</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C">Shot list auto</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C">Export animatique</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Storyboarder+(Wonder+Unit)" target="_blank">▶ Demo</a><a class="cta-a" href="https://wonderunit.com/storyboarder" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="SCÈNE 12 — INT. BUREAU — NUIT | Plan : Gros plan (GP) | Angle : Eye level | Mouvement : Push in lent | Durée : 3s | Action : Le personnage lit une lettre, regard descendant. Lumière : Key light latérale gauche, low key, clair-obscur marqué. | Dialogue : — | SFX : Bruissement de papier V.O." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>SCÈNE 12 — INT. BUREAU — NUIT | Plan : Gros plan (GP) | Angle : Eye level | Mouvement : Push in lent | Durée : 3s | Action : Le personnage lit une lettre, regar...</div></div><div class="vp"></div></div>
<div class="ac" data-v="boords.com"><div class="an">10</div><div class="ai" style="background:#E6F7F1"><img decoding="async" src="https://www.google.com/s2/favicons?domain=boords.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Boords</span><span class="pl" style="background:#fce8e8;color:#c0392b;padding:2px 6px;border-radius:3px;font-size:9px">Payant</span></div><p class="ad">Outil de storyboard professionnel avec génération IA intégrée. Idéal pour les agences et réalisateurs. Permet de générer une séquence de panels cohérents et de les exporter en animatique avec timing.</p><div class="af"><div class="at"><span class="tg" style="background:#E6F7F1;color:#0D8C6C">Panels cohérents</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C">Export PDF/vidéo</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C">Collaboration</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C">Animatique</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Boords" target="_blank">▶ Demo</a><a class="cta-a" href="https://boords.com" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Generate storyboard sequence: Scene opens with establishing shot (plan général) of empty parking garage, cut to plan moyen of protagonist entering, then close-up (gros plan) on hand gripping car keys, smash cut to exterior establishing shot. Low key lighting throughout, hard shadows, 1.85:1 flat format." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>Generate storyboard sequence: Scene opens with establishing shot (plan général) of empty parking garage, cut to plan moyen of protagonist entering, then close-u...</div></div><div class="vp"></div></div>
<div class="ac" data-v="scenaristai.com"><div class="an">11</div><div class="ai" style="background:#E6F7F1"><img decoding="async" src="https://www.google.com/s2/favicons?domain=scenaristai.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Scenarist AI</span><span class="pl" style="background:#fff8e8;color:#d4891a;padding:2px 6px;border-radius:3px;font-size:9px">Freemium</span></div><p class="ad">Plateforme tout-en-un : écriture du script, découpage technique et génération des panels. Transforme un traitement narratif en storyboard illustré automatiquement avec numérotation des scènes et plans.</p><div class="af"><div class="at"><span class="tg" style="background:#E6F7F1;color:#0D8C6C">Script → Storyboard</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C">Découpage auto</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C">Numéro de plan</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Scenarist+AI" target="_blank">▶ Demo</a><a class="cta-a" href="https://scenaristai.com" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Generate a storyboard from this treatment: ACT I — The protagonist arrives at a deserted train station (establishing shot, wide). She notices a letter on a bench (insert, extreme close-up). She reads it, reaction shot (gros plan, eye level). BEAT: she looks up — empty platform (pan left, plan d&#x27;ensemble). Cut to black." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>Generate a storyboard from this treatment: ACT I — The protagonist arrives at a deserted train station (establishing shot, wide). She notices a letter on a benc...</div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">✍️</span> Écriture & Structure narrative</div>
<div class="ac" data-v="claude.ai"><div class="an">12</div><div class="ai" style="background:#FFF3ED"><img decoding="async" src="https://www.google.com/s2/favicons?domain=claude.ai&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Claude (Anthropic)</span><span class="pl" style="background:#fff8e8;color:#d4891a;padding:2px 6px;border-radius:3px;font-size:9px">Freemium</span></div><p class="ad">Excellent pour la structure narrative, les beat sheets et les descriptions de plans. Génère des découpages techniques précis, des dialogues et des notes de réalisation détaillées. Idéal pour construire la colonne vertébrale d'un storyboard.</p><div class="af"><div class="at"><span class="tg" style="background:#FFF3ED;color:#CC4400">Beat sheet</span><span class="tg" style="background:#FFF3ED;color:#CC4400">Découpage technique</span><span class="tg" style="background:#FFF3ED;color:#CC4400">Logline</span><span class="tg" style="background:#FFF3ED;color:#CC4400">Notes de réalisation</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Claude+(Anthropic)" target="_blank">▶ Demo</a><a class="cta-a" href="https://claude.ai" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Crée un découpage technique storyboard pour la séquence du climax (Acte III) d&#x27;un thriller urbain. Pour chaque plan indique : numéro de plan, type de plan (GP/PM/PA/PE), angle (plongée/contre-plongée/eye level), mouvement caméra (travelling/pan/dolly), transition vers le plan suivant (coupe franche/match cut/dissolve), durée estimée, et une brève description de l&#x27;action et du dialogue. Ambiance : low key, clair-obscur, 2.39:1." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>Crée un découpage technique storyboard pour la séquence du climax (Acte III) d&#x27;un thriller urbain. Pour chaque plan indique : numéro de plan, type de plan (GP/P...</div></div><div class="vp"></div></div>
<div class="ac" data-v="chatgpt.com"><div class="an">13</div><div class="ai" style="background:#FFF3ED"><img decoding="async" src="https://www.google.com/s2/favicons?domain=chatgpt.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">ChatGPT (OpenAI)</span><span class="pl" style="background:#fff8e8;color:#d4891a;padding:2px 6px;border-radius:3px;font-size:9px">Freemium</span></div><p class="ad">Fort en génération de synopses, loglines et arcs narratifs. Utile pour développer la structure en 3 actes, générer des beat sheets et rédiger des notes de plateau. Compatible avec DALL·E pour une chaîne texte → image.</p><div class="af"><div class="at"><span class="tg" style="background:#FFF3ED;color:#CC4400">Synopsis</span><span class="tg" style="background:#FFF3ED;color:#CC4400">Arc narratif</span><span class="tg" style="background:#FFF3ED;color:#CC4400">Beat sheet</span><span class="tg" style="background:#FFF3ED;color:#CC4400">Chaîne texte→image</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=ChatGPT+(OpenAI)" target="_blank">▶ Demo</a><a class="cta-a" href="https://chatgpt.com" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Rédige un beat sheet en 12 points pour un court-métrage de 15 minutes (genre : drame psychologique). Identifie : l&#x27;exposition, le turning point de l&#x27;Acte I, la montée en tension, le nœud dramatique central, le climax et la résolution. Pour chaque beat, suggère un type de plan dominant (plan général, gros plan, etc.) et une ambiance lumineuse (high key / low key)." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>Rédige un beat sheet en 12 points pour un court-métrage de 15 minutes (genre : drame psychologique). Identifie : l&#x27;exposition, le turning point de l&#x27;Acte I, la ...</div></div><div class="vp"></div></div>
<div class="ac" data-v="gemini.google.com"><div class="an">14</div><div class="ai" style="background:#FFF3ED"><img decoding="async" src="https://www.google.com/s2/favicons?domain=gemini.google.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Gemini (Google)</span><span class="pl" style="background:#fff8e8;color:#d4891a;padding:2px 6px;border-radius:3px;font-size:9px">Freemium</span></div><p class="ad">Capable d'analyser une image et de décrire techniquement un plan (type de plan, angle, lumière) — très utile pour reverse-engineer des références visuelles et les adapter à son storyboard. Multimodal natif.</p><div class="af"><div class="at"><span class="tg" style="background:#FFF3ED;color:#CC4400">Analyse image</span><span class="tg" style="background:#FFF3ED;color:#CC4400">Référence visuelle</span><span class="tg" style="background:#FFF3ED;color:#CC4400">Multimodal</span><span class="tg" style="background:#FFF3ED;color:#CC4400">Description de plan</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Gemini+(Google)" target="_blank">▶ Demo</a><a class="cta-a" href="https://gemini.google.com" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Analyse this reference image and describe it using storyboard technical vocabulary: identify the shot type (plan général/moyen/rapproché/gros plan), camera angle (plongée/contre-plongée/eye level/dutch angle), lighting setup (high key/low key, key light direction, fill light, rim light), aspect ratio, and suggest a camera movement (pan/tilt/dolly/steadicam) that would work for the next shot." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>Analyse this reference image and describe it using storyboard technical vocabulary: identify the shot type (plan général/moyen/rapproché/gros plan), camera angl...</div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">🎯</span> Cohérence visuelle</div>
<div class="ac" data-v="leonardo.ai"><div class="an">15</div><div class="ai" style="background:#FEF3C7"><img decoding="async" src="https://www.google.com/s2/favicons?domain=leonardo.ai&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Leonardo.Ai</span><span class="pl" style="background:#fff8e8;color:#d4891a;padding:2px 6px;border-radius:3px;font-size:9px">Freemium</span></div><p class="ad">Spécialisé dans la cohérence de personnages entre les panels. Fonctionnalité « Character Reference » pour garder le même visage/costume sur tous les plans. Indispensable pour les storyboards avec protagonistes récurrents.</p><div class="af"><div class="at"><span class="tg" style="background:#FEF3C7;color:#D97706">Cohérence personnage</span><span class="tg" style="background:#FEF3C7;color:#D97706">Character reference</span><span class="tg" style="background:#FEF3C7;color:#D97706">Angles multiples</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Leonardo.Ai" target="_blank">▶ Demo</a><a class="cta-a" href="https://leonardo.ai" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="[Character reference attached] Same character, storyboard panel sequence: Panel 1 — plan moyen, profil gauche, eye level, natural soft light. Panel 2 — gros plan, three-quarter view, contre-plongée, hard key light from right creating clair-obscur. Panel 3 — très gros plan eyes only, eye level, rim light. Consistent art style, cinematic 16:9." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>[Character reference attached] Same character, storyboard panel sequence: Panel 1 — plan moyen, profil gauche, eye level, natural soft light. Panel 2 — gros pla...</div></div><div class="vp"></div></div>
<div class="ac" data-v="firefly.adobe.com"><div class="an">16</div><div class="ai" style="background:#FEF3C7"><img decoding="async" src="https://www.google.com/s2/favicons?domain=firefly.adobe.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Adobe Firefly</span><span class="pl" style="background:#fff8e8;color:#d4891a;padding:2px 6px;border-radius:3px;font-size:9px">Freemium</span></div><p class="ad">Intégré à Photoshop et Premiere. Génère et retouche des panels directement dans le workflow Adobe. La fonction Generative Fill permet d'ajuster un cadrage, ajouter un décor ou modifier une composition après génération.</p><div class="af"><div class="at"><span class="tg" style="background:#FEF3C7;color:#D97706">Workflow Adobe</span><span class="tg" style="background:#FEF3C7;color:#D97706">Retouche panel</span><span class="tg" style="background:#FEF3C7;color:#D97706">Générative Fill</span><span class="tg" style="background:#FEF3C7;color:#D97706">Décor</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Adobe+Firefly" target="_blank">▶ Demo</a><a class="cta-a" href="https://firefly.adobe.com" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Extend this storyboard panel to cinémascope 2.39:1 ratio by generating background on both sides. Maintain: low key lighting, hard light source from left, clair-obscur atmosphere, industrial interior décor, night time, consistent with existing foreground elements." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>Extend this storyboard panel to cinémascope 2.39:1 ratio by generating background on both sides. Maintain: low key lighting, hard light source from left, clair-...</div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">🎵</span> Son, V.O. & Soundtrack</div>
<div class="ac" data-v="elevenlabs.io"><div class="an">17</div><div class="ai" style="background:#FFE8F0"><img decoding="async" src="https://www.google.com/s2/favicons?domain=elevenlabs.io&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">ElevenLabs</span><span class="pl" style="background:#fff8e8;color:#d4891a;padding:2px 6px;border-radius:3px;font-size:9px">Freemium</span></div><p class="ad">Génération de voix off (V.O.) ultra-réalistes à partir de texte. Permet d'ajouter des dialogues et narrations à une animatique pour tester le rythme des plans et la durée des séquences avant tournage.</p><div class="af"><div class="at"><span class="tg" style="background:#FFE8F0;color:#BE123C">Voix off (V.O.)</span><span class="tg" style="background:#FFE8F0;color:#BE123C">Dialogue</span><span class="tg" style="background:#FFE8F0;color:#BE123C">Test de rythme</span><span class="tg" style="background:#FFE8F0;color:#BE123C">Durée des plans</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=ElevenLabs" target="_blank">▶ Demo</a><a class="cta-a" href="https://elevenlabs.io" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="[V.O. Narrateur — Ton : grave, posé, 90 mots/min] « La ville ne dort jamais. Mais cette nuit-là, quelque chose avait changé. » → Durée cible : 4 secondes → correspond à un plan d&#x27;ensemble avec panoramique lent. [SFX à ajouter : bruit de circulation lointain, vent]" title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>[V.O. Narrateur — Ton : grave, posé, 90 mots/min] « La ville ne dort jamais. Mais cette nuit-là, quelque chose avait changé. » → Durée cible : 4 secondes → corr...</div></div><div class="vp"></div></div>
<div class="ac" data-v="suno.com"><div class="an">18</div><div class="ai" style="background:#FFE8F0"><img decoding="async" src="https://www.google.com/s2/favicons?domain=suno.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Suno AI</span><span class="pl" style="background:#fff8e8;color:#d4891a;padding:2px 6px;border-radius:3px;font-size:9px">Freemium</span></div><p class="ad">Génération musicale à partir d'un prompt textuel. Crée une bande sonore temporaire pour accompagner l'animatique, tester l'émotion de chaque séquence et valider le rythme des transitions avant la production finale.</p><div class="af"><div class="at"><span class="tg" style="background:#FFE8F0;color:#BE123C">Bande son temp</span><span class="tg" style="background:#FFE8F0;color:#BE123C">Ambiance séquence</span><span class="tg" style="background:#FFE8F0;color:#BE123C">Rythme transition</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Suno+AI" target="_blank">▶ Demo</a><a class="cta-a" href="https://suno.com" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Cinematic score for a thriller storyboard sequence. Act II rising tension, building from sparse low-key piano to intense orchestral swell reaching climax at 1min20. Tempo matches smash cut editing rhythm. Dark, suspenseful, Hans Zimmer style. No lyrics. Duration: 2 minutes." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>Cinematic score for a thriller storyboard sequence. Act II rising tension, building from sparse low-key piano to intense orchestral swell reaching climax at 1mi...</div></div><div class="vp"></div></div>
<div class="ac" data-v="elevenlabs.io"><div class="an">19</div><div class="ai" style="background:#FFE8F0"><img decoding="async" src="https://www.google.com/s2/favicons?domain=elevenlabs.io&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">ElevenLabs SFX</span><span class="pl" style="background:#fff8e8;color:#d4891a;padding:2px 6px;border-radius:3px;font-size:9px">Freemium</span></div><p class="ad">Génération de SFX (effets sonores) depuis un prompt. Utile pour enrichir l'animatique avec des sons d'ambiance correspondant aux décors et actions de chaque panel — bruitage, atmosphères, impacts.</p><div class="af"><div class="at"><span class="tg" style="background:#FFE8F0;color:#BE123C">SFX</span><span class="tg" style="background:#FFE8F0;color:#BE123C">Ambiance décor</span><span class="tg" style="background:#FFE8F0;color:#BE123C">Bruitage</span><span class="tg" style="background:#FFE8F0;color:#BE123C">Animatique sonore</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=ElevenLabs+SFX" target="_blank">▶ Demo</a><a class="cta-a" href="https://elevenlabs.io/sound-effects" target="_blank">Accéder →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="SFX for storyboard panel: establishing shot of rainy city alley, night scene. Sound: distant traffic, rain on pavement, dripping water from rooftop, occasional distant siren. Duration: 6 seconds, seamlessly loopable, low key urban ambiance." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT · clic = copier</span>SFX for storyboard panel: establishing shot of rainy city alley, night scene. Sound: distant traffic, rain on pavement, dripping water from rooftop, occasional ...</div></div><div class="vp"></div></div>
<div class="ac" data-v="github.com"><div class="an">20</div><div class="ai" style="background:#FEF3C7"><img decoding="async" src="https://www.google.com/s2/favicons?domain=github.com&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Fooocus</span><span class="pl" style="background:#e8f5ed;color:#27ae60;padding:2px 6px;border-radius:3px;font-size:9px">Gratuit</span></div><p class="ad">Interface simplifiée pour Stable Diffusion / Flux. Génération d'images de haute qualité sans configuration complexe. ControlNet et IP-Adapter intégrés pour la cohérence visuelle entre panels.</p><div class="af"><div class="at"><span class="tg" style="background:#FEF3C7;color:#D97706">Workflow simplifié</span><span class="tg" style="background:#FEF3C7;color:#D97706">Cohérence totale</span><span class="tg" style="background:#FEF3C7;color:#D97706">ControlNet</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=Fooocus+tutorial" target="_blank">▶ Demo</a><a class="cta-a" href="https://github.com/lllyasviel/Fooocus" target="_blank">Accéder →</a></div></div></div><div class="vp"></div></div>


</div>
<div class="sb-panel" id="sb-glossaire">
<div class="cat"><span class="cat-icon">📖</span> Structure du récit</div>
<div class="ac"><div class="an">01</div><div class="ai" style="background:#FFF3ED"><span style="font-size:20px">📖</span></div><div class="ab"><div class="ah"><span class="nm">Structure du récit</span><span class="pl">17 termes</span></div><p class="ad">Cliquez sur un terme pour le copier dans vos prompts IA.</p><div class="af"><div class="at" style="flex-wrap:wrap"><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Acte I');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Acte I</span><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Acte II');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Acte II</span><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Acte III');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Acte III</span><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Arc narratif');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Arc narratif</span><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Beat');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Beat</span><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Beat sheet');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Beat sheet</span><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Climax');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Climax</span><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Dénouement');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Dénouement</span><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Exposition');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Exposition</span><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Intrigue');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Intrigue</span><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Logline');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Logline</span><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Montée en tension');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Montée en tension</span><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Nœud dramatique');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Nœud dramatique</span><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Point de bascule');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Point de bascule</span><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Prologue');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Prologue</span><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Résolution');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Résolution</span><span class="tg" style="background:#FFF3ED;color:#CC4400;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Scène-clé');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Scène-clé</span></div></div></div></div>
<div class="cat"><span class="cat-icon">🎥</span> Cadrage & Plans</div>
<div class="ac"><div class="an">02</div><div class="ai" style="background:#E8F0FE"><span style="font-size:20px">🎥</span></div><div class="ab"><div class="ah"><span class="nm">Cadrage & Plans</span><span class="pl">14 termes</span></div><p class="ad">Cliquez sur un terme pour le copier dans vos prompts IA.</p><div class="af"><div class="at" style="flex-wrap:wrap"><span class="tg" style="background:#E8F0FE;color:#1A73E8;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Amorce');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Amorce</span><span class="tg" style="background:#E8F0FE;color:#1A73E8;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Cadre');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Cadre</span><span class="tg" style="background:#E8F0FE;color:#1A73E8;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Champ');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Champ</span><span class="tg" style="background:#E8F0FE;color:#1A73E8;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Contre-champ');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Contre-champ</span><span class="tg" style="background:#E8F0FE;color:#1A73E8;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Establishing shot');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Establishing shot</span><span class="tg" style="background:#E8F0FE;color:#1A73E8;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Gros plan (GP)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Gros plan (GP)</span><span class="tg" style="background:#E8F0FE;color:#1A73E8;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Hors-champ');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Hors-champ</span><span class="tg" style="background:#E8F0FE;color:#1A73E8;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Insert');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Insert</span><span class="tg" style="background:#E8F0FE;color:#1A73E8;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Over-the-shoulder (OTS)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Over-the-shoulder (OTS)</span><span class="tg" style="background:#E8F0FE;color:#1A73E8;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Plan américain (PA)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Plan américain (PA)</span><span class="tg" style="background:#E8F0FE;color:#1A73E8;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Plan densemble (PE)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Plan d'ensemble (PE)</span><span class="tg" style="background:#E8F0FE;color:#1A73E8;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Plan général (PG)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Plan général (PG)</span><span class="tg" style="background:#E8F0FE;color:#1A73E8;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Plan moyen (PM)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Plan moyen (PM)</span><span class="tg" style="background:#E8F0FE;color:#1A73E8;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Plan poitrine (PP)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Plan poitrine (PP)</span></div></div></div></div>
<div class="cat"><span class="cat-icon">📐</span> Angles & Axes de caméra</div>
<div class="ac"><div class="an">03</div><div class="ai" style="background:#F0EAFF"><span style="font-size:20px">📐</span></div><div class="ab"><div class="ah"><span class="nm">Angles & Axes de caméra</span><span class="pl">9 termes</span></div><p class="ad">Cliquez sur un terme pour le copier dans vos prompts IA.</p><div class="af"><div class="at" style="flex-wrap:wrap"><span class="tg" style="background:#F0EAFF;color:#7C3AED;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Angle dattaque');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Angle d'attaque</span><span class="tg" style="background:#F0EAFF;color:#7C3AED;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Birds eye view');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Bird's eye view</span><span class="tg" style="background:#F0EAFF;color:#7C3AED;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Caméra subjective');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Caméra subjective</span><span class="tg" style="background:#F0EAFF;color:#7C3AED;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Contre-plongée');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Contre-plongée</span><span class="tg" style="background:#F0EAFF;color:#7C3AED;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Dutch angle');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Dutch angle</span><span class="tg" style="background:#F0EAFF;color:#7C3AED;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Eye level');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Eye level</span><span class="tg" style="background:#F0EAFF;color:#7C3AED;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Plongée');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Plongée</span><span class="tg" style="background:#F0EAFF;color:#7C3AED;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Profil');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Profil</span><span class="tg" style="background:#F0EAFF;color:#7C3AED;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Top shot');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Top shot</span></div></div></div></div>
<div class="cat"><span class="cat-icon">🎬</span> Mouvements de caméra</div>
<div class="ac"><div class="an">04</div><div class="ai" style="background:#E6F7F1"><span style="font-size:20px">🎬</span></div><div class="ab"><div class="ah"><span class="nm">Mouvements de caméra</span><span class="pl">12 termes</span></div><p class="ad">Cliquez sur un terme pour le copier dans vos prompts IA.</p><div class="af"><div class="at" style="flex-wrap:wrap"><span class="tg" style="background:#E6F7F1;color:#0D8C6C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Arc');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Arc</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Caméra portée');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Caméra portée</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Dolly in');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Dolly in</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Dolly out');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Dolly out</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Filé (swish pan)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Filé (swish pan)</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Panoramique (pan)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Panoramique (pan)</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Plan-séquence');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Plan-séquence</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Push in');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Push in</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Rack focus');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Rack focus</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Steadicam');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Steadicam</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Tilt down');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Tilt down</span><span class="tg" style="background:#E6F7F1;color:#0D8C6C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Tilt up');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Tilt up</span></div></div></div></div>
<div class="cat"><span class="cat-icon">✂️</span> Transitions</div>
<div class="ac"><div class="an">05</div><div class="ai" style="background:#FEF3C7"><span style="font-size:20px">✂️</span></div><div class="ab"><div class="ah"><span class="nm">Transitions</span><span class="pl">9 termes</span></div><p class="ad">Cliquez sur un terme pour le copier dans vos prompts IA.</p><div class="af"><div class="at" style="flex-wrap:wrap"><span class="tg" style="background:#FEF3C7;color:#D97706;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Coupe franche');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Coupe franche</span><span class="tg" style="background:#FEF3C7;color:#D97706;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Cut to black');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Cut to black</span><span class="tg" style="background:#FEF3C7;color:#D97706;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Dissolve (fondu enchaîné)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Dissolve (fondu enchaîné)</span><span class="tg" style="background:#FEF3C7;color:#D97706;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Ellipse');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Ellipse</span><span class="tg" style="background:#FEF3C7;color:#D97706;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Fade in');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Fade in</span><span class="tg" style="background:#FEF3C7;color:#D97706;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Fade out');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Fade out</span><span class="tg" style="background:#FEF3C7;color:#D97706;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Flash cut');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Flash cut</span><span class="tg" style="background:#FEF3C7;color:#D97706;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Jump cut');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Jump cut</span><span class="tg" style="background:#FEF3C7;color:#D97706;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Match cut');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Match cut</span></div></div></div></div>
<div class="cat"><span class="cat-icon">🎭</span> Composition & Mise en scène</div>
<div class="ac"><div class="an">06</div><div class="ai" style="background:#FFE8F0"><span style="font-size:20px">🎭</span></div><div class="ab"><div class="ah"><span class="nm">Composition & Mise en scène</span><span class="pl">9 termes</span></div><p class="ad">Cliquez sur un terme pour le copier dans vos prompts IA.</p><div class="af"><div class="at" style="flex-wrap:wrap"><span class="tg" style="background:#FFE8F0;color:#BE123C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Action line (axe des 180°)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Action line (axe des 180°)</span><span class="tg" style="background:#FFE8F0;color:#BE123C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Background (arrière-plan)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Background (arrière-plan)</span><span class="tg" style="background:#FFE8F0;color:#BE123C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Blocking (placement des acteurs)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Blocking (placement des acteurs)</span><span class="tg" style="background:#FFE8F0;color:#BE123C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Foreground (premier plan)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Foreground (premier plan)</span><span class="tg" style="background:#FFE8F0;color:#BE123C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Golden ratio');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Golden ratio</span><span class="tg" style="background:#FFE8F0;color:#BE123C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Lead room');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Lead room</span><span class="tg" style="background:#FFE8F0;color:#BE123C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Lignes de force');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Lignes de force</span><span class="tg" style="background:#FFE8F0;color:#BE123C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Look room (nose room)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Look room (nose room)</span><span class="tg" style="background:#FFE8F0;color:#BE123C;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Profondeur de champ');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Profondeur de champ</span></div></div></div></div>
<div class="cat"><span class="cat-icon">💡</span> Lumière & Ambiance</div>
<div class="ac"><div class="an">07</div><div class="ai" style="background:#E5F9FA"><span style="font-size:20px">💡</span></div><div class="ab"><div class="ah"><span class="nm">Lumière & Ambiance</span><span class="pl">9 termes</span></div><p class="ad">Cliquez sur un terme pour le copier dans vos prompts IA.</p><div class="af"><div class="at" style="flex-wrap:wrap"><span class="tg" style="background:#E5F9FA;color:#00A5AB;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Contre-jour');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Contre-jour</span><span class="tg" style="background:#E5F9FA;color:#00A5AB;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Clair-obscur');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Clair-obscur</span><span class="tg" style="background:#E5F9FA;color:#00A5AB;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Fill light');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Fill light</span><span class="tg" style="background:#E5F9FA;color:#00A5AB;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Hard light');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Hard light</span><span class="tg" style="background:#E5F9FA;color:#00A5AB;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('High key');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">High key</span><span class="tg" style="background:#E5F9FA;color:#00A5AB;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Key light');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Key light</span><span class="tg" style="background:#E5F9FA;color:#00A5AB;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Low key');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Low key</span><span class="tg" style="background:#E5F9FA;color:#00A5AB;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Lumière naturelle');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Lumière naturelle</span><span class="tg" style="background:#E5F9FA;color:#00A5AB;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Rim light');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Rim light</span></div></div></div></div>
<div class="cat"><span class="cat-icon">📝</span> Annotations & Format</div>
<div class="ac"><div class="an">08</div><div class="ai" style="background:#E6F2FF"><span style="font-size:20px">📝</span></div><div class="ab"><div class="ah"><span class="nm">Annotations & Format</span><span class="pl">12 termes</span></div><p class="ad">Cliquez sur un terme pour le copier dans vos prompts IA.</p><div class="af"><div class="at" style="flex-wrap:wrap"><span class="tg" style="background:#E6F2FF;color:#005EA6;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Action');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Action</span><span class="tg" style="background:#E6F2FF;color:#005EA6;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Animatique');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Animatique</span><span class="tg" style="background:#E6F2FF;color:#005EA6;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Case (panel)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Case (panel)</span><span class="tg" style="background:#E6F2FF;color:#005EA6;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Décor');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Décor</span><span class="tg" style="background:#E6F2FF;color:#005EA6;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Description de plan');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Description de plan</span><span class="tg" style="background:#E6F2FF;color:#005EA6;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Dialogue');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Dialogue</span><span class="tg" style="background:#E6F2FF;color:#005EA6;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Durée');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Durée</span><span class="tg" style="background:#E6F2FF;color:#005EA6;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Flèche de mouvement');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Flèche de mouvement</span><span class="tg" style="background:#E6F2FF;color:#005EA6;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Miniature (thumbnail)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Miniature (thumbnail)</span><span class="tg" style="background:#E6F2FF;color:#005EA6;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Note de réalisation');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Note de réalisation</span><span class="tg" style="background:#E6F2FF;color:#005EA6;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Numéro de plan');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Numéro de plan</span><span class="tg" style="background:#E6F2FF;color:#005EA6;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Numéro de scène');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Numéro de scène</span></div></div></div></div>
<div class="cat"><span class="cat-icon">📏</span> Formats & Ratio</div>
<div class="ac"><div class="an">09</div><div class="ai" style="background:#F5F0FF"><span style="font-size:20px">📏</span></div><div class="ab"><div class="ah"><span class="nm">Formats & Ratio</span><span class="pl">9 termes</span></div><p class="ad">Cliquez sur un terme pour le copier dans vos prompts IA.</p><div class="af"><div class="at" style="flex-wrap:wrap"><span class="tg" style="background:#F5F0FF;color:#6D28D9;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('1.33:1 (4:3)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">1.33:1 (4:3)</span><span class="tg" style="background:#F5F0FF;color:#6D28D9;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('1.78:1 (16:9)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">1.78:1 (16:9)</span><span class="tg" style="background:#F5F0FF;color:#6D28D9;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('1.85:1 (Flat)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">1.85:1 (Flat)</span><span class="tg" style="background:#F5F0FF;color:#6D28D9;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('2.35:1 (Scope)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">2.35:1 (Scope)</span><span class="tg" style="background:#F5F0FF;color:#6D28D9;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('2.39:1 (Cinémascope)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">2.39:1 (Cinémascope)</span><span class="tg" style="background:#F5F0FF;color:#6D28D9;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Format carré (1:1)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Format carré (1:1)</span><span class="tg" style="background:#F5F0FF;color:#6D28D9;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Format vertical (9:16)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Format vertical (9:16)</span><span class="tg" style="background:#F5F0FF;color:#6D28D9;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Letterbox');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Letterbox</span><span class="tg" style="background:#F5F0FF;color:#6D28D9;cursor:pointer;margin:2px" onclick="event.stopPropagation();navigator.clipboard.writeText('Masque (matte)');this.style.outline='2px solid #10B981';setTimeout(()=>this.style.outline='',800)">Masque (matte)</span></div></div></div></div>

</div>
</section>
<div class="print-contact" id="pcd-print-contact">
<img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/2026/03/abstract-gradient-background-Formation-page@1-2048x1111abstract-gradient-background-Formation_PresentComposedesign_footer.png" class="pc-bg" alt="">
<div class="pc-inner">
<a href="https://presentcomposedesign.fr" target="_blank" class="pc-logo"><img decoding="async" src="https://presentcomposedesign.fr/wp-content/uploads/2025/07/Present-Compose-design_logo-image.svg" alt="PCD"></a>
<div class="pc-info">
<div class="pc-name">Alban Desbarax · Présent Composé Design</div>
<div class="pc-role">Designer 360° · Directeur Artistique · Enseignant · Toulouse</div>
<div class="pc-phrase">Ma sélection d'outils et applications préconisés pour les formations universitaires et professionnelles dans les secteurs créatifs</div>
<div class="pc-links">
<a href="https://fr.linkedin.com/in/alban-desbarax-present-compose-design" target="_blank" class="pc-li" title="LinkedIn">in</a>
<a href="#" onclick="window.location=String.fromCharCode(109,97,105,108,116,111,58)+String.fromCharCode(99,111,110,116,97,99,116,64)+String.fromCharCode(112,114,101,115,101,110,116,99,111,109,112,111,115,101,100,101,115,105,103,110,46,102,114);return false" class="pc-email">&#9993; <span class="eml"></span></a>
<a href="tel:+33683949667">&#9742; +33 6 83 94 96 67</a>
<a href="https://buymeacoffee.com/presentcomposedesign" class="pc-thx" target="_blank">&#9829; Remercier / Soutenir</a>
<a href="https://presentcomposedesign.fr/contact/" class="pc-cta" target="_blank">&#8599; Mettre en place une formation</a>
</div>
</div>
</div>
</div>
<div class="print-footer"><div class="pf-b"><a href="https://presentcomposedesign.fr" target="_blank">Présent Composé Design</a> / Alban Desbarax</div><div class="pf-i">12, bis rue Louis Plana 31500 Toulouse · +33 (0) 683 949 667 · <span class="eml"></span> · SIREN : 789 583 242 000 35</div></div>
<section class="guide" id="pcd-guide-vc" style="display:none">
<div class="guide-back" onclick="showLanding()">← Retour aux formations</div>
<div class="sl">⚡ CLAUDE VIBECODING — Guide complet</div>
<div class="cat"><span class="cat-icon">🌊</span> C'est quoi le Vibecoding ?</div>
<div class="ac star"><div class="an">★</div><div class="ai" style="background:#F3EEFF"><span style="font-size:22px">🌊</span></div><div class="ab"><div class="ah"><span class="nm">Le concept — Définition</span><span class="pl" style="background:#D8C5FF;color:#4C1D95;padding:2px 6px;border-radius:3px;font-size:9px">Essentiel</span></div><p class="ad">Terme popularisé par Andrej Karpathy (ex-OpenAI, Tesla AI) en 2025. Vibecoder c'est <strong>déléguer l'écriture du code à Claude</strong> en pilotant par l'intention et le résultat visuel — sans lire chaque ligne. On décrit, Claude génère, on teste, on itère. La machine code, toi tu diriges.</p><div class="rulebox"><strong>La boucle du vibecoding</strong><ul><li><strong>Tu décris</strong> ce que tu veux voir / ce que tu veux que ça fasse.</li><li><strong>Claude génère</strong> — tu testes dans le navigateur ou VS Code.</li><li><strong>Si ça marche</strong> → tu construis dessus. <strong>Si ça casse</strong> → tu colles l'erreur et tu itères.</li><li>Tu n'as pas besoin de tout comprendre — mais tu dois <strong>valider</strong> chaque étape avant de continuer.</li></ul></div></div><div class="vp"></div></div>
<div class="ac"><div class="an">01</div><div class="ai" style="background:#F3EEFF"><span style="font-size:22px">🧠</span></div><div class="ab"><div class="ah"><span class="nm">Mindset — Ce qu'il faut accepter</span></div><p class="ad">Le vibecoding demande un changement de posture : tu passes de <em>«&nbsp;je code&nbsp;»</em> à <em>«&nbsp;je dirige un développeur qui ne dort jamais&nbsp;»</em>. Ce n'est pas parce que tu ne comprends pas une ligne que c'est mal écrit. Et ce n'est pas parce que Claude génère que le code est bon — <strong>c'est toi le chef de projet</strong>.</p><div class="af"><div class="at"><span class="tg" style="background:#F3EEFF;color:#7C3AED">Itération rapide</span><span class="tg" style="background:#F3EEFF;color:#7C3AED">Intention &gt; syntaxe</span><span class="tg" style="background:#F3EEFF;color:#7C3AED">Test &amp; correction</span><span class="tg" style="background:#F3EEFF;color:#7C3AED">Tu valides</span></div></div><div class="rulebox"><strong>Les 3 règles du bon vibecoder</strong><ul><li>Toujours <strong>tester avant de continuer</strong> — ne jamais empiler du code non vérifié.</li><li><strong>Committer ou sauvegarder</strong> avant chaque grosse intervention de Claude.</li><li>Si Claude tourne en rond après 3 tentatives → <strong>reformuler ou changer d'approche</strong>.</li></ul></div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">🌐</span> Claude.ai — Dans le navigateur</div>
<div class="ac"><div class="an">02</div><div class="ai" style="background:#F0F0FF"><img decoding="async" src="https://www.google.com/s2/favicons?domain=claude.ai&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">claude.ai — Les 3 fonctions clés</span><span class="pl" style="background:#E8E5FF;color:#1001EF;padding:2px 6px;border-radius:3px;font-size:9px">Freemium</span></div><p class="ad">La plateforme chat officielle. Trois fonctions à activer pour vibecoder efficacement : <strong>Projects</strong> (contexte persistant entre sessions), <strong>Artifacts</strong> (code HTML/JSX prévisualisable en live dans le panneau droit), et la <strong>recherche web</strong> pour les docs de frameworks récents. Demander explicitement un Artifact — Claude ne le fait pas toujours seul.</p><div class="af"><div class="at"><span class="tg" style="background:#E8E5FF;color:#1001EF">Projects</span><span class="tg" style="background:#E8E5FF;color:#1001EF">Artifacts</span><span class="tg" style="background:#E8E5FF;color:#1001EF">Recherche web</span><span class="tg" style="background:#E8E5FF;color:#1001EF">Mémoire contexte</span></div><div class="ctas"><a class="cta-a" href="https://claude.ai" target="_blank">Accéder →</a></div></div><div class="rulebox"><strong>Artifacts — comment en tirer le maximum</strong><ul><li>Préciser dans le prompt : <em>«&nbsp;génère le résultat dans un Artifact HTML complet et fonctionnel&nbsp;»</em>.</li><li>Artifact HTML = s'affiche et s'exécute en direct → parfait pour prototyper une interface ou une animation.</li><li>Artifact React/JSX → composants interactifs prévisualisés sans aucun setup local.</li><li>Le bouton <strong>Edit</strong> dans l'Artifact permet à Claude de modifier le code sans toucher à la conversation.</li></ul></div></div><div class="vp"></div></div>
<div class="ac"><div class="an">03</div><div class="ai" style="background:#F0F0FF"><img decoding="async" src="https://www.google.com/s2/favicons?domain=claude.ai&sz=64" alt="" onerror="this.style.display='none'"></div><div class="ab"><div class="ah"><span class="nm">Projects — Contexte persistant entre sessions</span><span class="pl" style="background:#E8E5FF;color:#1001EF;padding:2px 6px;border-radius:3px;font-size:9px">Pro</span></div><p class="ad">Les Projects stockent un contexte permanent : charte graphique, stack technique, règles de code, conventions de nommage. Claude s'en souvient à chaque nouvelle conversation dans ce projet — sans avoir à tout réexpliquer à chaque session.</p><div class="af"><div class="at"><span class="tg" style="background:#E8E5FF;color:#1001EF">Instructions permanentes</span><span class="tg" style="background:#E8E5FF;color:#1001EF">Multi-sessions</span><span class="tg" style="background:#E8E5FF;color:#1001EF">Fichiers de référence</span></div><div class="ctas"><a class="cta-a" href="https://claude.ai/projects" target="_blank">Ouvrir →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="[INSTRUCTIONS PROJECT] Stack : HTML/CSS vanilla/JS ES6+. Style : variables CSS dans :root, BEM pour les classes, mobile-first. Code : commenté en français. Réponses : Artifact HTML complet. Avant toute modification : rappeler ce qui existait, ce qui change et pourquoi." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">TEMPLATE INSTRUCTIONS PROJECT · clic = copier</span>[INSTRUCTIONS PROJECT] Stack : HTML/CSS vanilla/JS ES6+. Style : variables CSS :root, BEM, mobile-first. Code : commenté en français. Réponses : Artifact HTML complet. Avant toute modification : rappeler ce qui existait…</div></div><div class="vp"></div></div>
<div class="ac"><div class="an">04</div><div class="ai" style="background:#F3EEFF"><span style="font-size:22px">🎯</span></div><div class="ab"><div class="ah"><span class="nm">Structure d'un bon prompt de code</span></div><p class="ad">Un prompt efficace n'est pas une phrase — c'est une <strong>mini-spec</strong> : <em>quoi</em> (la tâche), <em>comment</em> (contraintes techniques), <em>format</em> (ce qu'on attend en sortie), <em>ne pas faire</em> (erreurs à éviter). Plus c'est précis, moins il y a d'itérations.</p><div class="af"><div class="at"><span class="tg" style="background:#F3EEFF;color:#7C3AED">Rôle + Contexte</span><span class="tg" style="background:#F3EEFF;color:#7C3AED">Contraintes tech</span><span class="tg" style="background:#F3EEFF;color:#7C3AED">Format de sortie</span><span class="tg" style="background:#F3EEFF;color:#7C3AED">Interdictions</span></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Tu es un développeur frontend expert en HTML/CSS vanilla et JavaScript ES6+. Tâche : Crée [DÉCRIRE ICI]. Contraintes : CSS pur, variables CSS dans :root, mobile-first, pas de librairies. Format : Artifact HTML unique, autonome, fonctionnel, commenté en français. Ne pas faire : animations inutiles, JS si CSS suffit, !important sauf exception justifiée." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">TEMPLATE PROMPT DÉMARRAGE · clic = copier</span>Tu es un développeur frontend expert HTML/CSS vanilla JS ES6+. Tâche : [DÉCRIRE]. Contraintes : CSS pur, variables :root, mobile-first. Format : Artifact HTML complet commenté. Ne pas faire : animations inutiles, JS si CSS suffit…</div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">💻</span> Claude Code — VS Code &amp; Terminal</div>
<div class="ac"><div class="an">05</div><div class="ai" style="background:#1A1A2E"><img decoding="async" src="https://www.google.com/s2/favicons?domain=code.visualstudio.com&sz=64" alt="" onerror="this.style.display='none'" style="filter:brightness(0) invert(1);width:26px;height:26px"></div><div class="ab"><div class="ah"><span class="nm">Claude Code — Installation &amp; commandes</span><span class="pl" style="background:#E8E5FF;color:#1001EF;padding:2px 6px;border-radius:3px;font-size:9px">Pro</span></div><p class="ad">Agent CLI qui s'intègre dans le terminal et dans VS Code. Il peut <strong>lire, écrire, renommer et exécuter</strong> des fichiers de façon autonome. Installation via npm — nécessite un compte Claude Pro ou un accès API Anthropic.</p><div class="af"><div class="at"><span class="tg" style="background:#F3EEFF;color:#7C3AED">CLI + VS Code</span><span class="tg" style="background:#F3EEFF;color:#7C3AED">Agentic</span><span class="tg" style="background:#F3EEFF;color:#7C3AED">Lire / Écrire / Exécuter</span></div><div class="ctas"><a class="cta-d" href="https://www.youtube.com/results?search_query=claude+code+vscode+2025" target="_blank">▶ Demo</a><a class="cta-a" href="https://docs.anthropic.com/fr/docs/claude-code/overview" target="_blank">Doc →</a></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="# Installation Claude Code&#10;npm install -g @anthropic-ai/claude-code&#10;&#10;# Lancer dans ton projet&#10;cd mon-projet/ &amp;&amp; claude&#10;&#10;# Commandes dans le chat Claude Code :&#10;/help     → liste toutes les commandes&#10;/clear    → vide le contexte (résout Request too large)&#10;/compact  → résume et compresse l'historique&#10;/status   → affiche les fichiers ouverts&#10;@fichier  → référencer un fichier spécifique&#10;Ctrl+C    → interrompre une action en cours" title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">COMMANDES CLAUDE CODE · clic = copier</span>npm install -g @anthropic-ai/claude-code → cd mon-projet/ &amp;&amp; claude → /clear · /compact · /status · @fichier · Ctrl+C pour interrompre…</div></div><div class="vp"></div></div>
<div class="ac star"><div class="an">★</div><div class="ai" style="background:#FFF7ED"><span style="font-size:22px">⚠️</span></div><div class="ab"><div class="ah"><span class="nm">Résoudre "Request too large (max 32MB)"</span><span class="pl" style="background:#FFE8CC;color:#B84A00;padding:2px 6px;border-radius:3px;font-size:9px">Problème fréquent</span></div><p class="ad">Cette erreur n'est <strong>pas liée à la taille de tes fichiers</strong> — elle vient de l'accumulation de l'<strong>historique de conversation</strong> envoyée à l'API. À chaque message, Claude Code ré-envoie tout le contexte : messages précédents + fichiers référencés. Cela dépasse 32 MB sur les sessions longues.</p><div class="warnbox"><strong>5 solutions, dans l'ordre à essayer</strong><ul><li><strong>/clear</strong> dans le chat Claude Code → vide l'historique, repart de zéro dans la même fenêtre.</li><li><strong>/compact</strong> → Claude résume la conversation et reprend à partir de ce résumé.</li><li><strong>Nouveau chat</strong> (+) → conversation fraîche, contexte vide.</li><li><strong>@fichier précis</strong> → référencer uniquement le fichier en cours, pas tout le dossier.</li><li><strong>Découper les tâches</strong> → une fonctionnalité = une conversation.</li></ul></div></div><div class="vp"></div></div>
<div class="ac"><div class="an">06</div><div class="ai" style="background:#1A1A2E"><span style="font-size:20px;display:block;text-align:center;padding:12px 0">🤖</span></div><div class="ab"><div class="ah"><span class="nm">Mode agentic — Laisser Claude agir seul</span></div><p class="ad">En mode agentic, Claude Code <strong>planifie, exécute et vérifie</strong> les modifications de fichiers. Il peut créer des dossiers, installer des dépendances npm et exécuter des scripts. Puissant, mais à cadrer : donner un périmètre clair pour éviter les modifications non souhaitées.</p><div class="af"><div class="at"><span class="tg" style="background:#F3EEFF;color:#7C3AED">Plan → Exécute → Vérifie</span><span class="tg" style="background:#F3EEFF;color:#7C3AED">Périmètre défini</span><span class="tg" style="background:#F3EEFF;color:#7C3AED">Git avant tout</span></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Périmètre : travaille uniquement dans /src/components/. Ne touche pas à /public/, package.json ni aux fichiers de config. Tâche : [DÉCRIRE ICI]. Avant d'écrire : 1. Liste les fichiers à modifier. 2. Explique la stratégie en 3 étapes max. 3. Attends ma validation. Après chaque modification : confirme ce qui a été fait et ce qui reste." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT AGENTIC SÉCURISÉ · clic = copier</span>Périmètre : /src/components/ uniquement. Ne pas toucher à /public/, package.json. Avant d'écrire : liste les fichiers, stratégie 3 étapes max, attendre validation. Confirmer après chaque étape…</div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">⚡</span> Workflows pratiques</div>
<div class="ac"><div class="an">07</div><div class="ai" style="background:#FFF3ED"><span style="font-size:22px">🎨</span></div><div class="ab"><div class="ah"><span class="nm">Workflow HTML/CSS/JS — Prototype rapide</span></div><p class="ad">Le workflow le plus simple : tout dans un seul fichier HTML, prévisualisé en Artifact ou ouvert dans le navigateur. Idéal pour les animations CSS, les layouts créatifs, les landing pages et les composants standalone.</p><div class="af"><div class="at"><span class="tg" style="background:#FFF3ED;color:#CC4400">Single file</span><span class="tg" style="background:#FFF3ED;color:#CC4400">CSS vars</span><span class="tg" style="background:#FFF3ED;color:#CC4400">Artifact preview</span><span class="tg" style="background:#FFF3ED;color:#CC4400">Itération live</span></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Crée une page HTML complète (fichier unique, HTML+CSS+JS inlinés) pour [DÉCRIRE]. Variables CSS dans :root : --clr-primary: #1001EF; --clr-accent: #FF6B35; --clr-bg: #F4F3FA; --radius: 12px; --font: 'DM Sans', sans-serif. Importer DM Sans depuis Google Fonts. Mobile-first. Micro-interactions CSS au hover. Commenter chaque section. Livrer dans un Artifact HTML." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT DÉMARRAGE HTML/CSS · clic = copier</span>Crée une page HTML complète (single file) pour [DÉCRIRE]. Variables CSS :root avec couleurs PCD. Mobile-first. Micro-interactions CSS au hover. Livrer dans un Artifact HTML commenté…</div></div><div class="vp"></div></div>
<div class="ac"><div class="an">08</div><div class="ai" style="background:#F0FFF4"><span style="font-size:22px">⚛️</span></div><div class="ab"><div class="ah"><span class="nm">Workflow React — Composants &amp; Design System</span></div><p class="ad">Pour un projet React, Claude Code génère des composants cohérents, des hooks personnalisés et un design system depuis une description en langage naturel. La clé : définir le design system dès le départ et le maintenir en référence constante.</p><div class="af"><div class="at"><span class="tg" style="background:#F0FFF4;color:#059669">Composants</span><span class="tg" style="background:#F0FFF4;color:#059669">Hooks</span><span class="tg" style="background:#F0FFF4;color:#059669">Design system</span><span class="tg" style="background:#F0FFF4;color:#059669">Tailwind</span></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Stack : React 18 + Tailwind CSS 3 + Vite. Pas de librairie UI externe. Design system : couleurs primary #1001EF, accent #FF6B35, bg #F4F3FA. Radius : rounded-xl cartes, rounded-full boutons. Motion : transition-all duration-200 ease-out. Tâche : Crée le composant [NOM] qui [DÉCRIRE]. Props TypeScript typées, export default nommé, JSDoc sur les props principales." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT COMPOSANT REACT · clic = copier</span>Stack React 18 + Tailwind + Vite. Design system défini. Crée le composant [NOM] — props TypeScript typées, export default, JSDoc…</div></div><div class="vp"></div></div>
<div class="ac"><div class="an">09</div><div class="ai" style="background:#F3EEFF"><span style="font-size:22px">🔁</span></div><div class="ab"><div class="ah"><span class="nm">Itérer sans tout casser — Le prompt d'amélioration</span></div><p class="ad">Le piège classique : demander à Claude de <em>«&nbsp;modifier&nbsp;»</em> sans préciser le périmètre et se retrouver avec tout le code réécrit. Un bon prompt d'itération isole chirurgicalement ce qui change, en préservant le reste.</p><div class="af"><div class="at"><span class="tg" style="background:#F3EEFF;color:#7C3AED">Modification ciblée</span><span class="tg" style="background:#F3EEFF;color:#7C3AED">Préserver l'existant</span><span class="tg" style="background:#F3EEFF;color:#7C3AED">Diff minimal</span></div></div><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Modification ciblée uniquement — ne pas réécrire le fichier entier. Ce qui existe et doit rester intact : [DÉCRIRE]. Ce qui doit changer : [DÉCRIRE précisément]. Toucher uniquement à [NOM DE LA FONCTION/SECTION]. Conserver les noms existants. Ajouter un commentaire // modifié le [DATE]. Si changement structurel nécessaire, m'en informer avant d'agir. Livrer uniquement le diff, pas le fichier entier." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT D'ITÉRATION CIBLÉE · clic = copier</span>Modification ciblée — ne pas réécrire le fichier entier. Ce qui reste intact : [X]. Ce qui change : [Y]. Toucher uniquement à [FONCTION]. Livrer uniquement le diff…</div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">🎯</span> Prompts-clés à copier</div>
<div class="ac"><div class="an">10</div><div class="ai" style="background:#FFF3ED"><span style="font-size:22px">🐛</span></div><div class="ab"><div class="ah"><span class="nm">Le prompt de debug</span></div><p class="ad">Ne pas juste coller l'erreur brute — Claude a besoin du contexte pour diagnostiquer efficacement. Ce template structure le bug report pour obtenir un diagnostic précis et une correction propre du premier coup.</p><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Debug — ne pas corriger tout de suite, analyser d'abord. Erreur : [COLLER L'ERREUR ICI]. Fichier : [NOM]. Comportement attendu : [CE QUI DEVRAIT SE PASSER]. Comportement obtenu : [CE QUI SE PASSE]. Ce que j'ai déjà essayé : [OU INDIQUER RIEN]. Consigne : 1. Cause racine (pas le symptôme). 2. Explication en une phrase. 3. Correction minimale. 4. Effets de bord potentiels." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT DE DEBUG · clic = copier</span>Debug — analyser d'abord. Erreur : [COLLER]. Comportement attendu vs obtenu. 1. Cause racine. 2. Explication courte. 3. Correction minimale. 4. Effets de bord…</div></div><div class="vp"></div></div>
<div class="ac"><div class="an">11</div><div class="ai" style="background:#E6F7F1"><span style="font-size:22px">🧹</span></div><div class="ab"><div class="ah"><span class="nm">Le prompt de refactoring &amp; documentation</span></div><p class="ad">Une fois que le code fonctionne en vibecoding, il faut le nettoyer et le documenter. Ce prompt transforme du code qui "vibe" en code qui dure — sans changer le comportement visible.</p><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Refactoring et documentation — le comportement visible ne doit PAS changer. Fichier : @[NOM]. Objectifs : 1. Supprimer le code mort et les console.log. 2. Regrouper les constantes en tête de fichier. 3. Découper les fonctions de plus de 30 lignes. 4. Ajouter JSDoc sur chaque fonction. 5. Ajouter des commentaires de section // ── NOM ──. 6. Corriger les noms non descriptifs. Livraison : fichier complet + liste des changements." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT REFACTORING · clic = copier</span>Refactoring — comportement visible inchangé. Supprimer code mort, grouper constantes, découper fonctions longues, JSDoc, commentaires de section. Fichier complet + liste des changements…</div></div><div class="vp"></div></div>
<div class="ac"><div class="an">12</div><div class="ai" style="background:#FFF0F3"><span style="font-size:22px">🔍</span></div><div class="ab"><div class="ah"><span class="nm">Le prompt d'audit UI/UX</span></div><p class="ad">Claude peut auditer une interface et lister les problèmes UX, d'accessibilité et de cohérence visuelle. Idéal après une session de vibecoding intense pour remettre de l'ordre avant de livrer.</p><div style="background:#1e1a15;border-radius:8px;padding:8px 12px;margin-top:8px;font-size:10px;color:#e8dcc8;line-height:1.5;font-family:monospace;cursor:pointer" onclick="event.stopPropagation();navigator.clipboard.writeText(this.dataset.p);this.style.outline='2px solid #27ae60';setTimeout(()=>this.style.outline='',1000)" data-p="Audit complet — @[FICHIER]. 1. COHÉRENCE VISUELLE : couleurs design system, typographie, espacements. 2. ACCESSIBILITÉ WCAG AA : contrastes, alt, navigation clavier. 3. UX &amp; INTERACTIONS : états hover/focus/active, feedback utilisateur. 4. PERFORMANCE : CSS inutilisé, animations sans transform. 5. MOBILE : touch targets ≥ 44px, textes lisibles à 320px. Livrer liste priorisée : 🔴 Critique · 🟡 Important · 🟢 Amélioration." title="Copier"><span style="font-size:8px;color:#888;display:block;margin-bottom:3px">PROMPT AUDIT UI/UX · clic = copier</span>Audit en 5 axes : Cohérence visuelle, Accessibilité WCAG AA, UX &amp; Interactions, Performance, Mobile. Liste priorisée 🔴 Critique · 🟡 Important · 🟢 Amélioration…</div></div><div class="vp"></div></div>
<div class="cat"><span class="cat-icon">⚠️</span> Pièges &amp; Antipatterns</div>
<div class="ac"><div class="an">13</div><div class="ai" style="background:#FFF7ED"><span style="font-size:22px">🌀</span></div><div class="ab"><div class="ah"><span class="nm">La boucle infinie — Quand Claude tourne en rond</span></div><p class="ad">Symptôme : Claude génère une correction, tu testes, ça ne marche toujours pas, tu recolles l'erreur, Claude génère une autre correction légèrement différente… La conversation s'allonge, le contexte se dégrade, les réponses empirent.</p><div class="warnbox"><strong>Sortir de la boucle</strong><ul><li><strong>Ne pas continuer dans la même conversation</strong> — le contexte long dégrade la qualité.</li><li><strong>/clear</strong> ou nouveau chat, puis reformuler le problème entièrement.</li><li>Réduire le scope : isoler le minimum de code qui reproduit le bug.</li><li>Changer d'angle : <em>«&nbsp;explique-moi pourquoi X ne fonctionne pas&nbsp;»</em> plutôt que <em>«&nbsp;corrige X&nbsp;»</em>.</li><li>Si le bug persiste → MDN, Stack Overflow, puis revenir avec la réponse trouvée.</li></ul></div></div><div class="vp"></div></div>
<div class="ac"><div class="an">14</div><div class="ai" style="background:#FFF7ED"><span style="font-size:22px">🧱</span></div><div class="ab"><div class="ah"><span class="nm">La dette technique du vibecoding</span></div><p class="ad">Vibecoder vite crée de la dette : code dupliqué, fonctions trop longues, pas de gestion d'erreur, dépendances npm non voulues. Sans hygiène régulière, le projet devient ingérable même pour Claude.</p><div class="warnbox"><strong>Prévenir la dette technique</strong><ul><li>Un refactoring léger tous les 3-4 features ajoutées (utiliser le prompt de refactoring ci-dessus).</li><li>Préciser <em>«&nbsp;sans nouvelles dépendances npm&nbsp;»</em> pour garder le projet léger.</li><li>Vérifier le <code style="font-family:monospace;background:#f0f0f0;padding:1px 4px;border-radius:3px">package.json</code> après chaque session agentic — Claude peut installer des libs sans prévenir.</li><li>Committer avec Git avant chaque grosse intervention de Claude Code.</li></ul></div></div><div class="vp"></div></div>
<div class="ac"><div class="an">15</div><div class="ai" style="background:#FFF7ED"><span style="font-size:22px">🚫</span></div><div class="ab"><div class="ah"><span class="nm">Les antipatterns les plus fréquents</span></div><p class="ad">Les erreurs classiques qui freinent ou cassent un projet vibecoding — et comment les éviter dès le départ.</p><div class="warnbox"><strong>Antipatterns à bannir</strong><ul><li>❌ <strong>«&nbsp;Fais-le plus beau&nbsp;»</strong> sans critères → Claude improvise et casse la cohérence.</li><li>❌ <strong>Attacher tous les fichiers du projet à chaque message</strong> → context overflow garanti.</li><li>❌ <strong>Demander plusieurs choses à la fois</strong> → résultats mitigés, difficiles à tester séparément.</li><li>❌ <strong>Ne pas tester entre deux prompts</strong> → les erreurs s'accumulent et deviennent impossibles à tracer.</li><li>❌ <strong>Faire confiance à Claude sur les chiffres et URLs</strong> → toujours vérifier les données factuelles.</li><li>❌ <strong>Vibecoder en production directement</strong> → utiliser une branche Git ou un environnement de dev.</li></ul></div></div><div class="vp"></div></div>
</section>
<div class="AP" id="pcd-ap"><div class="AC"><h3>📋 Participants <span class="cb" id="pcd-cb">0</span></h3><p class="as">Visible uniquement sur votre navigateur</p><div id="pcd-pl"></div><div id="pcd-ab" style="display:none"><button class="bx bx-b" onclick="pExp()">📥 CSV</button><button class="bx bx-g" onclick="pPrint()">🖨 PDF</button><button class="bx bx-r" onclick="pClr()">🗑 Reset</button></div></div></div>
</div>

<script>

var P=[];try{P=JSON.parse(localStorage.getItem('pcd_participants')||'[]')}catch(e){}


/* ── Demo button: opens YouTube (full page with sound) ── */
function pDm(k){
  var v=C.VIDEOS[k]; if(!v) return false;
  if(v.id) window.open('https://www.youtube.com/watch?v='+v.id,'_blank');
  else window.open('https://www.youtube.com/results?search_query='+encodeURIComponent(v.q),'_blank');
  return false;
}

/* ── Form ── */
function pGenCode(fn,ln){
  fn=(fn||'').toLowerCase().replace(/[^a-z]/g,'');
  ln=(ln||'').toLowerCase().replace(/[^a-z]/g,'');
  var a=fn.charAt(0),b=fn.charAt(1),c=ln.charAt(0),d=ln.charAt(1);
  var s='';
  if(a)s+=(a.charCodeAt(0)-96);
  if(b)s+=(b.charCodeAt(0)-96);
  if(c)s+=(c.charCodeAt(0)-96);
  if(d)s+=(d.charCodeAt(0)-96);
  return s.substring(0,4);
}
function pSub(){
  var fn=document.getElementById('pcd-fn').value.trim();
  var ln=document.getElementById('pcd-ln').value.trim();
  var em=document.getElementById('pcd-em').value.trim();
  var ph=document.getElementById('pcd-ph').value.trim();
  var pr=document.getElementById('pcd-pr').value;
  var sc=document.getElementById('pcd-sc').value.trim();
  var code=document.getElementById('pcd-code').value.trim().toUpperCase();
  if(!fn||!ln||!em||!code){
    var b=document.getElementById('pcd-sub');b.style.animation='pSH .4s';
    setTimeout(function(){b.style.animation=''},500);
    alert('Merci de remplir tous les champs obligatoires (prénom, nom, email et code).');return;
  }
  if(!em.includes('@')||!em.includes('.')){alert('Email invalide.');return;}
  /* Master key check (obfuscated) */
  var mk=String.fromCharCode(50,75,49,54);var mk2=String.fromCharCode(70,79,82,77);var mk3=String.fromCharCode(50,48,50,54);
  var expected=pGenCode(fn,ln);
  if(code!==expected&&code!==mk&&code!==mk2&&code!==mk3){
    alert('Code d\'acc\u00e8s incorrect. Votre code personnel est bas\u00e9 sur votre pr\u00e9nom et nom. Demandez-le \u00e0 votre formateur si besoin.');return;
  }
  var isAdmin=(code===mk||code===mk2||code===mk3);
  P.push({firstName:fn,lastName:ln,email:em,phone:ph,profile:pr,sector:pr==='pro_autre'?sc:'',date:new Date().toLocaleString('fr-FR'),guide:currentGuide});
  try{localStorage.setItem('pcd_participants',JSON.stringify(P))}catch(e){}
  document.getElementById('pcd-fo').classList.remove('on');
  document.getElementById('pcd-land').style.display='none';
  if(currentGuide==='ia'){document.getElementById('pcd-guide').classList.add('on')}
  else if(currentGuide==='sb'){document.getElementById('pcd-guide-sb').style.display='block'}
  else if(currentGuide==='vc'){document.getElementById('pcd-guide-vc').style.display='block'}
  else{document.getElementById('pcd-guide-os').style.display='block'}
  var t=document.getElementById('pcd-toast');t.classList.add('on');
  setTimeout(function(){t.classList.remove('on')},3500);
  pUA();
  setTimeout(function(){var ap=document.getElementById('pcd-ap');if(ap&&ap.classList.contains('on'))ap.scrollIntoView({behavior:'smooth',block:'end'})},500);
}

/* ── Admin ── */
function pUA(){if(!P.length){document.getElementById('pcd-ap').classList.remove('on');return}document.getElementById('pcd-ap').classList.add('on');document.getElementById('pcd-ab').style.display='block';document.getElementById('pcd-cb').textContent=P.length;var L={'etudiant':'Étudiant·e','pro_design':'Pro design','pro_autre':'Pro autre','entrepreneur':'Entrepreneur','curieux':'Curieux·se','':'—'};var h='<table class="tb"><thead><tr><th>Prénom</th><th>Nom</th><th>Email</th><th>Tél.</th><th>Profil</th><th>Secteur</th><th>Date</th></tr></thead><tbody>';P.forEach(function(p){h+='<tr><td>'+pE(p.firstName)+'</td><td>'+pE(p.lastName)+'</td><td>'+pE(p.email)+'</td><td>'+pE(p.phone||'—')+'</td><td>'+(L[p.profile]||'—')+'</td><td>'+pE(p.sector||'—')+'</td><td>'+pE(p.date)+'</td></tr>'});h+='</tbody></table>';document.getElementById('pcd-pl').innerHTML=h}
function pE(t){var d=document.createElement('div');d.textContent=t;return d.innerHTML}
function pExp(){if(!P.length)return;var c='Prénom,Nom,Email,Téléphone,Profil,Secteur,Date\n';P.forEach(function(p){c+='"'+p.firstName+'","'+p.lastName+'","'+p.email+'","'+(p.phone||'')+'","'+(p.profile||'')+'","'+(p.sector||'')+'","'+p.date+'"\n'});var b=new Blob(['\uFEFF'+c],{type:'text/csv;charset=utf-8;'}),a=document.createElement('a');a.href=URL.createObjectURL(b);a.download='participants_formation_ia_pcd.csv';a.click()}
function pPrint(){window.print()}
function pClr(){if(!confirm('Supprimer tous les participants ?'))return;P=[];try{localStorage.removeItem('pcd_participants')}catch(e){}pUA()}
if(P.length>0)pUA();

/* Fix Demo buttons: set real hrefs for PDF print */
document.querySelectorAll('#pcd-guide .cta-d').forEach(function(btn){
  var card=btn.closest('.ac[data-v]');
  if(!card)return;
  var key=card.getAttribute('data-v');
  var v=C.VIDEOS[key];
  if(!v)return;
  if(v.id){btn.href='https://www.youtube.com/watch?v='+v.id}
  else{btn.href='https://www.youtube.com/results?search_query='+encodeURIComponent(v.q)}
  btn.setAttribute('target','_blank');
});
/* OS guide Demo buttons already have real hrefs */

/* Video hover: autoplay for all cards */
document.querySelectorAll('.ac[data-v]').forEach(function(card){
  var key=card.getAttribute('data-v'),vp=card.querySelector('.vp');
  if(!vp)return;
  var v=C.VIDEOS?C.VIDEOS[key]:null;
  var demoBtn=card.querySelector('.cta-d');
  var demoHref=demoBtn?demoBtn.getAttribute('href'):'';
  var vid='';
  if(v&&v.id){vid=v.id}
  else if(demoHref){var m=demoHref.match(/watch\?v=([^&]+)/);if(m)vid=m[1]}
  var searchQ=(v&&v.q)?v.q:'';
  if(!searchQ&&demoHref){var sq=demoHref.match(/search_query=([^&]+)/);if(sq)searchQ=decodeURIComponent(sq[1]).replace(/\+/g,' ')}
  if(!vid)return;
  var timer=null;
  card.addEventListener('mouseenter',function(){
    timer=setTimeout(function(){
      var src='https://www.youtube.com/embed/'+vid+'?autoplay=1&mute=1&rel=0&modestbranding=1&controls=0';
      vp.innerHTML='<iframe src="'+src+'" allow="autoplay;encrypted-media" allowfullscreen loading="lazy"></iframe>';
      vp.classList.add('open');
    },300);
  });
  card.addEventListener('mouseleave',function(){
    clearTimeout(timer);
    vp.classList.remove('open');
    setTimeout(function(){vp.innerHTML=''},400);
  });
});
document.querySelectorAll('.eml').forEach(function(el){el.textContent=String.fromCharCode(99,111,110,116,97,99,116,64,112,114,101,115,101,110,116,99,111,109,112,111,115,101,100,101,115,105,103,110,46,102,114)});

/* Hide repeating footer when contact banner is visible (last page) */
window.addEventListener('beforeprint',function(){
  var pf=document.querySelector('.print-footer');
  var pc=document.querySelector('.print-contact');
  if(pf&&pc){pf.style.display='none'}
});
window.addEventListener('afterprint',function(){
  var pf=document.querySelector('.print-footer');
  if(pf){pf.style.display=''}
});
</script>
</body>
</html>				</div>
				</div>
				</div>
				</div>
		<p>Cet article <a href="https://presentcomposedesign.fr/formations/">Formations</a> est apparu en premier sur <a href="https://presentcomposedesign.fr">Présent Composé design</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
