html{-webkit-text-size-adjust:100%;text-size-adjust:100%} /* pas de mise à l'échelle auto du texte (Brave Android : réglage « taille du texte » / font boosting → textes agrandis, mise en page cassée) */
/* Extracted CSS from fret-theory-2026-06-15-bugfix2.html */

/* ── Seuil desktop/mobile ──────────────────────────────────────────────────────────────────────────────
   Toutes les media queries de layout desktop de ce fichier utilisent min-width:1024px (7 occurrences,
   cf. grep "@media(min-width:1024px)"). Doit rester strictement synchronisé avec DESKTOP_BREAKPOINT dans
   app.js (~30 tests window.innerWidth en JS) — CSS n'a aucun moyen natif de faire lire une media query
   depuis une custom property, donc cette synchronisation reste manuelle : pour changer le seuil, remplacer
   1150 par la nouvelle valeur aux deux endroits (grep "1150px" ici, DESKTOP_BREAKPOINT dans app.js).
   Abaissé de 1300 à 1150 (2026-07-25) : sur écran haute définition avec mise à l'échelle Windows élevée
   (ex. 14" 2560×1440 à 200%), la largeur CSS effective chute sous l'ancien seuil malgré un grand écran
   physique, faisant disparaître la sidebar desktop (bug rapporté). Quelques commentaires isolés plus loin
   dans ce fichier mentionnent encore "1300px" dans leur texte descriptif (non fonctionnels, non corrigés
   par souci de ne pas modifier des zones non liées à ce fix) — seules les valeurs de @media ci-dessus et
   DESKTOP_BREAKPOINT dans app.js font foi. */

/* ── Polices auto-hébergées (vendor/fonts/) ──────────────────────────────────────────────────────────
   Anciennement chargées depuis fonts.googleapis.com/fonts.gstatic.com (CDN Google Fonts) — téléchargées
   en local pour ne plus dépendre d'un service externe. DM Sans 400 sert aussi pour les poids 500/600 :
   Google Fonts publie les mêmes fichiers .woff2 pour ces 3 poids (vérifié dans le CSS généré par
   fonts.googleapis.com), donc un seul jeu de fichiers couvre les trois font-weight utilisés ici (400/
   500/600). Seuls les sous-ensembles latin + latin-ext sont conservés (pas vietnamese) : suffisant pour
   le contenu FR/EN de l'app, évite de télécharger un 3e jeu de fichiers inutile. */
@font-face{font-family:'DM Sans';font-style:normal;font-weight:400 600;font-display:swap;src:url('vendor/fonts/dmsans-400-latin.woff2') format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;}
@font-face{font-family:'DM Sans';font-style:normal;font-weight:400 600;font-display:swap;src:url('vendor/fonts/dmsans-400-latinext.woff2') format('woff2');unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;}
@font-face{font-family:'Space Mono';font-style:normal;font-weight:400;font-display:swap;src:url('vendor/fonts/spacemono-400-latin.woff2') format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;}
@font-face{font-family:'Space Mono';font-style:normal;font-weight:400;font-display:swap;src:url('vendor/fonts/spacemono-400-latinext.woff2') format('woff2');unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;}
@font-face{font-family:'Space Mono';font-style:normal;font-weight:700;font-display:swap;src:url('vendor/fonts/spacemono-700-latin.woff2') format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;}
@font-face{font-family:'Space Mono';font-style:normal;font-weight:700;font-display:swap;src:url('vendor/fonts/spacemono-700-latinext.woff2') format('woff2');unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;}

/* Symboles musicaux (bloc Unicode U+1D100-1D1FF : 𝅝 𝅗𝅥 𝅘𝅥𝅯 𝄻 𝄽…) — sous-ensemble de Noto Music (OFL, cf.
   vendor/fonts/NotoMusic-OFL.txt), 15 Ko, chargé seulement si un de ces caractères est affiché. Audit perf
   2026-09-28 : sans police qui les contient en tête de pile, le navigateur cherchait une police de secours à
   CHAQUE affichage de la palette de dictée d'Eartrain (mise en page 28 -> 3 ms à CPU ×4). */
@font-face{font-family:'FT Music';font-style:normal;font-weight:400;font-display:block;src:url('vendor/fonts/notomusic-subset.woff2') format('woff2');unicode-range:U+1D100-1D1FF;}
/* ── Polices système en attendant le chargement async ── */
/* Le suffixe commun (Apple Color Emoji...Noto Sans Symbols) sert de repli pour les
   glyphes musicaux/symboles (♩♪♫𝅗𝅥⌕◈⊟...) utilisés partout dans l'app en Unicode brut :
   sans lui, DM Sans/Segoe UI/Helvetica Neue ne couvrent pas ces blocs et macOS (Safari
   et Chrome) affiche des rectangles vides à la place (bug rapporté, absent sur Windows
   où les polices système par défaut couvrent ces plages par coïncidence). */
:root{
  --fsym:'FT Music','Apple Color Emoji','Segoe UI Symbol','Segoe UI Emoji','Noto Sans Symbols','Noto Sans Symbols 2',system-ui;
  --fd:'DM Sans','Trebuchet MS','Arial Black',var(--fsym),sans-serif;
  --fm:'Space Mono','Courier New',var(--fsym),monospace;
  --fb:'DM Sans','Segoe UI','Helvetica Neue',Arial,var(--fsym),sans-serif;
}


/* Sous-menu au survol (Progressions : Catalogue/Texte/Pool ; Harmony : Accords/Notes/Mélodie), sur les DEUX
   barres d'onglets (sidebar desktop verticale #sidebarTabs ET barre horizontale #mainTabs, visible aussi en
   desktop large en plus du mode mobile) — hors media query desktop pour s'appliquer aux deux contextes.
   .tab-flyout est un FRÈRE du bouton .tab (pas un enfant) : setLang() (cf. app.js) réécrit btn.textContent
   sur tous les .tab[data-tab] à chaque changement de langue/chargement, ce qui effacerait silencieusement
   un flyout imbriqué comme enfant du bouton. position:fixed (pas absolute) : la sidebar a overflow-x:hidden
   (pour son propre scroll vertical), qui coupait/masquait silencieusement un flyout en absolute dès qu'il
   dépassait sa largeur (bug constaté : le sous-menu ne s'affichait jamais malgré un état "open" correct en
   JS). En fixed, le positionnement (left/top) est calculé dynamiquement par _tabFlyoutInit() en coordonnées
   viewport — à droite du bouton pour la sidebar verticale, en dessous pour la barre horizontale (cf. JS).
   Affiché/masqué via mouseenter/mouseleave (couvre bouton + flyout) plutôt qu'un simple :hover CSS, pour
   tolérer la petite zone morte entre les deux éléments ; un seul flyout ouvert à la fois (_openTabFlyout,
   cf. app.js) pour éviter toute superposition en glissant rapidement d'un bouton à l'autre. */
.tab-flyout{
  display:none;position:fixed;min-width:150px;z-index:1001;
  background:var(--sf);border:1px solid var(--bd);border-radius:0 var(--rl) var(--rl) 0;
  box-shadow:2px 2px 10px var(--sh);flex-direction:column;padding:4px;
}
.tab-flyout.tab-flyout-below{border-radius:0 0 var(--rl) var(--rl)}
.tab-flyout.open{display:flex}
.tab-flyout a{
  display:block;width:100%;padding:8px 10px;border-radius:5px;background:transparent;
  color:var(--tx2);text-align:left;font-family:var(--fb);font-size:.72rem;font-weight:400;cursor:pointer;
  transition:all var(--tr);box-sizing:border-box;text-decoration:none;
}
.tab-flyout a:hover{background:var(--s2);color:var(--tx)}
.tab-flyout a:visited{color:var(--tx2)}

/* Module Accords (Chords) : .chords-cols regroupe #voicingGrid + #chStackBuilder — colonne simple par
   défaut (mobile/tablette <1150px), passe en 2 colonnes côte à côte au-delà (cf. override dans le bloc
   desktop juste en dessous). Règle de base placée ICI, AVANT le bloc @media(min-width:1024px), volontai-
   rement : elle doit rester déclarée avant tout override desktop du même sélecteur — sans ça, à spécificité
   CSS égale (0-1-0 des deux côtés), la règle la plus TARDIVE dans le fichier gagne la cascade quelle que
   soit la largeur d'écran réelle, rendant l'override desktop totalement inopérant (bug constaté : la règle
   se trouvait avant à la fin du fichier, après le bloc media — flex-direction:column s'appliquait donc
   TOUJOURS, même en desktop large, empêchant les 2 colonnes de jamais s'activer). #chStackBuilder passe en
   position:fixed à <1150px (règle dédiée plus bas, inchangée) — cohabite normalement avec ce flux colonne. */
.chords-cols{display:flex;flex-direction:column;gap:10px;min-width:0}
/* ═══ DESKTOP SIDEBAR LAYOUT ═══ */
@media(min-width:1024px){
  :root{font-size:26px}
  .tg-row-desktop{display:flex!important}
  .tg-row-mobile{display:none!important}
  /* Subbar Groovebox : jamais de scrollbar visible sur desktop (bug rapporté : "défileurs alors que la
     place nécessaire existe" — confirmé par mesure, aucun débordement réel même à 1150px, le seuil desktop
     le plus étroit). overflow-x:auto (posé en JS, cf. grooveInitPage) reste nécessaire pour le scroll
     tactile en dessous de ce seuil (sidebar en tiroir, largeur potentiellement bien plus étroite) — !important
     nécessaire pour surpasser le style inline, seul moyen de désactiver ce comportement UNIQUEMENT au-delà
     du breakpoint sans dupliquer toute la construction JS de la barre en 2 variantes. */
  #grooveToolbarWrap .subbar,#grooveToolbarWrap .subbar>div{overflow:visible!important}
  .pages-container{
    flex:1;overflow-y:auto;display:flex;flex-direction:column;align-items:stretch;
    width:100%;margin:0;padding:0;box-sizing:border-box;
    height:100%;gap:0;
  }
  .page > div:first-child,.page > .sticky-toolbar{
    position:sticky;top:0;z-index:100;background:var(--sf);
    width:100%;max-width:none;margin:0;border-top:none;
  }
  .page > *:not(div:first-child):not(.sticky-toolbar){
    max-width:1600px;width:100%;margin:0 auto;box-sizing:border-box;
    flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden;
  }
  /* Séquenceur (#page-melodie) exempté du plafond 1600px : c'est un piano-roll scrollable, pas un
     diagramme de largeur fixe (fretboard/tableau) — la grille de notes garde de toute façon sa
     largeur réelle (mesures×zoom, cf. app.js melResize), seul le CONTENEUR (toolbar déjà pleine
     largeur, zone de grille, panneau du bas) doit s'étirer jusqu'au bord pour éviter le décalage
     visuel toolbar-pleine-largeur / reste-du-module-plus-étroit-et-centré (bug rapporté : "sur
     desktop, utiliser toute la largeur disponible"). */
  #page-melodie > .sq-body{max-width:none;margin:0;overflow-y:auto;display:block}
  /* SURFACE (#page-play) : exemption du plafond 1600px devenue un RÉGLAGE (demande explicite : "fais-en un
     paramètre réglable... une option pour toute la surface, une option pour la même taille qu'avant") — au
     lieu d'exempter #gsGridWrap/#gsHexGridWrap en permanence comme #page-melodie ci-dessus, l'exemption ne
     s'applique plus que sous body.gs-full-width (posée/retirée par gsSetFullWidth, app.js). Sans la classe,
     la règle générique juste au-dessus (max-width:1600px) continue de s'appliquer normalement : c'est le
     comportement d'ORIGINE, gardé comme valeur par défaut (gsFullWidth=false). */
  body.gs-full-width #page-play > #gsGridWrap,body.gs-full-width #page-play > #gsHexGridWrap{max-width:none;margin:0}

  body{display:flex;flex-direction:column;height:100vh;height:100dvh}
  body>.app-row{display:flex;flex-direction:row;flex:1;min-height:0}
  .topbar{position:relative;height:auto;width:100%;z-index:1000;flex-direction:column;flex-shrink:0}
  .tb1{flex-direction:row}
  .tb2{display:flex}
  /* Indice visuel de scroll horizontal sur #mainTabs (même principe que #triadsHeader::after en mobile) :
     avec les modules ajoutés au fil du temps, la barre d'onglets déborde dès qu'on tombe sous ~1450px de
     large (ex. 2560×1440 zoomé à 200% ≈ 1280px logiques) — .tabs masque déjà volontairement sa propre
     scrollbar (cf. .tabs::-webkit-scrollbar), ce qui rendait ce débordement totalement invisible/muet
     (perçu comme une mise en page cassée plutôt qu'une liste scrollable). Toujours affiché plutôt que
     calculé dynamiquement (JS aurait dû réévaluer à chaque resize/ajout de module) : sans débordement réel,
     le dégradé retombe simplement sur le fond identique, donc invisible.  */
  .tb2{position:relative}
  .tb2::after{
    content:'';position:absolute;top:0;right:0;bottom:0;width:26px;
    background:linear-gradient(to right,transparent,var(--sf));pointer-events:none;
  }
  /* Réglages "Navigation desktop" (Paramètres > Général) : masque la barre horizontale d'onglets (.tb2) et/ou
     la sidebar verticale (.sidebar-desktop) — au moins une reste toujours affichée, cf. setDesktopNavVisibility.
     Scopé à ce media query : ces classes n'ont aucun sens en dessous de 1300px, où seule la topbar mobile existe. */
  body.hide-topbar-tabs .tb2{display:none}
  body.hide-sidebar-tabs .sidebar-desktop{display:none}
  /* .desktop-root-row/#desktopRootRowWrap (demande explicite : "un paramètre réglable qui choisit sa
     position, topbar ou barre dédiée") : masquée par défaut (le défaut reste .topbar-root-row/
     #topbarRootRowWrap dans la topbar), affichée uniquement si l'utilisateur choisit 'dedicated' (cf.
     APP_SETTINGS.rootSelectorPosition/setRootSelectorPosition dans app.js, body.root-sel-dedicated posée
     par applySettings). Classe DIFFÉRENTE de .topbar-root-row (pas une réutilisation) : les 2 lignes
     doivent basculer INDÉPENDAMMENT l'une de l'autre, une classe partagée les aurait fait apparaître/
     disparaître ensemble au lieu de s'exclure mutuellement.
     Alignée à GAUCHE (demande explicite, contrairement à .topbar-root-row qui reste centrée dans l'espace
     restreint de la topbar) : cette ligne lui appartient entièrement, aucune contrainte de partage d'espace
     avec #topbarStatus/.tbr ici.
     Masquée par défaut en dessous de 1150px via la règle de base (cf. .desktop-root-row{display:none} juste
     après .topbar-root-row, plus haut dans ce fichier — BUG CORRIGÉ : cette règle de base manquait
     initialement, le <div> restait display:block par défaut et s'affichait EN PLUS de .tb3 sur mobile,
     rapporté avec capture : "double sélecteur de tonalité"). */
  .desktop-root-row{display:none}
  body.root-sel-dedicated .desktop-root-row{
    display:flex;align-items:center;gap:4px;padding:6px 12px;border-bottom:1px solid var(--bd);background:var(--sf);
  }

  /* Sidebar avec onglets */
  .sidebar-desktop{
    position:fixed;left:0;top:var(--topbar-h,120px);width:190px;height:calc(100vh - var(--topbar-h,120px));
    background:var(--sf);border-right:1px solid var(--bd);overflow-y:auto;overflow-x:hidden;
    z-index:999;display:flex;flex-direction:column;gap:0;padding:8px 0;
    font-size:.75rem;
  }
  /* #sidebarTabs (id="sidebarTabs") réutilise la classe .tabs, pensée pour la barre horizontale mobile
     (overflow-x:auto, scrollbar-width:none — un swipe n'a pas besoin d'indice visuel). En desktop, ce même
     élément devient l'unique wrapper vertical de tous les onglets de la sidebar : sans ce reset, il devenait
     LUI-MÊME un 2e scroll container (overflow-y hérité, hauteur figée à celle du parent) empilé sous
     .sidebar-desktop — le débordement réel restait scrollable au clavier/molette mais totalement invisible
     (pas de scrollbar, pas de fade), lu comme "les derniers onglets ont disparu" sur les hauteurs de fenêtre
     réduites (haut zoom Windows sur petit écran haute def, tablette en petite hauteur). En repassant ce
     wrapper en hauteur naturelle (overflow:visible), c'est .sidebar-desktop (scrollbar visible, cf. ci-dessus)
     qui redevient le seul et unique conteneur de scroll de la sidebar. */
  .sidebar-desktop .tabs{overflow:visible;height:auto;flex:none}
  .sidebar-desktop .tab{
    /* Icônes rapprochées du bord gauche de l'écran (demande explicite, réduit une 2e fois : 8px→4px→2px de
       padding gauche) — le padding droit (8px) reste inchangé pour laisser respirer le raccourci clavier
       aligné à droite (cf. .tab-kbd-hint, margin-left:auto plus bas). Espacement vertical entre entrées
       réduit une 3e fois (demande explicite : "rapproche encore légèrement les noms de modules
       (verticalement)") — padding vertical 10px→7px→5px→3px, marge 1px→0 (hauteur totale d'une entrée
       ~37px→~33px, tout en restant une cible cliquable raisonnable au clavier/à la souris). */
    width:100%;padding:3px 8px 3px 2px;border:none;border-left:3px solid transparent;border-radius:0;
    background:transparent;color:var(--tx2);text-align:left;font-size:.75rem;
    transition:all var(--tr);justify-content:flex-start;gap:6px;height:auto;margin:0;
    /* .tab de base a white-space:nowrap — sans overflow:hidden ici, libellé+hint qui dépassent les 190px
       de la sidebar débordaient hors du conteneur au lieu d'être contenus (constaté : le hint se faisait
       couper à mi-mot, ex. "BACK" pour "Retour"). */
    overflow:hidden;
  }
  .sidebar-desktop .tab.active{
    background:var(--s2);border-left-color:var(--c-root);color:var(--tx);font-weight:600;
  }
  .sidebar-desktop .tab:hover,.sidebar-desktop .tab.nav-hover{background:var(--s2);color:var(--tx)}
  /* Colonnes icône/libellé (cf. setLang() dans app.js, qui construit .tab-icon/.tab-label à partir du texte
     "icône libellé" existant) — demande explicite : la 1ère lettre de chaque libellé de module doit
     s'aligner verticalement sur toute la sidebar, quelle que soit la largeur visuelle du glyphe d'icône
     (≋/♪/⬡/〜/𝅗𝅥... n'ont pas tous le même encombrement). .tab-icon prend une largeur fixe et centre son
     glyphe dedans ; .tab-label démarre donc toujours au même x sur toutes les lignes. gap existant du
     parent .tab (6px, cf. ci-dessus) reste le seul espacement entre les deux colonnes. */
  .sidebar-desktop .tab-icon{width:16px;flex-shrink:0;text-align:center}
  .sidebar-desktop .tab-label{overflow:hidden;text-overflow:ellipsis}
  /* Raccourci clavier associé au module, discret — cf. _syncSidebarShortcutLabels(). Aligné à droite
     via margin-left:auto (le .tab parent est déjà display:flex, cf. .tab de base). Couleur plus sombre
     que --tx2 (déjà utilisé par le libellé lui-même) pour rester clairement secondaire. max-width+ellipsis
     plutôt que flex-shrink:0 seul : un libellé de touche spéciale un peu long (malgré les abréviations de
     _kbdBindingLabel) se tronque proprement au lieu de forcer le libellé du module à rétrécir ou de
     déborder hors de la sidebar. */
  /* min-width:0 nécessaire pour que overflow/text-overflow fassent effet : .tab-kbd-hint est un flex-item
     (parent .tab en display:flex), qui reçoit min-width:auto par défaut — ce minimum implicite empêchait
     l'ellipsis de s'appliquer (le texte se coupait net sans "…", constaté). */
  .tab-kbd-hint{margin-left:auto;padding-left:8px;flex-shrink:0;min-width:0;max-width:64px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx3);letter-spacing:.02em}
  .sidebar-desktop .tab-sep{display:none}

  /* Main content area */
  .main-content{
    flex:1 1 0;width:auto;min-width:0;
    display:flex;flex-direction:column;height:100%;min-height:0;gap:0;
  }
  .sidebar-desktop{position:relative;top:0;height:100%;flex-shrink:0}

  /* Mixeur global (Paramètres > Général > Panneau desktop) — flex sibling de .sidebar-desktop/.main-content
     dans .app-row, PAS position:fixed (comportement volontairement différent de .nav-popup, le tiroir
     mobile) : masquer/afficher ce panneau (body.hide-mixer-panel) fait regagner/perdre sa largeur à
     .main-content automatiquement, sans recalcul JS, même mécanisme que la sidebar. Rempli par
     mixerRenderPanel() (cf. app.js). */
  body.hide-mixer-panel .mixer-desktop{display:none}
  .mixer-desktop{
    width:170px;flex-shrink:0;height:100%;background:var(--sf);border-right:1px solid var(--bd);
    overflow-y:auto;overflow-x:hidden;display:flex;flex-direction:column;gap:10px;
    padding:10px 8px;font-size:.72rem;box-sizing:border-box;
  }
  .mixer-desktop .mixer-channel{display:flex;flex-direction:column;gap:3px}
  .mixer-desktop .mixer-channel-label{color:var(--tx2);font-size:.62rem;text-transform:uppercase;letter-spacing:.02em}
  .mixer-desktop .mixer-channel-val{color:var(--tx3);font-size:.6rem;text-align:right}

  /* Lecture par canal (demande explicite : "remplacer le bouton play/stop par un double clic sur le nom du
     module") — plus de bouton dédié, le libellé lui-même porte le geste (double-clic, cf.
     mixerRenderPanel/_mixerWheelGuard) et l'indicateur visuel (classe .playing, recalée par
     _mixerSyncPlayButtons). Point-avant coloré façon "pastille active" plutôt qu'un fond inversé complet
     (réservé aux boutons topbar ▶/■ eux-mêmes) — le libellé reste un texte, pas un bouton carré. */
  .mixer-desktop .mixer-channel-label.playing{color:var(--c-root)}
  .mixer-desktop .mixer-channel-label.playing::before{
    content:'●';display:inline-block;margin-right:4px;font-size:max(10px,.55rem);
  }

  /* Pistes individuelles (Groovebox/Accompagnement, cf. MIXER_CHANNELS.getSubTracks) — dépliées par clic
     SUR LA FLÈCHE ▾/▸ uniquement (bug corrigé, signalé : "le simple clic prend du temps avant d'agir...
     remplacer par un clic sur la flèche de dépliage, ne plus attendre un double clic") — zone distincte du
     libellé (double-clic = Play/Stop, cf. .mixer-channel-label.playing plus haut), jamais superposée. */
  .mixer-desktop .mixer-channel-head{display:flex;align-items:center;gap:4px}
  .mixer-desktop .mixer-channel-arrow{cursor:pointer;user-select:none;color:var(--tx3);flex-shrink:0;width:12px;text-align:center}
  .mixer-desktop .mixer-channel-arrow:hover{color:var(--tx)}
  .mixer-desktop .mixer-subtracks{
    display:flex;flex-direction:column;gap:6px;margin-top:4px;padding-left:8px;border-left:2px solid var(--bd);
  }
  .mixer-desktop .mixer-subtrack{display:flex;flex-direction:column;gap:2px}
  .mixer-desktop .mixer-subtrack-label{color:var(--tx3);font-size:max(10px,.58rem)}

  /* Mixeur intégré à la sidebar (mode "Dans la sidebar", cf. setMixerMode) — mêmes canaux que .mixer-desktop
     ci-dessus mais tout en bas de #sidebarTabs, dans la colonne de 190px déjà existante plutôt qu'un panneau
     séparé. body.hide-inline-mixer masque aussi le séparateur juste au-dessus (#sidebarMixerSep, cf.
     index.html) pour ne pas laisser un simple trait orphelin quand ce mode n'est pas actif. Règles pour
     .mixer-inline lui-même déplacées HORS de ce media query (cf. plus bas dans le fichier) : ce panneau a
     aussi un équivalent MOBILE (#navMixer, tiroir de navigation, cf. index.html), qui doit rester stylé
     sous ce seuil de 1150px — seul body.hide-mixer-panel/.mixer-desktop restent desktop-only ici. */

  .page{max-width:100%;width:100%;display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden}
  
  .topbar .tbr{display:flex;gap:4px}
  .topbar{margin:0;padding:0}
  .topbar .ibtn{padding:6px 8px;font-size:.8rem}
  /* .logo n'a pas de hauteur de boîte propre (juste du texte centré par .tb1{align-items:center}). Le .82rem
     de base (pensé pour le root desktop à 26px, ~21px de corps de police) écrasait visuellement les boutons
     ronds/carrés de 28px à côté (cf. .ibtn) — corrigé une première fois à .62rem (~16px), jugé ensuite trop
     petit (le logo ne fait plus la hauteur des boutons voisins). .72rem (~19px) vise un compromis : plus
     proche des 28px de boîte des boutons sans retomber dans le poids visuel excessif du .82rem d'origine.
     Réduit uniquement ici, sur le seuil desktop — le .82rem de base reste inchangé pour mobile/tablette,
     où le root plus petit (18px, cf. html{font-size:} en tête de fichier) ne posait pas ce problème. */
  .topbar .logo{font-size:.72rem}

  /* Canvas/diagram scaling on desktop */
  .fbcard{max-width:none !important;width:auto !important;}
  #scaleFb,#scalePianoCanvas,#cofCanvas,#azPianoCanvas,#ivFb{
    max-width:100%;
    height:auto;
  }
  #strCanvas{max-width:none;}
  .page>style{display:none!important}
  #page-metro .play-btn{font-size:14px!important;height:34px;min-width:70px}
  /* Scopé à l'état "PLAY" uniquement (:not(.beat1)...:not(.beat12)) — cette règle (ID, donc plus spécifique
     que toute règle .metro-beatcount même !important) écrasait TOUJOURS la taille des chiffres de temps
     (1/2/3/4...) posée par les règles .beatN plus haut, quelle que soit leur valeur, réduisant même un
     "8rem" explicite à 20px fixe. Le confinement à "PLAY" (le seul texte pour lequel ce plafond a un sens,
     cf. le commentaire équivalent en mobile plus bas) restaure l'agrandissement demandé des chiffres tout en
     gardant "PLAY" à sa taille voulue. */
  #beatCount:not(.beat1):not(.beat2):not(.beat3):not(.beat4):not(.beat5):not(.beat6):not(.beat7):not(.beat8):not(.beat9):not(.beat10):not(.beat11):not(.beat12){font-size:20px!important;}
  #globalMetroBtn{font-size:22px!important;line-height:1;display:flex;align-items:center;justify-content:center}
  /* !important nécessaire : la règle de base .vgrid (hors media query, plus bas dans ce fichier — cascade
     par ordre d'apparition, pas de conflit de spécificité) fixe minmax(160px,...) pour mobile et gagnait
     silencieusement ICI aussi malgré ce bloc desktop, plafonnant les cartes à ~235px de large sur un grand
     écran quel que soit le plafond du canvas dans drawVoicing() côté JS (bug constaté : les diagrammes ne
     grandissaient pas malgré l'augmentation du plafond JS, cf. son commentaire 380→460).
     3 colonnes FIXES (pas auto-fit) — demande explicite : avec auto-fit, le nombre de colonnes qui tiennent
     réellement dépend du nombre de cartes ET de leur largeur, donc change d'une qualité d'accord à l'autre
     (ex. 3 voicings pour un accord maj, 5 pour un accord m7) — chaque diagramme changeait alors de position
     visuelle au clic sur une autre qualité, rendant impossible de suivre un intervalle précis (ex. la tierce)
     d'un voicing à l'autre. Un nombre de colonnes fixe garde la même position quel que soit le nombre de
     cartes (les qualités avec moins de voicings laissent simplement des cases vides en fin de grille). */
  .vgrid{grid-template-columns:repeat(3,1fr)!important}
  .vcard{max-width:480px}
  .vcard.expanded{max-width:none}
  /* Empileur d'intervalles agrandi sur desktop — même retour explicite que les diagrammes ci-dessus (trop
     petit à 100% de zoom navigateur sur grand écran). Élargit la tour + sa légende (nom d'étage/sélecteur de
     crans) proportionnellement, pour rester lisible sans avoir à monter le zoom du navigateur. */
  .ch-stack-col{max-width:620px}
  .ch-stack-tower-row{min-height:46px}
  .ch-stack-key{width:58px;font-size:.95rem}
  .ch-stack-tier-lbl{width:104px;font-size:max(10px,.58rem)}
  .ch-stack-cran{padding:4px 9px;font-size:.68rem}
  .ch-stack-result{font-size:1.2rem}
  /* 2 colonnes desktop : diagrammes à gauche, empileur d'intervalles à droite, chacun avec son propre
     scroll indépendant. Remplace un 1er essai (grille seule limitée en hauteur, empileur en flux juste en
     dessous) qui réglait bien le "saut" de l'empileur au changement d'empilement, mais créait un nouveau
     problème signalé par l'utilisateur : l'empileur déplié pouvait occuper toute la hauteur visible,
     masquant complètement les diagrammes au-dessus (les deux partageaient la même colonne verticale, donc
     le même budget d'espace total). Deux colonnes séparées : chacune a sa propre hauteur disponible, l'une
     ne peut plus jamais grignoter l'espace de l'autre. .chords-cols wrapper ajouté dans le HTML (autour de
     #voicingGrid et #chStackBuilder uniquement, PAS #crHeader qui reste pleine largeur au-dessus). */
  .chords-cols{flex-direction:row;align-items:flex-start}
  #voicingGrid{max-height:calc(100vh - 200px);overflow-y:auto;flex:1;min-width:0}
  #chStackBuilder{width:340px;flex-shrink:0;max-height:calc(100vh - 200px);overflow-y:auto;margin-top:0}
  /* .diat-play exclu : ce bouton vit dans une rangée normale (pas une .vcard), position:static forcé plus
     bas dans le fichier (cf. .vcard-play.diat-play) — top/right n'auraient de toute façon aucun effet sur
     un élément static, mais l'exclusion documente explicitement l'intention plutôt que de compter sur ce
     comportement implicite. */
  .vcard-play:not(.vcard-midi):not(.vcard-png):not(.vcard-clear-centered):not(.diat-play){top:30px!important;right:10px!important}
  /* Symétrique du bouton ♪ ci-dessus (même top/inset, juste left au lieu de right) — sans cette règle,
     .vcard-zoom gardait sa position de base (top:26px;left:5px, pensée pour le mobile) alors que .vcard-play
     était décalé à top:30px;right:10px ici en desktop uniquement : les deux boutons n'étaient donc PAS
     désalignés en soi, mais leur désalignement changeait de sens/ampleur entre desktop et mobile — chacun
     réagissant à une règle différente (constaté en usage : cohérent sur desktop, décalé sur mobile). */
  .vcard-zoom{top:30px!important;left:10px!important}
  #voicingGrid{
    justify-items:start;
  }

  /* Rythme desktop : cellules rectangulaires horizontales, hauteur fixe cohérente avec la largeur fixe de
     97px posée par JS (rhRenderGrid) — ~48px = même ratio 2/1 que la règle de base (styles.css ~2137),
     hauteur fixe plutôt qu'aspect-ratio ici pour ne pas dépendre de la largeur réellement calculée par la
     grille (peut différer légèrement de 97px en cas d'arrondi navigateur). */
  #page-rhythm .rh-cell{aspect-ratio:unset!important;height:48px;}
  #page-rhythm .rh-cell .rh-arrow{font-size:16px!important;}
  #page-rhythm .rh-cell .rh-lbl{font-size:9px!important;}
  #page-rhythm .rh-play-btn{height:36px;min-width:52px;font-size:14px!important;display:flex;align-items:center;justify-content:center;line-height:1;}
  #page-rhythm .rh-tempo{font-size:18px!important;}
  #page-rhythm .rh-wrap{max-width:1600px;width:100%;margin-left:auto;margin-right:auto;}
  #page-metro .wrap{max-width:1600px;width:100%;margin-left:auto;margin-right:auto;}
  /* Cercle du métronome agrandi sur desktop : le wrapper .metro-ring-outer est passé à width:100% (cf.
     index.html, plafonné à max-width:560px) pour remplir quasiment toute la largeur de sa colonne au lieu
     d'une taille fixe sous-dimensionnée. Le SVG (anneau + aiguille) suit automatiquement via width:100%/
     height:100% sur ce wrapper. Le disque central .metro-circle et la police du compteur sont en % du
     conteneur (cqw, cf. metro-circle-wrapper container-type plus bas) pour rester proportionnels à la
     taille réelle de l'anneau plutôt que fixes en px, qui paraissaient soit trop petits soit trop grands
     selon la largeur d'écran réelle entre 1300px et le plafond de 1600px.
     Plafond réduit ~20% (560px→440px, 56vh→44vh, demande explicite : "Métronome et Drone moins cohérents
     avec le reste de l'app à 100% de zoom que les autres modules") — l'inline style d'origine
     (min(100%,560px,56vh)) reste la valeur de base pour mobile, cette règle ne s'applique qu'à
     .metro-ring-outer sous ce media query desktop. */
  #page-metro .metro-ring-outer{width:min(100%,440px,44vh)!important;}
  #page-metro .metro-circle-wrapper{container-type:inline-size}
  /* container-type:size (pas seulement inline-size) posé ICI, sur .metro-circle lui-même — les chiffres de
     temps (.metro-beatcount.beatN, cqw ci-dessous) doivent suivre la taille RÉELLE du disque après clamp
     (min/max-width 110-160px), pas celle du wrapper englobant (jusqu'à 440px) dont ils dérivaient jusqu'ici
     via l'ancien container sur .metro-circle-wrapper : le disque étant presque toujours bloqué sur son
     plafond de 160px (rarement les 35cqw bruts du wrapper), une taille de police calculée en % du wrapper
     produisait un texte bien plus grand que le disque qui le contient (bug rapporté : chiffres énormes,
     débordant largement du cercle, illisibles). Un container dédié sur .metro-circle avec container-type:size
     (pas inline-size seul : nécessaire pour qu'un width/height CLAMPÉ, pas juste défini, serve de référence
     aux cqw internes) résout ce décalage à la source. */
  #page-metro .metro-circle{width:35cqw;height:35cqw;min-width:110px;max-width:160px;min-height:110px;max-height:160px;container-type:size;}
  /* !important nécessaire : la règle de base #page-metro .metro-beatcount.beatN{font-size:8rem} (hors media
     query, plus bas dans ce fichier) a la MÊME spécificité que celle-ci — une media query ne modifie QUE la
     condition d'application d'une règle, jamais sa spécificité ; la cascade normale départage alors par ordre
     d'apparition dans le fichier, et la règle de base (déclarée après celle-ci) gagnait silencieusement même
     ici, à l'intérieur du media query desktop actif (même mécanisme que #page-metro .play-btn/#beatCount
     juste au-dessus dans ce même bloc, qui utilisent déjà !important pour la même raison). */
  #page-metro .metro-beatcount{font-size:26cqmin!important;}
  #page-metro .metro-beatcount.beat1,#page-metro .metro-beatcount.beat2,#page-metro .metro-beatcount.beat3,#page-metro .metro-beatcount.beat4,#page-metro .metro-beatcount.beat5,#page-metro .metro-beatcount.beat6,#page-metro .metro-beatcount.beat7,#page-metro .metro-beatcount.beat8,#page-metro .metro-beatcount.beat9,#page-metro .metro-beatcount.beat10,#page-metro .metro-beatcount.beat11,#page-metro .metro-beatcount.beat12{font-size:58cqmin!important;}
  /* .az-wrap n'a plus besoin d'un override ici : la règle générique ci-dessus (".page > *:not(div:first-
     child):not(.sticky-toolbar)") lui donne déjà max-width/centrage/overflow:hidden identiques à ce qu'on
     veut désormais — .az-wrap ne scrolle plus lui-même, c'est .az-rescard (résultats) qui a sa propre
     région de scroll pendant que le manche/clavier restent verrouillés au-dessus (cf. .az-wrap/.az-rescard
     dans ce fichier). Un override existait ici quand .az-wrap devait scroller globalement (bug historique
     "module analyseur n'est pas scrollable") — devenu redondant avec ce changement de structure. */
  #page-drone .drone-wrap{max-width:1600px;width:100%;margin-left:auto;margin-right:auto;}
  /* Plafonné sur .rd-wrap (le conteneur commun) plutôt que séparément sur .rd-sticky-player et
     .rd-scroll-body — une divergence de calcul entre marge externe (sticky-player) et padding interne
     (scroll-body, cf. règle de base plus bas) faisait que les deux n'atteignaient PAS la même largeur
     finale malgré des formules en apparence équivalentes (bug rapporté : décalage gauche/droite entre le
     titre+BPM et les cartes en dessous). Plafonner une seule fois le parent, laisser les deux enfants
     hériter de width:100% (déjà leur comportement par défaut, cf. règles de base), élimine toute
     divergence par construction — même stratégie que #page-metro .wrap/#page-drone .drone-wrap ci-dessus. */
  #page-eartraining .rd-wrap{max-width:1600px;width:100%;margin-left:auto;margin-right:auto;}
  /* Accords : même écrasement pour .chords-main (conteneur scrollable réel du module, cf. plus bas dans ce
     fichier) — resté invisible tant que la barre d'empilement des intervalles était en position:sticky
     (elle restait à l'écran même sans vrai scroll fonctionnel derrière), mais devenu bloquant une fois le
     sticky retiré : la barre partait alors hors champ, inatteignable, sans aucun moyen de scroller jusqu'à
     elle (overflow:hidden empêche tout scroll malgré un scrollHeight > clientHeight réel). */
  #page-chords .chords-main{overflow:auto!important;}
  #page-rhythm .rh-sticky-player{max-width:none;margin-left:0;margin-right:0;}
  #page-rhythm .rh-scroll-body{max-width:none;}

  /* Cercle des quintes : layout horizontal desktop, bloc centré */
  #page-circle .cof-page{
    flex-direction:row;
    align-items:flex-start;
    justify-content:center;
    gap:16px;
  }
  #page-circle .cof-canvas-wrap{
    width:50%;
    /* Plafond proportionnel (clamp), pas une valeur fixe : un max-width:440px codé en dur convenait au
       cas d'origine (2560×1440 zoomé à 200% ≈ 1280px logiques) mais bridait le cercle à cette même taille
       sur un écran plus large non zoomé (ex. 3440×1440 à 100% ≈ 1650-1700px logiques disponibles pour
       cette colonne) — bug rapporté explicitement ("le cercle est trop petit"). clamp(360px, 32vw, 620px) :
       plancher proche de l'ancien plafond (reste raisonnable sur petit écran zoomé), grandit avec la
       largeur réelle de viewport au lieu de rester figé, plafonné à 620px pour ne jamais affamer
       .cof-right (grille d'accords diatoniques) même sur un très grand écran.
    */
    max-width:clamp(360px,32vw,620px);
    flex-shrink:0;
  }
  #page-circle #cofCanvas{
    width:100%!important;
    height:auto!important;
  }
  #page-circle .cof-right{
    flex:1 1 480px;
    max-width:900px;
    min-width:0;
    display:flex;
    flex-direction:column;
    gap:8px;
    padding-top:8px;
  }
  #page-circle .cof-deg-table{gap:10px;padding:12px 8px 8px}
  #page-circle .cof-deg-row{gap:10px}
  #page-circle .cof-deg-label{font-size:.6rem;width:56px}
  #page-circle .cof-deg-cell{width:68px;height:68px}
  /* #cofDegTable (index.html) est capé à max-width:480px en base (dimensionné pour les cellules 42px
     mobile) — jamais relevé quand les cellules sont passées à 68px + label à 56px sur ce breakpoint
     desktop, donc la rangée (56 + 7×68 + 8 gaps ≈ 612px) débordait son propre conteneur des deux côtés
     (centrage flex sur contenu plus large que la boîte). Le débordement à gauche recouvrait le canvas du
     cercle, qui interceptait alors tous les clics sur le label "▶ Maj/Min" (cf. cofPlayRow) — le bouton
     semblait ne rien faire alors que sa logique JS fonctionnait (vérifié par appel direct). */
  #page-circle .cof-deg-table{max-width:620px}
  /* Palier 1024-1279px (audit 2026-10-02) : la colonne à droite du cercle n'offre que ~430px — la rangée de
     612px était coupée des deux côtés (labels ▶ Maj/Min et 7e degré). Cellules et écarts réduits pour tenir. */
  @media (max-width:1279px){
    #page-circle .cof-deg-table,#page-circle .cof-deg-row{gap:5px}
    #page-circle .cof-deg-label{width:40px}
    #page-circle .cof-deg-cell{width:50px;height:50px}
  }
  /* Structures : canvas pleine largeur, éditeur en dessous aligné à gauche.
     BUG CORRIGÉ (rapporté : "énorme espace vide au-dessus du player" en vue Backing Vidéo) : ce sélecteur
     ciblait #page-structures > div:last-child, qui visait #strNativeView tant qu'il était le dernier enfant
     direct de la page — l'ajout de #strYoutubeView APRÈS lui (Phase 8, YouTubeBacking) en a fait le nouveau
     :last-child, donc height:100%/max-width:none/etc. s'appliquaient désormais à la vue Vidéo (contenu
     compact) au lieu de la vue Native (canvas+timeline, censée remplir toute la hauteur) — d'où le grand
     vide sous le player. Fix : cibler #strNativeView explicitement par id plutôt que par position DOM. */
  #page-structures #strNativeView{
    max-width:none;
    flex-direction:column !important;
    align-items:stretch;
    gap:12px;
    overflow-y:auto;
    overflow-x:hidden;
    height:100%;
  }
  /* Bloc canvas + timeline : pleine largeur */
  #page-structures .str-canvas-col{
    flex-shrink:0;width:100%;
    display:flex;flex-direction:column;gap:8px;
  }
  /* Timeline : reste horizontale sous le canvas */
  #page-structures #strTimeline{
    flex-direction:row !important;
    /* overflow-y:visible (pas hidden) : le scroll voulu ici est HORIZONTAL uniquement (navigation entre
       sections, largeur de bloc fixe) — overflow-y:hidden tronquait le bas des blocs dont les pastilles
       d'accords wrappent sur plusieurs lignes (accords nombreux/noms longs dépassant min-height:145px de
       .str-block), signalé explicitement par capture. visible laisse chaque bloc dicter sa propre hauteur
       réelle au conteneur flex plutôt que de couper ce qui dépasse une hauteur implicite. */
    overflow-x:auto;overflow-y:visible;
    width:auto;max-width:none;
    flex-shrink:0;
    scrollbar-width:thin;
    padding-bottom:8px;
    padding-left:32px;padding-right:32px;
    scroll-behavior:smooth;
  }
  #page-structures .str-timeline-row{flex-shrink:0}
  #page-structures #strTimeline::-webkit-scrollbar{height:8px}
  #page-structures #strTimeline::-webkit-scrollbar-thumb{background:var(--bd2);border-radius:4px}
  #page-structures #strTimeline::-webkit-scrollbar-track{background:transparent}
  #page-structures .str-tl-nav{
    display:flex!important;align-items:center;justify-content:center;
    position:absolute;top:0;bottom:8px;width:26px;z-index:5;
    border:1px solid var(--bd);border-radius:var(--r);
    background:var(--sf);color:var(--tx2);cursor:pointer;font-size:1rem;
    opacity:.85;transition:opacity var(--tr),color var(--tr);
  }
  #page-structures .str-tl-nav:hover{opacity:1;color:var(--tx)}
  #page-structures .str-tl-nav-l{left:0}
  #page-structures .str-tl-nav-r{right:0}
  /* Contrôles multipiste : LARGEUR RÉDUITE, alignée sur l'éditeur de section (demande explicite :
     "uniformiser la largeur des corps sur desktop... controle multipiste et editeur section sur une
     largeur réduite [comme actuellement l'editeur de section]") — #strTrackRows héritait jusqu'ici du
     même comportement pleine largeur que le canvas/les blocs de section (mesuré : 1714px sur un viewport
     1920px, contre 1600px pour #strSectionEditor juste en dessous) alors que canvas et contrôles par piste
     n'ont pas la même vocation : le canvas/la frise bénéficient de tout l'espace pour la lecture temporelle,
     les lignes de contrôle par piste (curseurs, menus) restent plus lisibles resserrées, comme le panneau
     d'édition de section qui les suit. Même max-width/align-self que #strSectionEditor pour un alignement
     visuel cohérent entre les 2 blocs. */
  #page-structures #strTrackRows{
    width:100%;
    max-width:1600px;
    align-self:center;
  }
  /* Éditeur : sous le canvas/timeline, aligné à gauche, toujours visible */
  #page-structures #strSectionEditor{
    width:100%;
    max-width:1600px;
    align-self:center;
    flex-shrink:0;
    overflow-y:visible;
    display:flex!important;
    background:var(--sf);
    border:1px solid var(--bd);
    border-radius:var(--r);
    padding:12px;
    gap:10px;
    flex-direction:column;
  }

  /* Structures topbar : sélecteur template limité */
  #page-structures #strTemplateSelect{max-width:180px;}

  /* Harmony : pills mode plus petites */
  .hn-mode-row .hn-mpill{font-size:12px!important;}

  /* Drone : layout 2 colonnes sur desktop, cartes uniformisées avec le reste de l'app.
     flex-wrap (pas grid à remplissage automatique) : l'ancienne grille alternait gauche/droite dans l'ordre
     du DOM sans notion de section, désalignant les cartes dès que "Drone tonal" et "Bruit ambiant/Focus"
     n'avaient pas le même nombre de cartes chacun (bug rapporté — pas de séparation visuelle claire à
     droite, cartes en face à face décalées). .drone-col (cf. index.html #droneToneCol/#droneFocusCol)
     regroupe désormais chaque section dans son propre bloc : "Fréquence" + "Slider Hz" gardent width:100%
     pour occuper toute la rangée en premier (pleine largeur, comme avant), puis les deux .drone-col
     (largeur naturelle ~50% chacun via flex:1) se placent l'un à côté de l'autre sur la rangée suivante et
     empilent CHACUN ses propres cartes verticalement, indépendamment du nombre de cartes en face. */
  #page-drone > div:first-child{
    display:flex!important;
    /* flex-direction:row!important nécessaire : .drone-wrap porte flex-direction:column en style inline
       (index.html), jamais contesté ici avant ce correctif (display:grid n'était pas concerné par cette
       propriété). Avec column+wrap, flexbox empile les enfants qui débordent en NOUVELLES COLONNES
       côte à côte plutôt qu'en nouvelles lignes — d'où le bug rapporté : 3 "colonnes" avec scroll
       horizontal au lieu d'une ligne pleine largeur suivie de 2 vraies colonnes. */
    flex-direction:row!important;
    flex-wrap:wrap;
    align-items:flex-start!important;
    /* 1600px (pas 900px) : alignée sur #page-metro .wrap, le module le plus large de l'app — le Drone était
       nettement plus étroit que tous les autres modules desktop sans raison particulière (demande
       utilisateur). */
    max-width:1600px;
    margin:0 auto;
    padding:1rem 1.5rem;
    gap:10px!important;
  }
  /* Fréquence + Slider Hz : pleine largeur (1600px, pas de plafond propre) comme les 2 colonnes en dessous —
     forment ensemble une seule "rangée pleine largeur" alignée sur le reste du module plutôt qu'un bloc
     centré isolé avec du vide de chaque côté (essai précédent écarté par l'utilisateur). Leur contenu
     interne reste centré (text-align:center/justify-content:center déjà en place dans le HTML), donc rien
     ne s'étire de façon disproportionnée malgré la carte plus large. */
  #page-drone > div:first-child > div:nth-child(1),
  #page-drone > div:first-child > div:nth-child(2){
    width:100%;
    max-width:none!important;
  }
  #page-drone > div:first-child > .drone-col{
    display:flex;flex-direction:column;gap:4px;
    flex:1 1 0;min-width:280px;max-width:none!important;
  }
  /* max-width:400px inline sur chaque .drone-card (utile en mobile, une seule colonne centrée) doit être
     neutralisé une fois niché dans .drone-col — sinon les cartes restent plafonnées à 400px même dans une
     colonne desktop plus étroite ou plus large que ça, cassant l'alignement visé par ce layout. */
  #page-drone > div:first-child > div{max-width:none!important;}
  #page-drone > div:first-child .drone-col > *{max-width:none!important;}
  #page-drone .drone-card{
    background:var(--sf);
    border:1px solid var(--bd);
    border-radius:var(--rl);
    padding:0.4rem 0.7rem;
  }
  #droneOctRow{flex-wrap:nowrap!important;}
  #droneOctRow .pill{font-size:.62rem!important;padding:6px 8px!important;}
  /* Bouton ▶ du Drone : 44px de base (cf. index.html) paraît disproportionné à côté des boutons ibtn/pill
     du reste de l'app (28px, cf. .ibtn) une fois sur desktop — signalé explicitement ("Métronome et Drone
     moins cohérents avec le reste de l'app à 100% de zoom que les autres modules"). ~36px (-18%) desktop
     uniquement, mobile inchangé (44px reste une cible tactile appropriée là où le zoom navigateur n'entre
     pas en jeu). */
  #dronePlayBtn{width:36px!important;height:36px!important;font-size:.95rem!important}
}

/* Mixeur intégré (mode "Dans la sidebar", cf. setMixerMode) — NON scopé à un media query, contrairement à
   .mixer-desktop plus haut : ce panneau a 2 emplacements possibles, #sidebarMixer (desktop, dans
   .sidebar-desktop) ET #navMixer (mobile, dans le tiroir #navSheet, cf. index.html) — mixerRenderPanel()
   choisit dynamiquement lequel remplir selon window.innerWidth, jamais les deux en même temps (cf. son
   commentaire dans app.js). Ces règles doivent donc rester actives des 2 côtés du breakpoint 1150px.
   body.hide-inline-mixer masque le panneau ET son séparateur (#sidebarMixerSep desktop, #navMixerSep
   mobile) quand mixerMode n'est pas 'inline' — un seul point de contrôle CSS pour les 2 emplacements. */
body.hide-inline-mixer #sidebarMixerSep,body.hide-inline-mixer #navMixerSep,body.hide-inline-mixer .mixer-inline{display:none}
.mixer-inline{display:flex;flex-direction:column;gap:8px;padding:6px 10px 4px;font-size:.72rem}
.mixer-inline .mixer-channel{display:flex;flex-direction:column;gap:2px}
.mixer-inline .mixer-channel-label{color:var(--tx2);font-size:.6rem;text-transform:uppercase;letter-spacing:.02em}
.mixer-inline .mixer-channel-val{color:var(--tx3);font-size:max(10px,.58rem);text-align:right}
.mixer-inline .mixer-channel-label.playing{color:var(--c-root)}
.mixer-inline .mixer-channel-label.playing::before{content:'●';display:inline-block;margin-right:4px;font-size:max(10px,.55rem)}
.mixer-inline .mixer-channel-head{display:flex;align-items:center;gap:4px}
.mixer-inline .mixer-channel-arrow{cursor:pointer;user-select:none;color:var(--tx3);flex-shrink:0;width:12px;text-align:center}
.mixer-inline .mixer-channel-arrow:hover{color:var(--tx)}
.mixer-inline .mixer-subtracks{display:flex;flex-direction:column;gap:6px;margin-top:4px;padding-left:8px;border-left:2px solid var(--bd)}
.mixer-inline .mixer-subtrack{display:flex;flex-direction:column;gap:2px}
.mixer-inline .mixer-subtrack-label{color:var(--tx3);font-size:max(10px,.58rem)}

/* Mixeur SIDEBAR (desktop) ET tiroir mobile en rangée de faders verticaux (demande explicite : "dans la
   sidebar, plutôt utiliser des sliders verticaux. Afficher le texte du module lié au slider DANS le
   slider, verticalement, roté à 90° antihoraire" puis étendue : "les ajustements du mixeur doivent aussi
   s'appliquer à la sidebar mobile") — .mixer-inline-vertical posé sur #sidebarMixer ET #navMixer (cf.
   mixerRenderPanel, mode 'inline' commun aux deux, seul le conteneur cible change selon la largeur réelle
   de la fenêtre) ; #desktopMixer (panneau séparé, mode 'mixer'/'both') garde seul le layout .mixer-inline
   d'origine ci-dessus, jamais concerné par cette demande.
   flex-wrap:wrap plutôt qu'un overflow-x:auto : 7 canaux à ~26px chacun tiennent difficilement sur 190px de
   large en une seule rangée sans dépasser — un retour à la ligne (2 rangées de faders) reste plus lisible
   qu'un défilement horizontal caché, y compris dans le tiroir mobile plus large mais pas illimité. */
/* BUG CORRIGÉ (rapporté : "la sidebar mobile affiche 5 et 3 sliders au lieu de 4 et 4") — flex-wrap laissait
   le nombre de faders par rangée dépendre de la largeur RÉELLE du conteneur, qui diffère entre la sidebar
   desktop et le tiroir mobile (#navMixer, cf. .nav-popup, largeur variable 140-200px selon le contenu) :
   plus de largeur disponible => la 5e colonne passait avant le retour à la ligne. grid à 4 colonnes FIXES
   garantit le même découpage (4 puis 4) quel que soit le conteneur, les 2 partageant maintenant cette
   même classe (cf. mixerRenderPanel/isVertical). */
.mixer-inline-vertical{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;padding:6px 10px;justify-items:center}
.mixer-channel-v{display:flex;flex-direction:column;align-items:center;gap:3px;width:26px;flex-shrink:0}
/* Hauteur fixe (pas flex:1 sur toute la sidebar) : une hauteur de fader qui varie avec le nombre de canaux
   affichés serait déroutante d'une session à l'autre (Backing/SURFACE apparaissent seulement une fois des
   sections existent, cf. _syncGlobalStrPlayBtn) — 120px est un compromis lisible sans dominer toute la
   colonne de navigation au-dessus de laquelle ce mixeur est empilé. */
/* border ICI (pas seulement sur .mixer-slider-v, cf. plus bas) — BUG CORRIGÉ (rapporté avec screenshot :
   "petit bug d'affichage sur les sliders qui n'ont pas été bougés", les faders à 100% de valeur initiale
   apparaissaient comme des blocs gris plats sans aucun contour, contrairement à ceux <100% où le vide
   au-dessus du remplissage laissait deviner la bordure de l'input en dessous) — le contour du track ne doit
   pas dépendre du remplissage qui le recouvre entièrement à 100%, d'où une bordure portée par le track
   LUI-MÊME, toujours visible quelle que soit la valeur du fader. */
.mixer-fader-track{position:relative;width:100%;height:120px;border-radius:4px;overflow:hidden;border:1px solid var(--bd);contain:layout paint style}
/* writing-mode:vertical-lr tout seul écrit le texte de haut en bas (comme .mel-zoom-v ci-dessus, pensé
   pour un curseur qu'on lit indifféremment) — "roté à 90° antihoraire" veut dire l'inverse : le début du
   texte en BAS, qui se lit en tournant la tête vers la GAUCHE. rotate(180deg) après vertical-lr renverse
   l'ordre de lecture sans changer l'axe (toujours vertical), obtenant exactement cette orientation. */
.mixer-slider-v{
  /* BUG CORRIGÉ (rapporté : "rendre leur apparence précédente aux sliders (bloc rectangulaire coins
     arrondis) plutôt qu'un slider étroit") — appearance:slider-vertical (utilisé au 1er jet, même recette
     que .mel-zoom-v ci-dessus) déclenche le rendu natif ANCIEN STYLE du navigateur pour un slider vertical,
     un système de dessin ENTIÈREMENT différent de celui utilisé par MIXER_SLIDER_CSS (les faders
     horizontaux, jamais touchés par cette demande) — celui-ci pose appearance:none, ce qui laisse
     accent-color peindre le même bloc rectangulaire à bords arrondis que sur les faders horizontaux,
     seulement tourné de 90°. writing-mode:vertical-lr ne sert plus ici qu'à donner l'AXE de glisser
     (vertical), pas l'apparence — d'où appearance:none combiné à lui, plutôt que slider-vertical seul.
     DEMANDE SUIVANTE : "remplacer le bouton du slider par un remplissage translucide couleur d'accent" —
     le curseur natif (thumb rond, accent-color) est retiré ci-dessous ; le remplissage est désormais un
     VRAI ÉLÉMENT séparé (.mixer-fader-fill, cf. plus bas), positionné par JS à chaque changement de valeur
     — accent-color seul ne permet pas la translucidité demandée (il peint un thumb opaque, jamais un
     dégradé de remplissage), d'où ce chemin entièrement custom plutôt qu'une variante d'accent-color. */
  -webkit-appearance:none;-moz-appearance:none;appearance:none;writing-mode:vertical-lr;direction:rtl;
  /* -moz-orient:vertical : BUG CORRIGÉ (rapporté avec screenshot Firefox : "pas du tout translucide" — un
     bloc gris uni couvrait tout le fader quel que soit le % réel). writing-mode:vertical-lr seul ne fait
     tourner que la BOÎTE de l'input sous Firefox, pas la géométrie interne que le moteur utilise pour
     calculer/peindre ::-moz-range-track et ::-moz-range-progress — ceux-ci restaient calculés comme si le
     slider était HORIZONTAL et pleine largeur, d'où le thème natif gris repeint par-dessus nos overrides
     transparent (perdants face au fallback du thème dans cet état). -moz-orient:vertical donne à Firefox la
     vraie orientation native du slider, seul moyen fiable d'obtenir un ::-moz-range-track qui respecte
     enfin nos règles transparent ci-dessous (confirmé sur Chrome/WebKit sans ce correctif, jamais concerné
     par cette propriété non standard, propre à Firefox). */
  -moz-orient:vertical;
  /* border retirée d'ici (posée dorénavant sur .mixer-fader-track lui-même, cf. son commentaire) — la
     garder ICI EN PLUS aurait dessiné 2 contours superposés (2px visibles au lieu d'1) puisque l'input
     occupe déjà toute la surface du track (width/height:100%). */
  width:100%;height:100%;border:none;background:transparent;border-radius:4px;
  cursor:pointer;margin:0;position:relative;z-index:2;
}
/* Piste ET curseur natifs rendus invisibles (le remplissage custom ci-dessous les remplace visuellement),
   tout en gardant l'input RÉELLEMENT fonctionnel par-dessus (z-index:2 ci-dessus, au-dessus du fill) :
   c'est lui qui reçoit le clic/glisser, jamais le fill (cf. pointer-events:none dessus). */
.mixer-slider-v::-webkit-slider-runnable-track{background:transparent}
.mixer-slider-v::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:100%;height:1px;background:transparent;border:none;box-shadow:none}
.mixer-slider-v::-moz-range-track{background:transparent!important;border:none}
.mixer-slider-v::-moz-range-thumb{width:1px;height:1px;background:transparent;border:none;box-shadow:none;opacity:0}
.mixer-slider-v::-moz-focus-outer{border:0}
.mixer-slider-v::-moz-range-progress{background:transparent!important}
/* Remplissage — translucide, couleur d'accent, ANCRÉ EN BAS (une valeur de mixeur qui monte remplit vers le
   haut, comme un vrai fader physique). Hauteur posée en style inline par JS (cf. _mixerSetFillV dans
   app.js), jamais en CSS pur : un input[type=range] ne calcule sa progression via aucune pseudo-classe/
   variable CSS accessible ici (::-moz-range-progress existe mais son équivalent WebKit n'a pas d'API
   fiable équivalente) — measurer/écrire la hauteur en JS à chaque 'input' reste la seule méthode qui
   fonctionne identiquement sur les 2 moteurs. pointer-events:none : ne doit jamais intercepter le glisser,
   qui reste géré par l'input (relevé au-dessus en z-index). */
.mixer-fader-fill{
  position:absolute;left:0;bottom:0;width:100%;height:0%;
  /* BUG CORRIGÉ 3x (rapporté avec screenshots Chrome desktop ET Firefox : "pas du tout translucide") —
     2 pistes essayées et rejetées : (1) opacity:.16 en background simple restait perçu comme un aplat gris
     pâle, pas comme un "voile" ; (2) mix-blend-mode:overlay sur fond déjà très sombre (--s3) n'éclaircit
     presque rien (overlay a un effet minimal sur des couleurs proches du noir), rendant le remplissage
     quasi invisible — pire que le problème initial. Solution retenue : dégradé (pas un aplat) + une bordure
     HAUTE nette (border-top) qui matérialise le niveau du fader comme une VRAIE ligne de niveau (repère
     visuel qu'un simple aplat ne donne pas), combinés à une opacité modérée qui reste nettement plus claire
     que le fond du track SANS atteindre le blanc plein — la juxtaposition dégradé+ligne de crête est ce qui
     lit visuellement comme "translucide" plutôt que color:blanc à un %. */
  background:linear-gradient(rgba(255,255,255,.22),rgba(255,255,255,.1));
  border-top:1px solid rgba(255,255,255,.55);
  pointer-events:none;z-index:1;
  /* pas de transition : elle retardait chaque pas de 50 ms (glisser en retard sur le doigt, surtout WebView2) */
  will-change:height;
}
/* Libellé SUPERPOSÉ au fader (position:absolute, même boîte que .mixer-fader-track), pas à côté ni
   au-dessus — c'est la demande explicite : le texte doit apparaître DANS le slider. pointer-events:none
   impératif : sans lui, le texte intercepterait le glisser du curseur au lieu de laisser l'input en
   dessous le recevoir. rotate(-90deg) : lecture de bas en haut (antihoraire), cf. commentaire ci-dessus —
   indépendant de l'orientation du writing-mode du input, qui ne sert ici qu'à donner sa géométrie verticale
   au track, pas à orienter ce texte-ci.
   ALIGNEMENT (demande explicite : "texte dans les sliders, aligné à gauche (donc en bas)") — le texte
   n'est plus centré dans le track mais ANCRÉ EN BAS : left:6px place le bord GAUCHE du texte NON tourné à
   6px du bord gauche du track, transform-origin:left center fait pivoter autour de CE bord (pas du centre
   du texte) — après rotate(-90deg), ce point d'ancrage se retrouve près du BAS du track (l'axe X devient
   l'axe Y), et le texte "part" de là vers le haut, exactement l'équivalent visuel d'un texte aligné à
   gauche une fois remis à la verticale. bottom (pas top) + transform pour positionner ce point d'ancrage :
   translateY(-50%) recentre seulement sur la LARGEUR du texte tourné (qui devient sa largeur visuelle
   verticale), sans quoi le texte entier serait décalé au lieu d'être simplement callé à gauche. */
.mixer-channel-label-v{
  /* BUG CORRIGÉ (rapporté : "le texte n'est pas du tout lisible, même à 0%" — donc indépendant du
     remplissage, cf. .mixer-fader-fill juste au-dessus) — vérifié via getBoundingClientRect() : le texte
     rendait à x:21-41 alors que le track fait x:35-61, quasi tout le texte tombait HORS du track (qui a
     overflow:hidden) et était donc rogné, peu importe la couleur/opacité du remplissage. Cause : left:6px
     positionne le bord gauche du texte NON tourné à 6px du bord gauche du track, mais rotate(-90deg) pivote
     autour de ce point SANS le garder à l'intérieur du track — la largeur du texte (~77px pour "Métronome")
     part alors presque entièrement vers la gauche, hors la boîte de 26px de large. Fix : ancrer avec
     bottom:0/left:50% (centre horizontal du track, qui devient l'axe de rotation) et ne décaler qu'après
     coup avec translate(-50%,-8px) rotate(-90deg) — translate AVANT rotate s'applique dans les coordonnées
     non tournées (centre du track), rotate ensuite fait pivoter le texte autour de ce point désormais
     correctement à l'intérieur du track. */
  /* Rotation d'un texte dont la largeur (non tournée) dépasse largement les 26px du track : la seule
     méthode fiable est de placer le POINT DE PIVOT exact où on veut que le texte apparaisse (ici : centré
     horizontalement dans le track, ancré près du bas), puis de faire tourner le texte AUTOUR de ce point en
     utilisant transform-origin sur ce même point relatif au texte lui-même (0 = bord gauche du texte non
     tourné, qui devient son bord BAS une fois tourné à -90°) — width:max-content nécessaire pour que le
     texte garde sa largeur naturelle malgré position:absolute (qui sinon le réduirait à la largeur du
     parent, 26px, et le ferait retourner à la ligne malgré white-space:nowrap). */
  /* Alignement à gauche (demande explicite : "aligne bien à gauche (en bas)") — vérifié par mesure : changer
     `left` seul ne déplace PAS le texte horizontalement dans le track (la position visuelle horizontale est
     fixée par transform-origin/bottom, pas par left, une fois le texte tourné) — le centrage restait
     identique quel que soit left. Le vrai levier pour "aligné à gauche, donc en bas" (texte qui part du bas
     du track plutôt que d'être centré verticalement dans sa hauteur) est `bottom` : ramené de 10px à 2px
     pour caler le début du texte tout en bas du track plutôt qu'à mi-hauteur. left repassé à 50% (centre du
     track, seule valeur confirmée ne PAS faire sortir le texte de la zone overflow:hidden). */
  position:absolute;left:50%;bottom:2px;width:max-content;transform-origin:0 50%;
  transform:rotate(-90deg);
  white-space:nowrap;pointer-events:none;
  font-family:var(--fm);font-size:max(10px,.52rem);text-transform:uppercase;letter-spacing:.02em;
  /* Couleur adoucie (demande explicite : "réduis légèrement sa couleur de blanc pur à gris léger") —
     var(--tx) est blanc quasi pur dans ce thème ; var(--tx2) (gris clair intermédiaire du projet, déjà
     utilisé ailleurs pour un texte secondaire toujours lisible) apporte la nuance demandée sans retomber
     dans l'illisibilité du contraste initial qui avait motivé var(--tx) ici. Ombre portée conservée
     (nécessaire contre le remplissage translucide, cf. .mixer-fader-fill). */
  color:var(--tx2);text-shadow:0 0 3px var(--s3),0 0 3px var(--s3);z-index:3;
  max-width:106px;overflow:hidden;text-overflow:ellipsis;
}
.mixer-channel-label-v.playing{color:var(--c-root)}
.mixer-channel-val-v{color:var(--tx3);font-size:max(10px,.56rem);font-variant-numeric:tabular-nums;min-height:1.2em}

@media(max-width:1023px){
  :root{font-size:18px}
  /* .metro-circle fait 108px fixe en mobile (règle de base non scopée par media query, cf. plus bas) —
     .metro-beatcount{font-size:4.2rem} (75.6px à 18px de racine) déborde largement ce cercle pour le texte
     "PLAY". Taille réduite pour tenir dans le cercle. */
  /* Scopé à l'état "PLAY" uniquement (:not(.beat1)...:not(.beat12), même correctif que la règle desktop
     équivalente ci-dessus) — écrasait aussi les chiffres de temps en mobile, pas seulement "PLAY". */
  #beatCount:not(.beat1):not(.beat2):not(.beat3):not(.beat4):not(.beat5):not(.beat6):not(.beat7):not(.beat8):not(.beat9):not(.beat10):not(.beat11):not(.beat12){font-size:1.6rem!important}
  /* Chiffres de temps (1/2/3/4...) en mobile — bug signalé : "coupé par le cercle intérieur du métronome".
     Cause distincte du correctif "PLAY" ci-dessus (volontairement non couvert à l'époque, cf. son
     commentaire) : la règle de base .metro-beatcount.beatN{font-size:8rem} (règle hors media query, cf. plus
     bas dans ce fichier) vaut 144px à 18px de racine mobile, dans un disque .metro-circle fixé à 108px —
     débordement mécanique quel que soit le nombre de temps. !important nécessaire : même spécificité que la
     règle de base, gagnée sinon par l'ordre d'apparition dans le fichier (même mécanisme déjà documenté sur
     la règle desktop équivalente plus haut). ~42px choisi pour tenir confortablement dans les 108px du
     disque avec une marge, cohérent avec le rapport déjà retenu pour "PLAY" ci-dessus (28.8px/108px) tout en
     restant plus grand qu'un mot de 4 lettres puisqu'un seul chiffre est plus étroit. */
  #page-metro .metro-beatcount.beat1,#page-metro .metro-beatcount.beat2,#page-metro .metro-beatcount.beat3,#page-metro .metro-beatcount.beat4,#page-metro .metro-beatcount.beat5,#page-metro .metro-beatcount.beat6,#page-metro .metro-beatcount.beat7,#page-metro .metro-beatcount.beat8,#page-metro .metro-beatcount.beat9,#page-metro .metro-beatcount.beat10,#page-metro .metro-beatcount.beat11,#page-metro .metro-beatcount.beat12{font-size:2.35rem!important}
  /* Indice visuel de scroll horizontal sur la subbar Triades (contrôles nombreux, overflow-x:auto sans indice sinon) */
  #triadsHeader::after{
    content:'';position:absolute;top:0;left:0;height:44px;right:0;width:22px;margin-left:auto;
    background:linear-gradient(to right,transparent,var(--sf));pointer-events:none;
  }
  .sidebar-desktop{display:none}
  .mixer-desktop{display:none}
  .main-content{margin-left:0;padding-top:0;width:100%}
  .pages-container{max-width:100%;margin:0;padding:0}
  /* #rhAudioSourceSeg (sélecteur source audio Strum : Voix/Synthétique/Échantillon/Échantillon muet, cf.
     index.html) déborde du viewport en mobile depuis l'ajout d'un 4e bouton — bug rapporté par capture.
     .pg est partagé par d'autres groupes de pastilles ailleurs dans l'app (flex-shrink:0, pensé pour une
     seule ligne compacte) : override scopé ICI plutôt que sur .pg lui-même, pour ne pas changer le
     comportement des autres groupes qui n'ont pas ce problème. Passe sur 2 lignes de 2 boutons (demande
     explicite) plutôt qu'un scroll horizontal — même pattern déjà utilisé pour .str-track-row-l2 (trop de
     contrôles sur une ligne étroite), height:auto nécessaire car .pg fixe height:26px pour le cas 1 ligne. */
  /* !important nécessaire : #rhAudioSourceSeg porte un style inline flex-shrink:0 (cf. index.html), plus
     spécifique que toute règle de feuille de style — sans !important, cette règle mobile serait purement et
     simplement ignorée (constaté : le débordement persistait malgré cette règle avant l'ajout des !important). */
  #rhAudioSourceSeg{flex-wrap:wrap!important;height:auto!important;flex-shrink:1!important}
  #rhAudioSourceSeg button.pill{flex:1 1 calc(50% - 2px)}
  /* .spark-result-row déborde en mobile — cf. les règles mobiles dédiées juste après sa déclaration de base
     (grep ".spark-result-row{flex-wrap:wrap" plus bas dans ce fichier) : regroupées là-bas, pas ici, pour
     rester APRÈS la règle de base #page-spark .spark-result-chips-mini{flex:1...} dans l'ordre du fichier —
     à spécificité égale (#page-spark .classname), la règle la PLUS TARDIVE dans le fichier l'emporte quel
     que soit l'ordre d'écriture d'une media query ; les poser ici (avant la base, plus haut dans le fichier)
     les aurait rendues silencieusement inopérantes malgré le media query correspondant bien à l'écran
     (bug constaté : flex-basis:100% ici n'avait aucun effet, la base flex:1 gagnait toujours). */
  /* BUG CORRIGÉ / évolution (demande explicite : "remettre le bouton recherche (T) dans la topbar pour un
     accès rapide") — #cmdPaletteTopbarBtn était masqué ici depuis un correctif antérieur ("pour faire de la
     place dans la topbar", la loupe vivait alors uniquement dans le tiroir via #cmdPaletteDrawerBtn) ;
     restauré en topbar sur demande explicite ultérieure, la copie du tiroir reste en place en plus (aucune
     des deux ne porte d'état propre, cmdPaletteOpen() est la même fonction partagée). */
  /* Switch de synchro inter-modules : DÉPLACÉ dans Paramètres > Général (demande explicite, cf.
     #setGlobalSync dans index.html) — #globalSyncBtn (topbar) reste desktop-only (>=600px, cf.
     _syncBtnFitTopbar dans app.js, simplifiée en un simple seuil de largeur depuis ce déplacement), plus de
     copie mobile dans le tiroir (#globalSyncDrawerBtn, retirée) à mesurer/basculer. */
  .tg-row-desktop{display:none!important}
  .tg-row-mobile{display:flex!important}
  #page-rhythm.page.active{
    position:fixed;left:0;right:0;bottom:0;top:var(--rh-top,0px);
    z-index:5;overflow:hidden;
  }
  /* Structures — pistes du multipiste en 2 lignes sur mobile (demande explicite : "donner 2 lignes par
     piste pour la gestion du multipiste") au lieu de tasser 9 colonnes fixes (cf. STR_TRACK_ROW_COLS/
     strRenderTrackRows dans app.js) dans une largeur insuffisante. .str-track-row passe de grid (1 ligne,
     colonnes fixes desktop) à flex column ; ses 2 groupes internes (.str-track-row-l1 = titre/A/S/P/volume,
     .str-track-row-l2 = densité/instrument/octave/pattern) passent de display:contents (invisibles en tant
     que conteneurs, enfants directs du grid desktop) à display:flex pour redevenir de vraies lignes
     empilées. grid-template-columns neutralisé (sinon persiste sans effet visible mais fausserait tout
     recalcul de layout). */
  /* Séparation fine mais lisible entre pistes empilées sur 2 lignes (demande explicite) — en plus du
     border-bottom existant (1px, var(--bd), déjà sur .str-track-row en desktop comme en mobile) : sur 2
     lignes, la limite entre pistes est moins évidente au premier coup d'œil qu'en 1 ligne dense, d'où un
     renfort visuel dédié au mobile (marge + bordure légèrement plus contrastée) plutôt que de changer le
     border-bottom partagé avec le desktop. */
  /* align-items:stretch (pas center, hérité de la règle desktop align-items:center posée en JS sur .str-
     track-row — cf. strRenderTrackRows) : avec align-items:center, .str-track-row-l1/-l2 se comportent
     comme des flex-items qui RÉTRÉCISSENT à leur contenu puis se centrent horizontalement, chacun à une
     largeur DIFFÉRENTE selon son contenu (nom de piste plus ou moins long) — cause exacte du désalignement
     signalé par capture (le groupe A/S/P/volume flottait à des positions différentes d'une piste à
     l'autre, jamais collé au même bord droit). stretch force l1/l2 à occuper toute la largeur de la ligne,
     rendant le margin-left:auto sur le bouton A enfin fiable (bord droit identique sur toutes les pistes). */
  .str-track-row{display:flex!important;flex-direction:column;align-items:stretch!important;grid-template-columns:none!important;gap:6px!important;padding-bottom:10px!important;margin-bottom:2px;border-bottom:1px solid var(--bd2)!important}
  .str-track-row-l1,.str-track-row-l2{display:flex!important;align-items:center;gap:6px}
  /* Ligne 1 (titre/A/S/P/volume) — demande explicite : nom de piste à gauche, A/S/P + slider volume à
     droite, alignement IDENTIQUE sur toutes les pistes. Premier essai (flex-wrap:wrap + min-width sur
     chaque élément) cassait la ligne différemment selon la longueur du nom de piste ("Rythmique 2" plus
     long que "Basse"/"Lead" faisait passer le groupe droit à la ligne suivante sur certaines pistes
     seulement — signalé par capture, alignements incohérents d'une piste à l'autre). Fix : flex-wrap
     retiré sur cette ligne (jamais de retour à la ligne, largeur totale fixe et connue : 3 boutons 22px +
     slider), titre avec overflow:hidden/text-overflow:ellipsis pour absorber toute variation de longueur
     de nom SANS pousser le groupe droit — ellipsis déjà en place sur le span interne du titre (cf.
     strRenderTrackRows), il suffit que le span.first-child lui-même se limite plutôt que de s'étirer selon
     son contenu. margin-left:auto sur le bouton A (1er du groupe droit) plaque A/S/P/volume au bord droit. */
  .str-track-row-l1{flex-wrap:nowrap}
  .str-track-row-l1>span:first-child{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .str-track-row-l1>button:nth-child(2){margin-left:auto;flex-shrink:0}
  .str-track-row-l1>button{flex-shrink:0}
  .str-track-row-l1>input[type="range"]{flex:0 0 110px;width:110px}
  /* Ligne 2 (densité/instrument/octave/pattern) : le groupe densité (rateWrap, width:100% en JS pour tenir
     dans sa colonne de grid fixe en desktop) est contraint à sa largeur naturelle ici (assez large pour que
     ½/×1/×2 restent lisibles — un 1er essai plus étroit rognait leur texte), pour tenir sur la même sous-
     ligne que le sélecteur d'instrument plutôt que de s'étirer seul sur toute la largeur ; le select de
     pattern (dernier enfant, minmax(110px,1fr) en desktop) prend le reste de la largeur. Cette ligne 2 GARDE
     flex-wrap:wrap (contrairement à la ligne 1) — 4 contrôles de largeur variable, un passage à la ligne
     occasionnel y reste acceptable/attendu (pas de risque d'incohérence puisqu'il n'y a rien à "pousser
     à droite" ici, contrairement à la ligne 1). */
  .str-track-row-l2{flex-wrap:wrap}
  .str-track-row-l2>div:first-child{flex:0 0 auto;width:auto!important;min-width:110px}
  .str-track-row-l2>select:not(:last-child){flex:1 1 120px;min-width:90px}
  .str-track-row-l2>select:last-child{flex:1 1 140px;min-width:110px}

}
/* Tablette en portrait (700-1023px, audit 2026-10-02) : la 2e ligne de chaque piste se repliait sur 3 lignes
   (4 lignes par piste au total) alors que la largeur suffit pour une seule — densité, instrument, octave,
   égrenage et motif restent sur la même ligne, le motif prenant la place restante. */
@media (min-width:700px) and (max-width:1023px){
  .str-track-row-l2{flex-wrap:nowrap!important}
  .str-track-row-l2>*{flex:0 1 auto!important;min-width:0!important;width:auto!important}
  .str-track-row-l2>div:first-child{flex:0 0 auto!important;min-width:110px!important}
  .str-track-row-l2>select:not(:last-child){flex:0 1 150px!important}
  .str-track-row-l2>:last-child{flex:1 1 160px!important}
}

:root{--r:8px;--rl:14px;--tr:.16s ease;--c-root:#ff6b00;color-scheme:dark light}
[data-accent="default"]{--c-root:#ff6b00 !important}
[data-accent="cyan"]{--c-root:#00d9ff !important}
[data-accent="magenta"]{--c-root:#ff006e !important}
[data-accent="lime"]{--c-root:#39ff14 !important}
[data-accent="gold"]{--c-root:#ffd700 !important}
[data-accent="pink"]{--c-root:#ff1493 !important}
[data-accent="red"]{--c-root:#ff3333 !important}
[data-accent="green"]{--c-root:#00ff66 !important}
[data-accent="purple"]{--c-root:#cc33ff !important}
[data-accent="orange"]{--c-root:#ff9933 !important}
[data-accent="teal"]{--c-root:#00ffcc !important}
[data-accent="violet"]{--c-root:#ee82ee !important}
[data-accent="white"]{--c-root:#ffffff !important}
/* input[type=color] rond façon pastille de thème : Chrome/Edge dessinent le swatch via des pseudo-éléments
   internes (padding/bordure propres) que les styles inline ne peuvent pas atteindre. */
#setAccentCustom::-webkit-color-swatch-wrapper{padding:0;border-radius:50%}
#setAccentCustom::-webkit-color-swatch{border:none;border-radius:50%}
#setAccentCustom::-moz-color-swatch{border:none;border-radius:50%}
[data-theme="dark"]{--bg:#121218;--sf:#1c1c26;--s2:#242432;--s3:#2c2c3c;--bd:#383850;--bd2:#4a4a60;--tx:#f4f4fc;--tx2:#b0b0cc;--tx3:#8a8aae;--sh:rgba(0,0,0,.55);--pbg:#242432;--phov:#2c2c3c;--ov:rgba(6,6,12,.9);}
/* Palette sombre "Deep" (proche OLED) — alternative à la palette sombre par défaut. */
[data-theme="dark"][data-palette="deep"]{--bg:#000000;--sf:#0a0a0e;--s2:#121218;--s3:#1a1a22;--bd:#2a2a38;--bd2:#3a3a4c;--tx:#f0f0f8;--tx2:#a8a8c8;--tx3:#7d7da6;--sh:rgba(0,0,0,.7);--pbg:#121218;--phov:#1a1a22;--ov:rgba(0,0,0,.94);}
/* Palette sombre "Monochrome" — mêmes NIVEAUX DE LUMINANCE que le thème sombre par défaut ([data-theme=
   "dark"] ci-dessus) mais désaturée (R=G=B sur chaque variable, teinte bleue du défaut retirée) : demande
   explicite ("garde les nuances du thème sombre par défaut, mais sans la teinte bleue... comme par défaut,
   mais désaturé"), distincte de "Deep" (quasi-noir/OLED, jugé trop foncé pour cet usage). Pas plus foncée ni
   plus claire que le défaut, seulement neutralisée chromatiquement. */
[data-theme="dark"][data-palette="mono"]{--bg:#141414;--sf:#202020;--s2:#282828;--s3:#303030;--bd:#404040;--bd2:#525252;--tx:#f6f6f6;--tx2:#b8b8b8;--tx3:#929292;--sh:rgba(0,0,0,.55);--pbg:#282828;--phov:#303030;--ov:rgba(8,8,8,.9);}
/* Palette sombre "Monochrome froid" — même principe que "Monochrome" ci-dessus (désaturée, pas de teinte
   bleue franche comme le défaut) mais avec un très léger virage froid (B très légèrement > R, quelques
   valeurs de moins par canal, à peine perceptible comme couleur mais lisible comme "froid" plutôt que
   "chaud") ET légèrement plus sombre que Monochrome/le défaut — sans aller jusqu'au quasi-noir de "Deep"
   (--bg #000000). Demande explicite : "une nouvelle palette, monochrome froid, légèrement plus froide et
   sombre, mais pas aussi sombre que la profonde". */
[data-theme="dark"][data-palette="monoCold"]{--bg:#0e1012;--sf:#181b1e;--s2:#1f2226;--s3:#272b30;--bd:#383d44;--bd2:#4a5058;--tx:#f0f2f4;--tx2:#a8afb8;--tx3:#828a94;--sh:rgba(0,0,0,.6);--pbg:#1f2226;--phov:#272b30;--ov:rgba(4,5,6,.92);}
/* Palette sombre "mono2" — inspirée de dev/colors.png (capture de panneaux de voies Ableton Live) : gris
   neutre-froid façon logiciel audio pro, avec un contraste marqué entre le fond (quasi-noir, très légèrement
   bleu-gris) et les surfaces de panneau (nettement plus claires, comme les "channel strips" de la capture)
   plutôt que la progression douce des autres palettes mono — vise le look "console de studio" plutôt qu'un
   simple gris désaturé. Nom temporaire "mono2" en attendant un nom définitif si l'utilisateur la valide. */
[data-theme="dark"][data-palette="mono2"]{--bg:#131416;--sf:#232529;--s2:#2b2e33;--s3:#34373d;--bd:#454951;--bd2:#585c66;--tx:#e4e6ea;--tx2:#a4a8b0;--tx3:#94969b;--sh:rgba(0,0,0,.6);--pbg:#2b2e33;--phov:#34373d;--ov:rgba(6,7,9,.92);}
/* Palette sombre "Midnight" (bleu marine profond) — 3e option sombre, distincte de Deep (quasi noir/neutre)
   par une teinte bleue nette plutôt qu'une simple variation de luminosité. --tx3 vérifié ≈4.8:1 sur --sf. */
[data-theme="dark"][data-palette="midnight"]{--bg:#0a0e1a;--sf:#121a2c;--s2:#182238;--s3:#1e2a44;--bd:#2c3a58;--bd2:#3c4d70;--tx:#eef2fc;--tx2:#a8b4d0;--tx3:#7e8eb0;--sh:rgba(0,0,10,.65);--pbg:#182238;--phov:#1e2a44;--ov:rgba(4,6,16,.94);}
/* Palette sombre "Forêt" (vert profond) — 4e option sombre, teinte verte distincte de Midnight (bleue) et
   Deep (neutre). --tx3 vérifié ≈4.6:1 sur --sf. */
[data-theme="dark"][data-palette="forest"]{--bg:#0a140e;--sf:#101e16;--s2:#16281c;--s3:#1c3224;--bd:#2c4a36;--bd2:#3c6048;--tx:#eefcf2;--tx2:#a8ccb0;--tx3:#78a084;--sh:rgba(0,10,4,.65);--pbg:#16281c;--phov:#1c3224;--ov:rgba(4,12,8,.94);}
/* Palette sombre "Vin" (bordeaux profond) — 5e option sombre, seule teinte rouge/pourpre du groupe. --tx3
   vérifié ≈4.5:1 sur --sf. */
[data-theme="dark"][data-palette="wine"]{--bg:#160a0e;--sf:#221217;--s2:#2c181e;--s3:#382026;--bd:#54303a;--bd2:#6c3e4a;--tx:#fcecf0;--tx2:#ccaab4;--tx3:#a57e8c;--sh:rgba(10,0,4,.65);--pbg:#2c181e;--phov:#382026;--ov:rgba(12,4,8,.94);}
/* Palette sombre "Violette" (pourpre profond) — 6e option sombre, distincte de Vin (rouge) et Midnight
   (bleu) par une teinte violette nette. --tx3 vérifié ≈4.5:1 sur --sf. */
[data-theme="dark"][data-palette="violet"]{--bg:#120a1a;--sf:#1c1228;--s2:#241834;--s3:#302044;--bd:#4a3266;--bd2:#5e4280;--tx:#f4ecfc;--tx2:#c0accc;--tx3:#9484a8;--sh:rgba(8,0,16,.65);--pbg:#241834;--phov:#302044;--ov:rgba(8,4,16,.94);}

/* Sliders de volume par piste (Structures multipiste) — agrandis ×1.5 (demande explicite) via les pseudo-
   éléments natifs plutôt que transform:scale, qui grandit le RENDU sans agrandir la BOÎTE de layout et
   déborde dans les contrôles voisins d'une grid dense (constaté par capture sur le bouton "½" adjacent).
   Tailles de référence : piste/thumb natifs Chrome/Firefox ≈4px/16px — visées ici ≈8px/28px (×1.5-1.75,
   un 1er essai plus proche de ×1.4 était passé inaperçu — retour explicite "n'ont pas changé de taille").
   overflow:visible sur le conteneur nécessaire : le thumb (28px) dépasse la boîte de l'input (24px) via
   margin-top négatif, comme tout slider natif — un overflow:hidden hérité l'aurait rogné. */
.str-vol-slider{-webkit-appearance:none;appearance:none;height:24px;background:transparent;cursor:pointer;overflow:visible}
.str-vol-slider::-webkit-slider-runnable-track{height:8px;border-radius:4px;background:var(--bd)}
.str-vol-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:28px;height:28px;border-radius:50%;background:var(--c-root);margin-top:-10px;border:3px solid var(--sf);box-shadow:0 1px 3px var(--sh)}
.str-vol-slider::-moz-range-track{height:8px;border-radius:4px;background:var(--bd)}
.str-vol-slider::-moz-range-progress{height:8px;border-radius:4px;background:var(--c-root)}
.str-vol-slider::-moz-range-thumb{width:24px;height:24px;border-radius:50%;background:var(--c-root);border:3px solid var(--sf);box-shadow:0 1px 3px var(--sh)}


*{box-sizing:border-box;margin:0;padding:0}
/* Désactive la sélection de texte (surbrillance bleue au clic-glisse) sur toute l'app — interface
   d'instrument/outil, pas un document à copier. Exception explicite pour les champs éditables
   (input/textarea/contenteditable) : y couper/copier/coller doit rester possible. */
*{user-select:none;-webkit-user-select:none}
input,textarea,[contenteditable="true"]{user-select:text;-webkit-user-select:text}
html{font-size:18px;-webkit-tap-highlight-color:transparent}
html,body{overscroll-behavior:none} /* pas d'effet élastique en butée de scroll (WebView Android : la page et la topbar se déformaient) */
body{background:var(--bg);color:var(--tx);font-family:var(--fb);min-height:100vh;display:flex;flex-direction:column;forced-color-adjust:none}
/* Masque toute l'app tant que l'auth n'est pas résolue (cf. auth.js, ftBootAfterAuth) — cible tous les
   enfants directs de body SAUF #authScreen, plutôt que d'envelopper l'app existante dans un nouveau
   conteneur (aurait risqué de casser des règles CSS qui supposent .topbar/.pages-container enfants
   directs de body). Retirée (classe enlevée de body) une fois la session résolue, dev ou réelle. */
body.ft-auth-pending>:not(#authScreen){display:none!important}
/* Masque le HTML statique jusqu'à la fin du boot d'app.js (retirée sur window 'load', cf. app.js) — opacity
   et non display/visibility : la mise en page reste calculée (mesures du boot justes) et aucun descendant
   ne peut réapparaître par un visibility:visible propre. Filet : retirée aussi après 6 s (index.html). */
body.ft-booting>:not(#authScreen){opacity:0!important}

/* TOPBAR */
.topbar{display:flex;flex-direction:column;border-bottom:1px solid var(--bd);background:var(--sf);position:sticky;top:0;z-index:200;flex-shrink:0}
/* Texture d'interface (topbar/sidebar/ligne dédiée du sélecteur de tonalité/subbars) — demande explicite :
   "la barre derrière le sélecteur de tonalité"/"les subbar doivent aussi être affectées", puis "une
   continuité de l'image entre les différents éléments". VALIDÉ PAR TEST RÉEL (Playwright, cf. capture) :
   un calque UNIQUE ::before sur <body>.ui-bg-enabled, position:fixed;inset:0, SANS transform:scale — les
   tentatives précédentes qui gardaient transform:scale(1.06) (utile sur un calque local en inset:-5% pour
   masquer le bord flouté d'une boîte, cf. .module-bg-enabled::before plus haut) cassaient le rendu une fois
   appliquées à un calque plein-viewport ; sans transform, aucun souci. Chaque barre devient une simple
   fenêtre transparente sur ce calque commun : plus de recadrage indépendant par boîte (chacune réétirait
   l'image entière sur sa propre largeur avec background-size:cover, d'où la "duplication" visible entre
   topbar/subbar de largeurs différentes). */
/* #uiBgRotateLayer : <div> réel (ajouté dans index.html, juste avant TOPBAR), seul à porter transform:rotate
   — un ::before de <body> (cf. calque partagé ci-dessus) ne peut pas avoir lui-même de pseudo-élément
   enfant, d'où ce vrai <div> intermédiaire pour porter la rotation par pas de 90° sans la poser sur le calque
   partagé lui-même (qui doit rester sans transform, cf. commentaire ci-dessus sur le calque partagé). Vérifié
   pixel par pixel (lecture directe du buffer PNG, capture d'écran trompeuse à l'œil nu sur une photo faible
   opacité/désaturée/floutée) : s'affiche correctement, aucun bug d'empilement malgré une fausse piste
   initiale (z-index négatif sur un enfant direct de body suspecté à tort). */
#uiBgRotateLayer{display:none;position:fixed;inset:0;z-index:-1;pointer-events:none;overflow:hidden}
.ui-bg-enabled #uiBgRotateLayer{display:block}
#uiBgRotateLayer::before{content:"";position:absolute;inset:0;
  background-image:linear-gradient(rgba(0,0,0,var(--ui-bg-scrim)),rgba(0,0,0,var(--ui-bg-scrim))),var(--ui-bg-image);
  background-size:cover;background-position:var(--ui-bg-pos,center);
  filter:blur(var(--ui-bg-blur)) saturate(calc(var(--ui-bg-saturation) * 2));opacity:var(--ui-bg-opacity);
  transform:scale(var(--ui-bg-zoom,1)) rotate(var(--ui-bg-rotate,0deg));
}
.ui-bg-enabled .topbar,.ui-bg-enabled .sidebar-desktop,.ui-bg-enabled .subbar,body.ui-bg-enabled.root-sel-dedicated .desktop-root-row{isolation:isolate;background:transparent}
.ui-bg-enabled .topbar>.tb1,.ui-bg-enabled .topbar>.tb2,.ui-bg-enabled .topbar>.tb3,.ui-bg-enabled .sidebar-desktop>*{position:relative;z-index:1;background:rgba(0,0,0,.08)}
.ui-bg-enabled .desktop-root-row{position:relative}
.ui-bg-enabled .desktop-root-row>*,.ui-bg-enabled .subbar>*{position:relative;z-index:1}
.tb1{display:flex;align-items:center;gap:8px;height:44px;padding:0 14px;border-bottom:1px solid var(--bd)}
/* Sur très petit écran, la rangée logo+métronome+groovebox+BPM+drone+sélecteur G/C+plein écran ne tient
   pas dans 390px de large avec les espacements/largeurs desktop — le bouton plein écran (le seul enfant
   sans flex-shrink:0, cf. .tbr plus bas) se faisait pousser hors du viewport et devenait invisible/
   inaccessible sur les modules où le sélecteur G/C est affiché (bug constaté en audit). Resserre gap et
   largeur du champ BPM plutôt que de masquer un élément — l'app reste utilisable en plein écran depuis
   le tiroir de navigation de toute façon, mais le garder ici aussi n'imposait pas de vrai compromis une
   fois la place regagnée. */
@media(max-width:480px){
  .tb1{gap:2px;padding:0 2px}
  #globalBpmInput{width:40px!important;padding:0!important}
  /* ⌕/?/⛶ doivent garder la même taille (28px, cf. .ibtn) que les autres boutons de la topbar (▶⠿〜) —
     règle explicite : ne jamais rétrécir des boutons topbar pour gagner de la place. Le sélecteur G/C
     (2×28px, seul élément aussi large que le logo/BPM après le déplacement de la loupe à côté du logo)
     reste le seul candidat de rétrécissement ; gaps/padding/BPM resserrés au maximum ci-dessus pour
     compenser le retour de ⌕/?/⛶ à leur taille pleine. */
  #gcGuitar,#gcPiano{width:24px!important;height:24px!important;padding:0!important}
  .tbr{gap:1px!important}
  /* Cellules Strum inclicables sur mobile étroit (signalé explicitement, PLUSIEURS fois — "toujours
     complètement inutilisable" même après un premier passage de correctifs) — rhRenderGrid réduit déjà le
     nombre de colonnes par ligne sous 480px (cf. app.js _rhNarrowMobile) pour élargir chaque cellule, mais
     l'aspect-ratio:2/1 hérité de la règle de base laissait la hauteur suivre passivement, retombant parfois
     sous le minimum tactile utilisable (~44px) selon la résolution affichée. Plancher explicite ici.
     Bug trouvé (cause racine du signalement répété) : aspect-ratio:unset SANS !important perdait contre la
     règle de base #page-rhythm .rh-cell{aspect-ratio:2/1} déclarée PLUS LOIN dans ce fichier (styles.css
     ~2360) — à spécificité CSS égale, c'est l'ordre dans le FICHIER qui tranche, pas la présence d'un media
     query correspondant à l'écran (même piège déjà rencontré et corrigé cette session pour Spark). Résultat
     mesuré : aspect-ratio restait "2/1" sur mobile malgré cette règle, forçant une largeur de cellule
     MINIMALE de 88px (44×2) quel que soit le nombre de colonnes demandé par gridCols — 6 colonnes × 88px
     dépassait largement les ~340px réellement disponibles, débordant hors du conteneur grid (qui ne rogne
     pas ses propres pistes en trop par défaut). !important ajouté pour gagner la cascade sans réorganiser
     tout le fichier — même technique déjà en place sur la règle desktop jumelle juste au-dessus (~ligne 267,
     aspect-ratio:unset!important). */
  #page-rhythm .rh-cell{aspect-ratio:unset!important;min-height:44px}
  /* #rhClearAllBtn ("vider toute la grille", ✕✕) retiré sur mobile (demande explicite : "gagner une ligne")
     — remplace l'ancien fix flex-wrap (le bouton passait sur sa propre ligne faute de place à côté de
     #rhColClearRow, qui occupe déjà toute la largeur de la grille sur mobile étroit) : plus la peine de lui
     trouver une ligne puisqu'il n'est plus affiché du tout ici. Reste inchangé sur desktop (assez de place à
     côté de la grille, cf. règle de base #page-rhythm .rh-col-clear-header{display:flex}). Vider une
     colonne individuelle reste possible via #rhColClearRow (✕ par colonne), ou en repassant en Simple/en
     rechargeant un pattern — pas de perte de fonctionnalité irréversible, juste un raccourci en moins. */
  #page-rhythm #rhClearAllBtn{display:none}
  /* Scroll global du module sur mobile (signalé : "le module strum est toujours complètement inutilisable
     (mise en page)... la matrice empêche de lire les patterns dans la bibliothèque"). Cause : la règle de
     base (styles.css ~2239) découpe .rh-wrap en deux zones flex — .rh-sticky-player (titre+switchs+grille,
     flex-shrink:0, jamais rétréci) au-dessus d'une .rh-scroll-body (bibliothèque de patterns) qui hérite
     seule de tout l'espace restant et scrolle. Pensé pour desktop, où la grille tient dans une hauteur
     raisonnable ; sur mobile, la grille (switchs empilés + N lignes de cellules) mesure couramment 400px+
     dans un viewport de ~600-700px de haut, ne laissant que ~100px à .rh-scroll-body — la bibliothèque
     devient quasi illisible/inutilisable alors que .rh-sticky-player, non scrollable par construction
     (overflow:hidden hérité de .rh-wrap), ne peut pas non plus être réduite. Fix : sur mobile, .rh-wrap
     devient LUI-MÊME le conteneur de scroll (une seule colonne, grille + bibliothèque scrollent ensemble)
     plutôt que deux zones à comportement différent — .rh-sticky-player perd son flex-shrink:0 (n'a plus de
     raison d'être épinglé), .rh-scroll-body perd son propre overflow-y:auto (un seul scroll, pas deux
     imbriqués, qui aurait capturé le geste de scroll de façon imprévisible selon la position du doigt). */
  /* !important nécessaire : la règle de base #page-rhythm .rh-wrap{overflow:hidden} (styles.css ~2239) est
     déclarée PLUS LOIN dans ce fichier — même piège de cascade déjà rencontré et corrigé plusieurs fois
     cette session (Spark, .rh-cell aspect-ratio ci-dessus) : à spécificité égale, l'ordre dans le fichier
     l'emporte sur la correspondance media query. Vérifié : sans !important, overflow-y restait "hidden"
     sur mobile malgré cette règle. */
  #page-rhythm .rh-wrap{overflow-y:auto!important;overflow-x:hidden!important}
  #page-rhythm .rh-sticky-player{flex-shrink:1!important}
  #page-rhythm .rh-scroll-body{overflow-y:visible!important;flex:0 1 auto!important}
}
/* En dessous de 390px (iPhone SE/8 = 375px), la marge ci-dessus ne suffit plus : le sélecteur G/C
   (dernier candidat de rétrécissement compatible avec la contrainte tactile, cf. commentaire ci-dessus)
   descend encore un peu et perd son gap interne — ⌕/?/⛶ restent à 28px, jamais rétrécis. */
@media(max-width:375px){
  #gcGuitar,#gcPiano{width:16px!important;height:16px!important;font-size:max(10px,.55rem)!important}
  #gcSelector{gap:0!important}
}
/* Caché par défaut (mobile) — .tb3 garde son rôle de ligne dédiée sous ce seuil (cf. son commentaire dans
   le media query desktop juste plus bas). Affiché en display:flex UNIQUEMENT au-delà de 1150px (règle
   desktop juste après ce bloc de base) — même principe que .topbar-status, un mécanisme de bascule déjà
   établi plutôt qu'un nouveau. DOIT rester ICI, avant la règle desktop qui la surcharge : la cascade CSS
   départage par ordre d'apparition à spécificité égale — la poser après aurait à son tour écrasé le
   display:flex desktop, cachant le sélecteur MÊME au-delà de 1150px (même bug de cascade que .tb3 lui-même
   juste en dessous, cf. son propre commentaire). */
.topbar-root-row{display:none}
/* BUG CORRIGÉ (rapporté avec capture : "sur mobile, j'ai un double sélecteur de tonalité") — cette règle
   de base (mobile) manquait entièrement : .desktop-root-row n'avait de display:none QUE dans le media
   query desktop (cf. son commentaire un peu plus haut, "toujours masqué en dessous de 1150px" y était
   affirmé à tort, sans règle réelle pour l'appliquer) — un <div> est display:block par défaut, donc
   #desktopRootRow (peuplé par le même buildRootRows() que #globalRootRow, cf. app.js) s'affichait EN PLUS
   de .tb3 sur mobile, où il n'a jamais eu de rôle. Même principe que .topbar-root-row juste au-dessus :
   masqué ICI par défaut, la règle desktop (body.root-sel-dedicated .desktop-root-row) reste inchangée et
   continue de le réafficher normalement au-delà de 1150px quand ce réglage est choisi. */
.desktop-root-row{display:none}
.tb2{display:flex;align-items:center;height:36px;padding:0 14px}
.tb3{display:flex;align-items:center;gap:4px;height:32px;padding:0 6px;border-top:1px solid var(--bd);background:var(--sf);overflow-x:auto;overflow-y:hidden;flex-shrink:0;scrollbar-width:none}
.tb3::-webkit-scrollbar{display:none}
/* BUG CORRIGÉ (rapporté avec capture : "le sélecteur n'a pas bougé") — ce bloc desktop DOIT venir APRÈS
   la règle de base .tb3{display:flex} ci-dessus : à spécificité égale (même sélecteur simple .tb3), la
   cascade CSS départage par ordre d'APPARITION dans le fichier, jamais par la présence d'un media query —
   un 1er jet plaçait .tb3{display:none} dans le tout premier bloc @media(min-width:1024px) du fichier
   (tout en haut), avant cette règle de base, qui la réécrasait donc silencieusement à display:flex malgré
   le media query actif. Déplacé ici, physiquement après, pour que ce display:none gagne réellement. */
@media(min-width:1024px){
  /* .tb3 (sélecteur de tonalité + mini-accordeur) masqué en desktop — demande explicite : "déplacer le
     sélecteur de tonalité global dans la barre supérieure pour libérer de la hauteur" — remplacé par
     #topbarRootRowWrap directement dans .tb1, cf. .topbar-root-row plus bas. Reste affiché tel quel en
     mobile (hors de ce media query), où il garde son rôle historique de ligne dédiée sous les onglets. */
  .tb3{display:none}
  /* min-width:0 (pas overflow-x ici : .rrow interne à #topbarRootRow a déjà son propre overflow-x:auto +
     scrollbar masquée, cf. .rrow/.rrow::-webkit-scrollbar plus bas dans ce fichier) — filet de sécurité si
     la fenêtre desktop est juste au-dessus du seuil 1150px et que l'espace restant entre #topbarStatus et
     .tbr devient trop étroit pour les 12 pastilles + le bouton accordeur : sans min-width:0, ce wrapper
     (flex item comme les autres enfants de .tb1) ne rétrécirait jamais sous sa largeur intrinsèque,
     empêchant .rrow de scroller et débordant plutôt toute la topbar. */
  .topbar-root-row{display:flex;align-items:center;gap:4px;flex-shrink:1;min-width:0;margin:0 auto}
  /* Bascule inverse de .desktop-root-row/#desktopRootRowWrap (cf. son propre commentaire, plus haut dans
     ce fichier) — les 2 présentations ne doivent jamais coexister. DOIT rester après la règle display:flex
     ci-dessus (même sélecteur simple .topbar-root-row, cascade CSS départagée par ordre d'apparition à
     spécificité égale, cf. le bug déjà documenté pour .tb3 un peu plus haut dans ce fichier). */
  body.root-sel-dedicated .topbar-root-row{display:none}
  /* #topbarStatus (flex:1 1 auto par défaut, cf. règle de base plus bas) grandirait pour occuper TOUT
     l'espace à sa gauche avant que .topbar-root-row n'ait la moindre chance d'utiliser margin:0 auto pour
     se centrer symétriquement — le sélecteur se retrouverait alors collé juste après lui, pas "centré
     dans l'espace disponible" comme demandé. flex-grow:0 ici (flex-shrink:1 gardé, pour que l'ellipsis
     du texte long continue de fonctionner si jamais l'espace vient à manquer) laisse le VRAI espace
     restant être réparti symétriquement par le margin:auto du sélecteur. */
  #topbarStatus{flex-grow:0}
}
/* Repliée sur les modules sans fondamentale (Métronome, Dictée, cf. switchTab) — gagne les 32px+bordure
   de hauteur qu'occupait la rangée de sélection de tonalité, sans effet ici. Scopé au mobile : en desktop
   la sidebar laisse largement la place, pas besoin de récupérer cet espace. */
@media(max-width:1023px){
  .tb3.tb3-hidden{height:0;padding:0;border-top:none;overflow:hidden}
}
.logo{font-family:var(--fd);font-weight:800;font-size:.82rem;white-space:nowrap;color:var(--tx);letter-spacing:.04em;flex-shrink:0}
.logo em{color:var(--c-root);font-style:normal}
.ctx-badge{display:flex;align-items:center;gap:5px;padding:2px 9px;border-radius:999px;border:1px solid var(--bd);background:var(--s2);font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx2);cursor:pointer;transition:all var(--tr);flex-shrink:0}
.ctx-badge:hover{border-color:var(--c-root);color:var(--c-root)}
.ctx-root{font-weight:700;color:var(--tx)}
.tsep{width:1px;background:var(--bd);align-self:stretch;margin:6px 3px;flex-shrink:0}
.tabs{display:flex;overflow-x:auto;flex:1;border-left:1px solid var(--bd);padding-left:8px;scrollbar-width:none}
.tabs::-webkit-scrollbar{display:none}
.tabs::-webkit-scrollbar{height:3px}
.tabs::-webkit-scrollbar-thumb{background:var(--bd2);border-radius:2px}
.tab{padding:0 13px;height:100%;display:flex;align-items:center;gap:5px;border:none;background:transparent;color:var(--tx2);font-family:var(--fb);font-size:.8rem;font-weight:500;cursor:pointer;border-bottom:2px solid transparent;transition:all var(--tr);white-space:nowrap;flex-shrink:0;-webkit-tap-highlight-color:transparent;text-decoration:none;user-select:none;-webkit-user-select:none}
.tab:visited{color:var(--tx2)}
@media(hover:hover){.tab:hover{color:var(--tx);background:var(--s2)}}  /* :hover gardé (pas de survol collé au tap) */
.tab.nav-hover{color:var(--tx);background:var(--s2)}
.tab.active{color:var(--c-root);border-bottom-color:var(--c-root);font-weight:600}
/* Audit perf 2026-09-26 : `transition:all` + font-weight différent à l'état .active => le navigateur INTERPOLE
   la graisse (500->600/700) pendant --tr : une mise en page complète du document à CHAQUE image pendant 160 ms
   après chaque clic (mesuré : ~17 layouts, 73 ms par changement d'onglet ; 2 ms avec ce correctif). Les
   propriétés visuelles restent animées, la graisse et les dimensions changent d'un coup. */
.tab,.sidebar-desktop .tab,.rbtn,.rbtn-underline,.pill,.scale-pill,.cti,.crb,.caged-badge,.settings-seg button{transition-property:color,background-color,border-color,box-shadow,opacity,transform}
/* Audit perf 2026-09-28 : même les couleurs des 2 onglets concernés (ancien/nouveau actif) repeignaient la barre
   d'onglets à chaque image pendant 160 ms — ~35 peintures par changement d'onglet contre 2 sans. Onglets :
   changement d'état instantané. */
.tab,.sidebar-desktop .tab{transition:none}
/* Pulsation "ce module émet du son" — posée/retirée par _syncNavPulse (cf. app.js), sur le TEXTE de l'onglet
   (pas un halo autour du bouton, demande explicite) dans les deux barres (#mainTabs/#sidebarTabs partagent
   le même data-tab par onglet, cf. switchTab). animation-duration posée dynamiquement en JS = 60/tempo
   secondes pour les modules cadencés (métronome/structures/rythmique/mélodie), durée fixe pour le drone
   (pas de tempo pertinent) — cf. _syncNavPulse. */
/* Intensité seule (opacité) — pas de changement de taille (retour explicite : "seulement highlight
   d'intensité, pas de mouvement de changement de taille"), pour rester discret et ne jamais faire "sauter"
   le texte ou décaler ses voisins dans la barre. */
.tab.tab-pulsing{animation:tabPulse 0.6s ease-in-out infinite}
#mainTabs .tab.tab-pulsing,#sidebarTabs .tab.tab-pulsing{color:var(--c-root)}
@keyframes tabPulse{0%,100%{opacity:1}50%{opacity:.5}}
.tab-sep{width:1px;background:var(--bd);align-self:stretch;margin:8px 4px;flex-shrink:0}
/* flex-shrink:0 explicite : sans lui (valeur implicite 1), .tbr était le SEUL enfant de .tb1 non protégé
   contre le rétrécissement — sur mobile (≤480px), quand le sélecteur G/C est visible (modules Gammes,
   Accords...), la rangée ne tenait pas sur une largeur d'écran et .tbr (donc le bouton plein écran qu'il
   contient) se faisait pousser hors du viewport, devenant invisible/inaccessible (bug constaté en audit).
   Le vrai fix de largeur est côté @media(max-width:480px) ci-dessous (BPM/gaps réduits) ; ce flex-shrink:0
   garantit qu'à l'avenir un débordement résiduel pousse un AUTRE élément plutôt que de faire disparaître
   silencieusement le bouton plein écran sans qu'aucun indice visuel ne le signale. */
.tbr{margin-left:auto;display:flex;gap:4px;align-items:center;flex-shrink:0}
/* Résumé d'état desktop (gamme active / section Structures en cours) — occupe l'espace libre entre les
   boutons de transport et .tbr (qui a margin-left:auto et se pousse déjà tout à droite), tronqué avec
   ellipsis plutôt que de forcer un retour à la ligne ou de pousser .tbr hors du viewport. Masqué sous
   900px (pas de place réaliste à côté des onglets/BPM sur petit écran, cf. média-requête ci-dessous). */
.topbar-status{flex:1 1 auto;min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-family:var(--fm);font-size:.62rem;color:var(--tx3);padding:0 6px;user-select:none}
@media(max-width:900px){.topbar-status{display:none!important}}
/* Horloge + chrono de la topbar (desktop, si la place le permet — _tbClockFit dans app.js pose
   style.display:none quand la rangée déborde). */
.tb-clock,.tb-chrono{font-family:var(--fm);font-size:.72rem;color:var(--tx2);font-variant-numeric:tabular-nums;white-space:nowrap;user-select:none;flex-shrink:0}
.tb-clock{padding:0 4px}
.tb-chrono{height:28px;padding:0 8px;border-radius:7px;border:1px solid var(--bd);background:var(--pbg);cursor:pointer;line-height:1;transition:all var(--tr)}
.tb-chrono:hover{border-color:var(--bd2);color:var(--tx);background:var(--phov)}
.tb-chrono.on{border-color:#fff}
.tb-chrono.on:hover{color:var(--tx2)}
@media(max-width:900px){.tb-clock,.tb-chrono{display:none!important}}
.ibtn{width:28px;height:28px;border-radius:7px;border:1px solid var(--bd);background:var(--pbg);color:var(--tx2);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all var(--tr);font-size:.75rem;flex-shrink:0;user-select:none;line-height:1}
/* Couleur active passée de var(--c-root) à blanc (demande explicite : "bouton metronome dans topbar : blanc
   au lieu de couleur d'accent") — bordure gardée en var(--c-root) (non concernée par la demande, sert
   toujours de repère "actif" même si la couleur du glyphe change). */
#globalMetroBtn.on{background:transparent;color:#fff;border-color:var(--c-root)}
/* font-size:1.1rem ICI (mobile, hors media query desktop) : le glyphe ♩ rend visuellement petit dans son
   cadre .ibtn de 28px au .75rem de base des autres icônes topbar (bug signalé initialement, cf. la règle
   desktop équivalente #globalMetroBtn{font-size:22px!important} sous @media(min-width:1024px) plus haut
   dans ce fichier) — mais 2.1rem (l'ancienne valeur inline HTML, pensée uniquement pour être écrasée par
   ce !important desktop) débordait franchement le bouton sur mobile, où RIEN ne l'écrase (signalé : "sur
   mobile, l'icône métronome est trop grande" — même défaut de comportement mobile/desktop déjà rencontré
   sur les boutons du Cercle des quintes). Retiré l'inline style HTML correspondant : cette règle CSS de
   base est désormais la SEULE source pour la taille mobile.
*/
#globalMetroBtn{line-height:1;display:flex;align-items:center;justify-content:center;font-size:1.1rem}
@media(hover:hover){.ibtn:hover{border-color:var(--bd2);color:var(--tx);background:var(--phov)}}
.ibtn.on{background:var(--tx);color:var(--bg);border-color:var(--tx)}

/* PAGE SYSTEM */
.page{display:none;flex:1;flex-direction:column;overflow:hidden;min-height:0}
/* Fond opaque explicite quand la texture d'interface est active (.ui-bg-enabled, cf. son commentaire dans
   TOPBAR plus haut) mais pas le fond de module (.module-bg-enabled, règle suivante, gère déjà son propre cas
   avec sa propre image) — sans lui, le calque UNIQUE position:fixed de la texture d'interface (posé sur
   <body>, derrière tout le contenu) traversait la zone de contenu du module, qui n'a normalement aucun fond
   propre (rapporté avec capture : l'image de la texture d'interface débordait dans le contenu).
   isolation:isolate ici crée un nouveau contexte d'empilement LOCAL à .page : ça empêche structurellement le
   calque body-level de traverser un enfant transparent de .page, y compris .subbar (constaté avec capture :
   un simple z-index ne suffit PAS à percer une isolation — affirmation erronée du commentaire précédent).
   .subbar reçoit donc ici son PROPRE calque ::before local (pour rester affectée par la texture malgré
   l'isolation de son parent .page) mais avec background-attachment:fixed — l'image se positionne par rapport
   au VIEWPORT, pas à la boîte locale, donc elle tombe exactement sur la même portion que le calque partagé
   de topbar/sidebar (demande explicite : "il faut matcher", décalage visible sinon). Contrairement aux
   tentatives précédentes de calque UNIQUE qui combinaient (à tort) background-attachment:fixed avec
   transform/filter SUR LE MÊME ÉLÉMENT (ce qui neutralise l'ancrage viewport, cf. historique dans TOPBAR
   plus haut), ce calque-ci n'a PAS de transform, seulement filter — sans transform, aucun conflit constaté.
   var(--ui-bg-pos) partagée avec le calque body/#uiBgRotateLayer pour rester cohérent visuellement si le
   cadrage est ajusté (Paramètres > position X/Y) ; PAS de rotation ici en revanche (var(--ui-bg-rotate)) —
   elle casserait l'ancrage fixed de ce calque local par le même mécanisme. Limitation acceptée : avec une
   rotation active, seules topbar/sidebar/ligne de tonalité (calque body, #uiBgRotateLayer) tournent ; la
   subbar reste non tournée dans ce cas précis (fond de module désactivé + rotation réglée). */
.ui-bg-enabled .page:not(.module-bg-enabled){position:relative;isolation:isolate;background:var(--bg)}
.ui-bg-enabled .page:not(.module-bg-enabled) .subbar{position:relative;isolation:isolate;background:transparent}
.ui-bg-enabled .page:not(.module-bg-enabled) .subbar::before{content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;background-image:linear-gradient(rgba(0,0,0,var(--ui-bg-scrim)),rgba(0,0,0,var(--ui-bg-scrim))),var(--ui-bg-image);background-size:cover;background-position:var(--ui-bg-pos,center);background-attachment:fixed;filter:blur(var(--ui-bg-blur)) saturate(calc(var(--ui-bg-saturation) * 2));opacity:var(--ui-bg-opacity)}
.ui-bg-enabled .page:not(.module-bg-enabled) .subbar>*{position:relative;z-index:1}
.page.module-bg-enabled{position:relative;isolation:isolate;background:var(--bg)}
.page.module-bg-enabled::before{
   content:"";position:absolute;inset:-7%;z-index:-1;pointer-events:none;
   background-image:linear-gradient(rgba(0,0,0,var(--module-bg-scrim)),rgba(0,0,0,var(--module-bg-scrim))),var(--module-bg-image);
   background-size:cover;background-position:var(--module-bg-pos,center);background-repeat:no-repeat;
   opacity:var(--module-bg-opacity);filter:blur(var(--module-bg-blur)) saturate(calc(var(--module-bg-saturation) * 2));transform:scale(calc(1.08 * var(--module-bg-zoom,1))) rotate(var(--module-bg-rotate,0deg));
}
/* Fond unique : le calque d'interface plein viewport traverse les pages (motif continu, aucun recadrage par module) */
body.single-bg .page.module-bg-enabled{background:transparent}
body.single-bg .page.module-bg-enabled::before{display:none}
.page.module-bg-enabled>.sticky-toolbar,
.page.module-bg-enabled>.scales-body,
#page-metro.module-bg-enabled>.wrap,
/* Harmonie/Drone/Strum/Inspiration/Eartrain n'ont pas de .sticky-toolbar dédiée : leur PREMIER enfant direct
   de .page est le wrapper de TOUT le contenu du module (.hn-app, .drone-wrap, .rh-wrap, .spark-wrap,
   .rd-wrap), pas une simple barre d'outils — la règle générique ".page>div:first-child{background:var(--sf);
   position:sticky;z-index:100}" (pensée pour une vraie barre sticky comme #page-metro/#page-groove) le
   rendait donc opaque en entier, masquant le fond de module ET empêchant tout effet de verre sur les cartes
   à l'intérieur, même avec .rcard/.drone-card/etc. correctement transparents (bug rapporté : "toujours
   opaques" malgré l'image de fond visible). */
.page.module-bg-enabled>.hn-app,
.page.module-bg-enabled>.drone-wrap,
.page.module-bg-enabled>.rh-wrap,
.page.module-bg-enabled>.spark-wrap,
.page.module-bg-enabled>.rd-wrap{position:relative;z-index:1;background:transparent;border:0;box-shadow:none}
/* Même bug que ci-dessus, variante "en-tête" : #triadsHeader/#azHeader/#intervallesHeader/#strHeader/
   .chords-top sont eux aussi le PREMIER enfant direct de leur .page, avec en plus leur PROPRE fond opaque
   inline (background:var(--sf)) qui s'ajoute par-dessus — un simple ajout à la liste de verre ne suffit pas
   ici, il faut !important pour battre l'inline. Rendus transparents comme .sticky-toolbar (même rôle : une
   barre d'en-tête, pas le contenu du module) ; le verre visible vient de la barre interne qu'ils enveloppent
   (.subbar/.triads-subbar-scroll/.chords-top .rrow, cf. liste de verre plus bas). */
.page.module-bg-enabled>#triadsHeader,
.page.module-bg-enabled>#azHeader,
.page.module-bg-enabled>#intervallesHeader,
.page.module-bg-enabled>.chords-top{background:transparent!important;border-color:transparent}
/* #strHeader (Backing) enveloppe 2 rangées SANS classe .subbar propre (contrairement à triads/az/intervalles
   qui enveloppent une vraie barre interne à glasser séparément) : le verre est donc posé directement sur ce
   conteneur plutôt que de le rendre transparent, sans quoi les 2 rangées internes resteraient sans aucun
   fond (juste la photo brute, pas l'effet verre dépoli). */
.page.module-bg-enabled>#strHeader{
  background:color-mix(in srgb,var(--sf) var(--glass-opacity,84%),transparent)!important;
  
  border-color:transparent;
  position:relative!important;z-index:1!important;
}
/* #page-circle et #page-progressions n'ont ni .sticky-toolbar ni wrapper d'en-tête dédié : leur .subbar EST
   le premier enfant direct de .page, donc directement soumis à la règle générique ".page>div:first-child"
   (styles.css ~ligne 99, spécificité 0 ID/2 classes-pseudo/1 élément) qui bat la règle de verre générique
   ".page.module-bg-enabled .subbar" (0 ID/2 classes/0 élément) à spécificité égale de classes mais avec un
   élément en plus — !important nécessaire ici pour rétablir le verre (même famille de bug que .hn-app/
   .drone-wrap/etc. plus haut, mais sur une VRAIE barre d'outils cette fois, donc verre direct plutôt que
   transparence). */
.page.module-bg-enabled>.subbar{
  background:color-mix(in srgb,var(--sf) var(--glass-opacity,84%),transparent)!important;
  
}
/* .insp-toolbar (Progressions, mode Banque) a un background inline (background:var(--sf), index.html) —
   !important nécessaire, une classe seule ne bat jamais un style inline. */
.page.module-bg-enabled .insp-toolbar{
  background:color-mix(in srgb,var(--sf) var(--glass-opacity,84%),transparent)!important;
  
}
#page-metro.module-bg-enabled>.wrap>.card{border-color:transparent;box-shadow:none}
.page.module-bg-enabled .scale-fs{z-index:9000}
#page-scales.scale-fs-page-active{position:fixed;inset:0;z-index:10000;width:100vw;height:100vh;display:flex;background:var(--bg)}
/* Effet "verre dépoli" appliqué à toutes les cartes/subbars quand un fond de module est actif — généralisé
   depuis l'effet initialement propre au métronome (#page-metro), désormais retiré au profit de cette règle
   commune (demande explicite : "étendre la transparence aux autres modules"). Sans fond de module actif,
   .card/.subbar restent opaques (var(--sf) plein) comme avant : ces règles ne s'appliquent qu'à l'intérieur
   de .module-bg-enabled.
   BUG CORRIGÉ (rapporté : "effet verre dépoli uniquement actif sur le module métronome") — le sélecteur ne
   ciblait QUE la classe exacte .card, mais la plupart des autres modules utilisent leur propre variante
   (.rh-card, .rd-card, .spark-card, .drone-card, .fbcard, .insp-card, .prog-preview-card...), jamais captée.
   2e tentative [class$="-card"] (sélecteur d'attribut, testait la fin littérale de la chaîne class="...")
   ABANDONNÉE : ne matche que si "-card" est la DERNIÈRE classe de l'élément — échoue silencieusement dès
   qu'une 2e classe suit (ex. class="rh-card rh-sticky-player", qui se termine par "-player", pas "-card" —
   constaté avec Playwright sur #page-rhythm .rh-sticky-player, toujours opaque malgré la règle). Liste
   explicite ci-dessous à la place : plus verbeuse mais fiable, sans piège de correspondance partielle. Une
   nouvelle classe carte ajoutée plus tard devra être ajoutée ici à la main.
   Intensité réglable (demande explicite : "permettre à l'utilisateur de régler les paramètres de l'effet") —
   --glass-opacity (0-100, opacité du fond var(--sf) sous les cartes) et --glass-blur (px, flou du fond
   derrière) posés globalement sur <body> par applyModuleBackgroundSettings() (cf. app.js), avec les mêmes
   valeurs par défaut (84%, 12px) que l'ancien réglage fixe propre au métronome. saturate(0) reste fixe (pas
   un réglage exposé) : c'est ce qui empêche une image en couleur de teinter les cartes, un choix de design
   plutôt qu'un curseur d'intensité. */
.page.module-bg-enabled .card,
.page.module-bg-enabled .rh-card,
.page.module-bg-enabled .rd-card,
.page.module-bg-enabled .spark-card,
.page.module-bg-enabled .drone-card,
.page.module-bg-enabled .fbcard,
.page.module-bg-enabled .az-fbcard,
.page.module-bg-enabled .az-rescard,
.page.module-bg-enabled .insp-card,
.page.module-bg-enabled .prog-preview-card,
.page.module-bg-enabled .prog-block-card,
.page.module-bg-enabled .vcard,
.page.module-bg-enabled .rcard,
.page.module-bg-enabled .metro-feat-item,
.page.module-bg-enabled .diat-row,
.page.module-bg-enabled .ref-table-panel,
.page.module-bg-enabled .scales-left,
.page.module-bg-enabled .scales-drawer-handle,
.page.module-bg-enabled #chStackBuilder,
.page.module-bg-enabled .hn-scroll,
.page.module-bg-enabled .hn-input-a,
.page.module-bg-enabled .hn-input-b,
.page.module-bg-enabled .hn-timeline,
.page.module-bg-enabled .hn-results,
.page.module-bg-enabled .hn-sec,
.page.module-bg-enabled .hn-mode-row,
.page.module-bg-enabled .hn-mel-beat,
.page.module-bg-enabled .hn-note-grid,
.page.module-bg-enabled .hn-qual-grid,
.page.module-bg-enabled .hn-pool-grid,
.page.module-bg-enabled .hn-prog-strip,
.page.module-bg-enabled .ch-stack-key-empty,
.page.module-bg-enabled .ch-stack-legend,
.page.module-bg-enabled .triads-subbar-scroll,
.page.module-bg-enabled .mel-toolbar,
.page.module-bg-enabled .subbar{
  background:color-mix(in srgb,var(--sf) var(--glass-opacity,84%),transparent);
   box-shadow:0 10px 30px rgba(0,0,0,var(--glass-shadow-opacity,.22));
  
}
/* Wrappers internes opaques: laisser apparaître le fond du module derrière eux et réserver le verre aux
    unités de contenu réellement lisibles. */
.page.module-bg-enabled#page-harmony .hn-left,
.page.module-bg-enabled#page-harmony .hn-right{background:transparent!important;-webkit-backdrop-filter:none;backdrop-filter:none}
.page.module-bg-enabled#page-harmony .hn-sec,
.page.module-bg-enabled#page-harmony .hn-mode-row,
.page.module-bg-enabled#page-harmony .hn-timeline,
.page.module-bg-enabled#page-harmony .hn-results,
.page.module-bg-enabled#page-harmony .hn-prog-strip,
.page.module-bg-enabled#page-harmony .hn-mel-beat,
.page.module-bg-enabled#page-harmony .hn-np,
.page.module-bg-enabled#page-drone .drone-card,
.page.module-bg-enabled#page-rhythm .rh-card,
.page.module-bg-enabled#page-eartraining .rd-card{
   background:color-mix(in srgb,var(--sf) var(--glass-opacity,84%),transparent)!important;
   box-shadow:0 10px 30px rgba(0,0,0,var(--glass-shadow-opacity,.22));
   
}
.groove-track-rows-host{background:var(--s2)}
.str-track-row{background:var(--s2)}
/* Groovebox/Backing (Structures) : le conteneur de pistes et chaque ligne avaient un fond uni (var(--s2))
   posé en plus de la .card/.str-canvas-col parente, masquant son verre dans les interstices entre pistes
   (séparateurs, padding) — seules les cellules de pas (.groove-track-grid, verre plus bas) ou rien
   (Backing, aucune règle) étaient dépolies, jamais l'espace autour ("groovebox en partie [...] derrière les
   pistes", signalé). Rendus transparents ici sur fond de module actif pour laisser apparaître le verre du
   parent à la place. */
.page.module-bg-enabled .groove-track-rows-host,
.page.module-bg-enabled .str-track-rows-host{background:transparent}
.page.module-bg-enabled .str-track-row{
  background:color-mix(in srgb,var(--sf) var(--glass-opacity,84%),transparent)!important;
  
}
/* Ces modules gardent leur verre même sans image de fond de module. La Groovebox applique le verre au fond
   de chaque piste, jamais à la grille de cellules. */
#page-harmony .hn-left,
#page-harmony .hn-right{background:transparent!important;-webkit-backdrop-filter:none;backdrop-filter:none}
#page-harmony .hn-sec,
#page-harmony .hn-mode-row,
#page-harmony .hn-timeline,
#page-harmony .hn-results,
#page-harmony .hn-prog-strip,
#page-harmony .hn-mel-beat,
#page-harmony .hn-np,
#page-drone .drone-card,
#page-rhythm .rh-card,
#page-eartraining .rd-card,
:is(#page-groove,.gsz-gb) .groove-track-grid{
  background:color-mix(in srgb,var(--sf) var(--glass-opacity,84%),transparent)!important;
  box-shadow:0 10px 30px rgba(0,0,0,var(--glass-shadow-opacity,.22));
  
}
/* Cas texture d'interface + fond de module actifs ENSEMBLE sur la même page : .subbar est à l'intérieur de
   .page.module-bg-enabled, qui a isolation:isolate (nécessaire pour empiler son propre fond d'image) — ça
   empêche structurellement le calque body-level de la texture d'interface (position:fixed, TOPBAR plus
   haut) de traverser un enfant transparent de .page, quel que soit le z-index. Contournement testé et
   abandonné (rendait la subbar transparente vers le fond du module, pas vers la texture interface — pas le
   résultat voulu). Décision (demande explicite) : dans ce cas combiné, le fond de MODULE reste prioritaire
   sur la subbar (règle "verre dépoli" ci-dessus, cohérent avec les .card du même module juste en dessous) ;
   seules topbar/sidebar/ligne de tonalité dédiée (hors .page) montrent la texture d'interface. */
.page.active{display:flex;animation:pageIn 150ms ease both}
@keyframes rootWave{
  0%{transform:scale(0);opacity:.6}
  100%{transform:scale(1);opacity:0}
}
.root-wave{
  position:fixed;
  border-radius:50%;
  pointer-events:none;
  z-index:8000;
  transform:scale(0);
  animation:rootWave 600ms cubic-bezier(.1,.7,.3,1) forwards;
}
@keyframes chordPulse{0%{transform:scale(1)}30%{transform:scale(1.18)}60%{transform:scale(.96)}100%{transform:scale(1)}}
@keyframes dronePulse{0%,100%{transform:scale(1);opacity:.7}50%{transform:scale(1.18);opacity:.3}}
@keyframes dronePulse{0%,100%{transform:scale(1);opacity:.7}50%{transform:scale(1.18);opacity:.3}}
/* sealPulse : anneau pulsant en transform+opacity (compositable GPU) plutôt que box-shadow animé (cf. audit
   performance — box-shadow force un repaint complet à 60fps en continu tant que le bouton est actif, un
   anneau expansif via ::after + transform:scale/opacity obtient le même effet visuel sans repaint). */
@keyframes sealPulse{0%{transform:scale(1);opacity:.5}100%{transform:scale(1.35);opacity:0}}
#droneSealBtn.active,#droneWhiteNoiseBtn.active,#dronePinkNoiseBtn.active{position:relative;border-color:rgba(255,255,255,.6)}
#droneSealBtn.active::after,#droneWhiteNoiseBtn.active::after,#dronePinkNoiseBtn.active::after{
  content:'';position:absolute;inset:-1px;border-radius:inherit;border:1px solid rgba(255,255,255,.5);
  pointer-events:none;animation:sealPulse 1.5s ease-in-out infinite;
}
#droneHzSlider.playing{animation:droneTrackPulse 2s ease-in-out infinite}
.str-chord-active{border-bottom:2px solid currentColor}

/* SHARED ATOMS */
.rrow{display:flex;gap:2px;overflow-x:auto;flex-wrap:nowrap;scrollbar-width:none}
.rrow::-webkit-scrollbar{height:0}
.rbtn{min-width:26px;height:26px;padding:0 4px;border-radius:5px;border:1px solid var(--bd);background:var(--pbg);color:var(--tx2);font-family:var(--fm);font-size:.75rem;cursor:pointer;transition:all var(--tr);display:flex;align-items:center;justify-content:center;flex-shrink:0}
@media(hover:hover){.rbtn:hover{border-color:var(--c-root);color:var(--tx)}}
.rbtn.active{background:var(--c-root);border-color:var(--c-root);color:#111;font-weight:700}
.pg{display:flex;align-items:center;gap:2px;background:var(--s2);border:1px solid var(--bd);border-radius:7px;padding:2px;height:26px;box-sizing:border-box;flex-shrink:0}
.pill{display:flex;align-items:center;justify-content:center;height:100%;box-sizing:border-box;padding:0 9px;border-radius:5px;border:1px solid transparent;background:transparent;color:var(--tx2);font-family:var(--fm);font-size:.62rem;cursor:pointer;transition:all var(--tr);white-space:nowrap}
@media(hover:hover){.pill:hover{color:var(--tx)}}
.pill.active{border-color:rgba(255,255,255,.6);color:var(--tx);font-weight:700;box-shadow:0 1px 3px var(--sh)}
select.st{padding:4px 7px;border-radius:var(--r);border:1px solid var(--bd);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.65rem;cursor:pointer;outline:none;-webkit-appearance:none;appearance:none}
.tog{width:30px;height:17px;border-radius:999px;background:var(--s3);border:1px solid var(--bd);position:relative;cursor:pointer;transition:background var(--tr);flex-shrink:0}
.tog.on{background:var(--c-root);border-color:var(--c-root)}
.tog::after{content:'';position:absolute;width:11px;height:11px;border-radius:50%;background:var(--sf);top:2px;left:2px;transition:left var(--tr);box-shadow:0 1px 2px var(--sh)}
.tog.on::after{left:15px;background:#111}
.togrow{display:flex;align-items:center;justify-content:space-between;padding:2px 0}
.toglbl{font-size:.7rem;color:var(--tx2)}
.ic{padding:3px 10px;border-radius:999px;border:1px solid var(--bd);font-family:var(--fm);font-size:.63rem;color:var(--tx2);background:var(--s2)}
.ic strong{color:var(--tx)}

/* LAYOUT */
.abody{flex:1;display:flex;overflow:hidden;min-height:0}
.abody.portrait{flex-direction:column}
.sidebar{width:280px;flex-shrink:0;border-right:1px solid var(--bd);background:var(--sf);overflow-y:auto;overflow-x:hidden;display:flex;flex-direction:column;transition:width var(--tr),opacity var(--tr)}
.sidebar.closed{width:0;overflow:hidden;opacity:0;border:none}
.abody.portrait .sidebar{width:100%;max-height:200px;border-right:none;border-bottom:1px solid var(--bd);overflow-y:auto;flex-direction:row;flex-wrap:wrap;align-items:flex-start;gap:10px;padding:10px}
.abody.portrait .sidebar.closed{max-height:0;padding:0;overflow:hidden}
.asec{border-bottom:1px solid var(--bd)}
.ahead{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;cursor:pointer;user-select:none;font-family:var(--fm);font-size:max(10px,.52rem);letter-spacing:.1em;text-transform:uppercase;color:var(--tx3);transition:background var(--tr)}
.ahead:hover,.ahead.open{background:var(--s2);color:var(--tx2)}
.ahead .arr{font-size:max(10px,.45rem);transition:transform .14s}
.ahead.open .arr{transform:rotate(90deg)}
.abody2{display:none;padding:6px 10px 8px;flex-direction:column;gap:5px}
.abody2.open{display:flex}
.trow{display:flex;align-items:center;gap:3px}
.tlbl{font-size:max(10px,.55rem);font-family:var(--fm);color:var(--tx3);white-space:nowrap}
.tval{flex:1;text-align:center;font-family:var(--fm);font-size:.6rem;color:var(--tx2)}
.tbtn{width:20px;height:20px;border-radius:4px;border:1px solid var(--bd);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.8rem;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all var(--tr);flex-shrink:0}
@media(hover:hover){.tbtn:hover{background:var(--s3);border-color:var(--bd2)}}
.ti{display:flex;flex-direction:column;gap:2px}
.srow-t{display:flex;align-items:center;gap:3px}
.slbl{font-family:var(--fm);font-size:max(10px,.52rem);color:var(--tx3);width:12px;text-align:right;flex-shrink:0}
.snb{flex:1;padding:3px 4px;border-radius:4px;border:1px solid var(--bd);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.6rem;cursor:pointer;text-align:center;transition:all var(--tr)}
.snb:hover{border-color:var(--bd2);background:var(--s3)}
.cgrid{display:grid;grid-template-columns:1fr 1fr;gap:5px}
.ci{display:flex;flex-direction:column;gap:2px}
.cl{font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3)}
.csr{display:flex;align-items:center;gap:3px}
input[type="color"]{width:22px;height:22px;border-radius:5px;border:1px solid var(--bd);background:transparent;cursor:pointer;padding:1px}
.chex{font-family:var(--fm);font-size:max(10px,.52rem);color:var(--tx2)}
.caged-row{display:flex;align-items:center;gap:2px;flex-wrap:wrap}
.caged-badge{display:flex;align-items:center;justify-content:center;height:26px;box-sizing:border-box;padding:0 6px;border-radius:3px;border:1px solid var(--bd);background:var(--pbg);font-family:var(--fm);font-size:max(10px,.58rem);color:var(--tx3);cursor:pointer;transition:all var(--tr)}
.caged-badge.active{border-color:var(--c-root);color:var(--c-root);background:var(--s2);font-weight:700}
.main{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;padding:2px;overflow-y:auto;overflow-x:hidden;gap:2px}
.inforow{display:flex;gap:6px;flex-wrap:wrap;align-items:center;flex-shrink:0}
/* Bouton info générique + popover — cf. infoBtn()/toggleInfoPopover() dans app.js. Composant unique
   réutilisé par Gammes et Accords (et tout futur module) plutôt qu'une bulle par module. */
.info-btn{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;border:1px solid var(--bd);background:var(--pbg);color:var(--tx3);font-family:var(--fm);font-size:max(10px,.56rem);font-weight:700;cursor:pointer;flex-shrink:0;transition:all var(--tr);-webkit-tap-highlight-color:transparent;position:relative}
.info-btn:hover,.info-btn.active{border-color:var(--c-root);color:var(--c-root);background:var(--s2)}
/* max-height+overflow-y:auto : filet de sécurité pour un texte d'aide plus long que le viewport n'est haut
   (ex. #azInfoBtn de l'Analyseur, ~672px de contenu déjà rencontré) — le JS (toggleInfoPopover) clampe déjà
   la position dans les bornes du viewport, mais un popover qui dépasserait quand même 90% de la hauteur
   d'écran scrolle désormais en interne plutôt que de continuer à déborder hors champ. */
/* z-index:60 → 150 : bug trouvé (rapporté "les boutons i n'ouvrent rien") — un module dont le PREMIER <div>
   enfant de sa .page sert de conteneur pleine page (ex. #page-spark > .spark-wrap) hérite malencontreusement
   de la règle générique ".page>div:first-child{position:sticky;z-index:100}" pensée pour une vraie barre
   d'outils collante (cf. commentaire existant sur ce même piège pour #page-metro plus bas dans ce fichier),
   ce qui plafonne tout descendant en-dessous de z-index:100 — le popover (attaché au <body>, donc visible en
   DOM/coordonnées correctes) se retrouvait donc visuellement RECOUVERT par .spark-wrap malgré tout, aucune
   erreur JS pour le signaler. 150 reste sous les vrais overlays/modales (900+) tout en dépassant n'importe
   quel z-index:100 hérité de cette règle générique. */
.info-popover{position:absolute;z-index:150;max-width:260px;max-height:90vh;overflow-y:auto;padding:8px 10px;border-radius:var(--r);border:1px solid var(--bd);background:var(--sf);box-shadow:0 4px 16px var(--sh);font-family:var(--fb);font-size:.62rem;line-height:1.45;color:var(--tx2);text-align:left;white-space:normal}
.info-popover strong{color:var(--tx)}
/* Bouton "i" de fin de liste (Gammes/Accords) + modal catalogue — cf. catalogInfoBtn()/showCatalogModal()
   dans app.js. Même famille visuelle que .info-btn mais légèrement plus grand car isolé en bout de liste
   plutôt qu'accolé à un libellé. */
.info-btn-catalog{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;border:1px solid var(--bd);background:var(--pbg);color:var(--tx3);font-family:var(--fm);font-size:.6rem;font-weight:700;cursor:pointer;flex-shrink:0;transition:all var(--tr);-webkit-tap-highlight-color:transparent;margin-left:2px}
.info-btn-catalog:hover{border-color:var(--c-root);color:var(--c-root);background:var(--s2)}
/* z-index 10600 : doit pouvoir s'ouvrir par-dessus .settings-modal (9999, bouton "?" de Modules visibles)
   en plus de son usage historique dans Gammes/Accords (contenu de page normal) — donc au-dessus de tout. */
.catalog-modal-ov{position:fixed;inset:0;z-index:10600;background:var(--ov);display:flex;align-items:center;justify-content:center;padding:20px}
.catalog-modal{width:100%;max-width:560px;max-height:80vh;display:flex;flex-direction:column;border-radius:var(--rl);border:1px solid var(--bd);background:var(--sf);box-shadow:0 8px 32px var(--sh)}
.catalog-modal-hd{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--bd);flex-shrink:0}
.catalog-modal-hd strong{font-size:.8rem;color:var(--tx)}
/* .modal-close-btn : style commun aux boutons de fermeture ronds ("✕") des popups/modales de l'app —
   auparavant dupliqué à l'identique sous .catalog-modal-close/.settings-close/.tuner-popup-close (même
   composant visuel, trois définitions séparées, découvert en corrigeant un bug similaire sur .diat-play/
   .vcard-play : une dérive silencieuse entre les trois était possible sans qu'on s'en rende compte). Modif.
   .modal-close-btn-sm (24px, cf. .catalog-modal-close/.tuner-popup-close) réservé aux popups plus petites
   que Réglages (28px, taille par défaut). Chaque classe d'origine reste posée dans le HTML/JS pour ne rien
   casser (sélecteurs existants comme .tuner-popup-close utilisé aussi pour le bouton "détacher" ⇱, pas
   seulement fermer) — elles ne portent plus elles-mêmes aucune propriété visuelle. */
.modal-close-btn{width:28px;height:28px;border-radius:50%;border:1px solid var(--bd);background:transparent;color:var(--tx2);font-size:.9rem;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all var(--tr)}
.modal-close-btn:hover{border-color:var(--c-root);color:var(--c-root)}
.modal-close-btn-sm{width:24px;height:24px;font-size:.7rem}
.catalog-modal-close{background:var(--pbg)}
.catalog-modal-body{overflow-y:auto;padding:8px 16px 16px;display:flex;flex-direction:column;gap:10px}
.catalog-modal-item{padding-bottom:10px;border-bottom:1px solid var(--bd)}
.catalog-modal-item:last-child{border-bottom:none;padding-bottom:0}
/* Desktop large : le modal suit le plafond de largeur du corps de module (1600px, cf. .wrap) plutôt que
   les 560px pensés pour mobile/tablette. Au-delà de ~700px de large, une seule colonne de texte devient
   illisible (lignes trop longues) — passage en grille multi-colonnes de cartes à la place. */
@media(min-width:900px){
  .catalog-modal{max-width:1600px}
  .catalog-modal-body{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px 20px;align-content:start}
  .catalog-modal-item{border-bottom:none;padding-bottom:0;padding:10px;border-radius:var(--r);background:var(--pbg)}
}
/* Variante ouverte par-dessus .settings-modal (bouton "?" de Modules visibles, cf. showModuleInfo() dans
   app.js) : reprend exactement les largeurs de .settings-box pour rester visuellement dans le cadre de la
   fenêtre Paramètres plutôt que de s'étaler en pleine largeur d'écran comme l'usage catalogue normal. Une
   seule carte affichée → pas besoin de la grille multi-colonnes desktop, une colonne reste lisible. */
.catalog-modal--settings{max-width:480px}
@media(min-width:1024px){
  .catalog-modal--settings{max-width:640px}
}
.catalog-modal--settings .catalog-modal-body{display:flex;flex-direction:column}
.catalog-modal-item-name{font-family:var(--fm);font-size:.68rem;font-weight:700;color:var(--tx);display:block;margin-bottom:2px}
.catalog-modal-item-desc{font-family:var(--fb);font-size:.64rem;line-height:1.5;color:var(--tx2)}
.catalog-modal-item-desc em{color:var(--tx3)}
/* Palette de commande (raccourci T) — cf. cmdPaletteOpen()/cmdPaletteSearch() dans app.js. z-index 10500 :
   sous le modal catalogue (10600, cf. plus haut) mais au-dessus de l'onboarding global/tour guidé
   (10000-10003), pour rester utilisable même si l'utilisateur l'ouvre pendant un tour (cas limite mais
   jamais bloquant). */
.cmdp-ov{position:fixed;inset:0;z-index:10500;background:var(--ov);display:flex;align-items:flex-start;justify-content:center;padding:80px 16px 16px}
.cmdp-box{width:100%;max-width:560px;max-height:70vh;display:flex;flex-direction:column;border-radius:var(--rl);border:1px solid var(--bd);background:var(--sf);box-shadow:0 16px 48px var(--sh);overflow:hidden}
.cmdp-input-row{display:flex;align-items:center;gap:8px;padding:12px 14px;border-bottom:1px solid var(--bd);flex-shrink:0}
.cmdp-input-icon{font-family:var(--fm);color:var(--tx3);font-size:.8rem;flex-shrink:0}
.cmdp-input{flex:1;border:none;background:transparent;color:var(--tx);font-family:var(--fb);font-size:.85rem;outline:none;min-width:0}
.cmdp-input::placeholder{color:var(--tx3)}
.cmdp-hint{font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx3);border:1px solid var(--bd);border-radius:4px;padding:1px 5px;flex-shrink:0;white-space:nowrap}
.cmdp-results{overflow-y:auto;padding:6px}
.cmdp-group-label{font-family:var(--fm);font-size:max(10px,.56rem);font-weight:700;color:var(--tx3);text-transform:uppercase;letter-spacing:.06em;padding:8px 10px 4px}
.cmdp-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:var(--r);cursor:pointer;-webkit-tap-highlight-color:transparent}
.cmdp-item:hover,.cmdp-item.active{background:var(--s2)}
.cmdp-item-icon{font-family:var(--fm);font-size:.75rem;color:var(--tx3);width:20px;text-align:center;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center}
.cmdp-item-label{font-family:var(--fb);font-size:.72rem;color:var(--tx);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cmdp-item-sub{font-family:var(--fm);font-size:max(10px,.58rem);color:var(--tx3);flex-shrink:0}
.cmdp-empty{padding:20px 14px;text-align:center;font-family:var(--fb);font-size:.68rem;color:var(--tx3)}
.cmdp-item-label mark{background:transparent;color:var(--c-root);font-weight:700}
.legstrip{display:flex;gap:8px;flex-wrap:wrap;padding:7px 11px;border-radius:var(--r);border:1px solid var(--bd);background:var(--sf);align-items:center;flex-shrink:0}
.li{display:flex;align-items:center;gap:4px;font-size:.62rem;color:var(--tx2)}
.ld{width:14px;height:14px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:max(10px,.44rem);font-weight:700;font-family:var(--fm);flex-shrink:0}
/* position:relative ici plutôt qu'en style inline sur #scaleFbCard (index.html) : un style inline a
   toujours priorité sur une règle de classe externe quelle que soit sa spécificité, donc ce
   position:relative écrasait le position:fixed de .fbcard.scale-fs (cf. juste plus bas) — le manche
   "s'agrandissait dans une box" au lieu de passer en vrai plein écran (bug rapporté). Ici, la classe
   .fbcard.scale-fs (2 classes = spécificité plus forte que .fbcard seule) reprend normalement la main. */
.fbcard{position:relative;background:var(--sf);border:1px solid var(--bd);border-radius:var(--rl);padding:2px;overflow-x:auto;flex:1;min-width:0;display:flex;align-items:flex-start}
/* Triades desktop : légende compacte quand fusionnée dans la subbar, pour égaler la hauteur d'Intervalles */
#triadsInfoRow{display:flex;align-items:center;gap:8px;flex-shrink:0;white-space:nowrap}
#triadsInfoRow .inforow,#triadsInfoRow .legstrip{flex-wrap:nowrap;margin:0;padding:0}
#triadsInfoRow .legstrip{border:0;background:transparent}
#triadsInfoRow .ic{white-space:nowrap}
@media(min-width:1024px){
  /* Triades/Intervalles : manche + piano toujours affichés ensemble, chacun à hauteur fixe (~500px) — pas d'étirement/centrage créant du vide */
  #page-triads .fbcard,#page-intervalles .fbcard{flex:0 0 auto!important}
  #page-triads #fb,#page-intervalles #ivFb{align-self:flex-start!important}
  /* Gammes : manche à hauteur fixe (~420px) — pas d'étirement créant du vide */
  #page-scales .fbcard{flex:0 0 auto!important}
}
#page-scales #scaleFb{align-self:flex-start!important}
.fbcard.scale-fs{position:fixed;inset:0;z-index:9000;border-radius:0;border:none;padding:8px;background:var(--bg);flex-direction:column;justify-content:center;cursor:zoom-out;overflow:auto}
.fbcard.scale-fs canvas{flex-shrink:0}
/* Mini-canvas "progression en cours" en plein écran manche (cf. #scaleFbCardWrap dans index.html,
   toggleScaleFullscreen() dans app.js qui ne touche que .scale-fs sur #scaleFbCard lui-même, jamais sur ce
   wrapper — repositionné ici via un sélecteur frère plutôt que de risquer de modifier cette logique de
   plein écran déjà éprouvée). #scaleFbCard.scale-fs passe en position:fixed;inset:0 et sort du flux normal
   de #scaleFbCardWrap ; sans cette règle, le mini-canvas (resté en flux normal, sous le manche désormais
   fixed) se retrouverait cadré au même endroit qu'avant le passage en plein écran — visuellement perdu/mal
   positionné derrière le manche agrandi plutôt que de rester une bande fine tout en bas du plein écran
   (demande explicite : "y compris en mode zoom plein écran du fretboard"). */
#scaleFbCardWrap:has(.fbcard.scale-fs){position:fixed;inset:0;z-index:9001;display:flex;flex-direction:column;background:var(--bg);padding:8px;gap:6px}
#scaleFbCardWrap:has(.fbcard.scale-fs)>.fbcard.scale-fs{flex:1;min-height:0;position:relative;inset:auto;z-index:auto;padding:0}
#scaleFbCardWrap:has(.fbcard.scale-fs)>#strMiniCanvasWrap{flex-shrink:0}
canvas{display:block;cursor:pointer;height:auto;width:auto}
.subbar{display:flex;align-items:center;gap:4px;padding:4px;border-bottom:1px solid var(--bd);background:var(--sf);flex-wrap:wrap;flex-shrink:0;min-height:36px;box-sizing:border-box}

/* SCALES */
.scales-body{flex:1;display:flex;flex-direction:column;overflow:hidden;min-height:0}
/* Tiroir repliable de la liste des gammes : avec les catégories ajoutées au fil du temps (modes grecs,
   symétriques, exotiques...), la liste dépliée peut occuper 4-5 lignes et pousser le manche/piano hors de
   vue sur les écrans bas en hauteur (petit portable, grand écran zoomé). "Déplié" par défaut (repris tel
   quel, l'ergonomie actuelle ne change pas tant qu'on n'y touche pas) — l'état est un choix explicite de
   l'utilisateur parmi 3 modes clairement nommés (data-mode="expanded|collapsed|auto", cf. toggleScalesDrawer/
   APP_SETTINGS.scalesDrawerMode dans app.js), persisté plutôt que recalculé, pour ne pas changer tout seul
   sous l'utilisateur en cours de session au moindre resize. La poignée reste toujours visible, affiche la
   gamme active même repliée, et son état courant est écrit en toutes lettres (pas juste un chevron) pour
   rester compréhensible sans avoir à deviner ce qu'un simple picto signifie. */
.scales-drawer-handle{
  display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;
  padding:3px 8px;border:none;border-bottom:1px solid var(--bd);background:var(--sf);
  color:var(--tx2);font-family:var(--fm);font-size:max(10px,.58rem);cursor:pointer;
  -webkit-tap-highlight-color:transparent;box-sizing:border-box;
}
.scales-drawer-handle:hover{color:var(--tx);background:var(--s2)}
/* Fond de module actif : sans ce correctif, :hover (même spécificité que la règle de verre générique,
   déclarée plus tard dans le fichier) regagnait la main et repassait la poignée en fond uni au survol. */
.page.module-bg-enabled .scales-drawer-handle:hover{
  background:color-mix(in srgb,var(--sf) var(--glass-opacity,84%),transparent);
  
}
/* display:flex (pas juste overflow/ellipsis en bloc simple) — nécessaire depuis que le bouton "i" (cf.
   _syncScalesDrawerLabel dans app.js, demande explicite : "déplacer le bouton i à côté du nom de la gamme")
   vit ICI, en frère du <span> de texte tronqué : sans flex, ce <span> texte (display inline par défaut)
   n'aurait pas cédé de largeur au bouton "i" qui le suit, qui se serait retrouvé poussé hors de la zone
   visible (min-width:0 sur le span texte permet à son overflow:hidden/ellipsis de jouer son rôle DANS un
   flex item, un span sans ça grandirait à son contenu plein malgré tout). */
.scales-drawer-current{display:flex;align-items:center;min-width:0;overflow:hidden}
.scales-drawer-current>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.scales-drawer-state-pill{display:flex;align-items:center;gap:4px;flex-shrink:0;padding:2px 7px;border-radius:999px;border:1px solid var(--bd);background:var(--s2);color:var(--tx2)}
.scales-drawer-chevron{flex-shrink:0;transition:transform var(--tr);line-height:1}
.scales-drawer-state{white-space:nowrap}
/* Rotation du chevron + couleur de la pastille : un renfort visuel redondant avec le texte (jamais la
   seule source d'info) — le mot Déplié/Replié/Auto reste la référence en cas de doute. */
.scales-drawer[data-mode="expanded"] .scales-drawer-chevron{transform:rotate(0deg)}
.scales-drawer[data-mode="collapsed"] .scales-drawer-chevron{transform:rotate(-90deg)}
.scales-drawer[data-mode="auto"] .scales-drawer-chevron{transform:rotate(-90deg)}
.scales-drawer[data-mode="auto"] .scales-drawer-state-pill{border-color:var(--c-root);color:var(--c-root)}
/* max-height généreux (600px) plutôt qu'une valeur ajustée au contenu actuel : sert uniquement de cible
   d'animation pour la transition ouverture/fermeture (comme #chStackBuilder.expanded .ch-stack-body déjà
   dans ce fichier) — la liste dépliée n'est JAMAIS rognée par cette valeur en usage réel, seuls les modes
   "collapsed"/"auto" (max-height:0 au repos) contraignent réellement la hauteur. */
.scales-drawer[data-mode="collapsed"] .scales-left,
.scales-drawer[data-mode="auto"] .scales-left{max-height:0;padding-top:0;padding-bottom:0;border-bottom:none;overflow:hidden}
/* Mode "auto" uniquement : dépliage temporaire au survol (souris uniquement — @media(hover:hover) exclut
   le tactile, où :hover resterait "collé" après un tap et bloquerait le tiroir ouvert en permanence). Ne
   change pas le mode persisté, juste un aperçu borné (scroll interne si besoin) qui se referme dès que la
   souris quitte. Le mode "collapsed" volontaire, lui, ne réagit jamais au survol — replié veut dire replié. */
@media(hover:hover){
  .scales-drawer[data-mode="auto"]:hover .scales-left{max-height:220px;padding-top:3px;padding-bottom:3px;border-bottom:1px solid var(--bd);overflow-y:auto}
}
.scales-left{display:flex;flex-wrap:wrap;gap:0;border-bottom:1px solid var(--bd);padding:3px 4px;overflow-y:hidden;overflow-x:hidden;background:var(--sf);flex-shrink:0;width:100%;align-content:flex-start;max-height:600px;transition:max-height var(--tr),padding var(--tr);box-sizing:border-box}
.scales-main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-height:0}
.scale-cat{font-family:var(--fm);font-size:max(10px,.47rem);letter-spacing:.09em;text-transform:uppercase;color:var(--tx3);padding:2px 4px 0;white-space:nowrap;align-self:center}
.scale-pill{padding:2px 7px;border-radius:4px;border:1px solid transparent;background:transparent;color:var(--tx2);font-family:var(--fb);font-size:.6rem;cursor:pointer;transition:all var(--tr);text-align:center;display:inline-block;white-space:nowrap;flex-shrink:0}
.scale-pill:hover{background:var(--s2);color:var(--tx)}
.scale-pill.active{background:var(--s2);border-color:var(--c-root);color:var(--c-root);font-weight:600}
.scales-main{flex:1;display:flex;flex-direction:column;padding:2px;overflow:auto;gap:6px;min-height:0}
/* Empilement d'intervalles interactif (module Accords, sous la grille de voicings) — un slider à crans
   discrets par étage (3ce/Sus, 5te, 6te/7e, Extension), reflète ET modifie l'accord sélectionné. Colonne
   lue de bas en haut (fondamentale en bas, non slidable) : column-reverse sur un HTML écrit dans l'ordre de
   lecture naturel (fondamentale d'abord, puis 3ce→5te→7e→extension, cf. app.js _chStackComputeHtml) —
   column-reverse place le PREMIER enfant du DOM en BAS visuellement, donc la fondamentale écrite en
   premier atterrit bien en bas. (Un essai avec flex-direction:column normale + fondamentale toujours
   première dans le DOM avait produit l'inverse : premier enfant du DOM = haut par défaut sans reverse.)
   Les crans non atteignables (aucun type catalogué ne matche compte tenu des 3 autres étages) restent
   visibles mais grisés — jamais masqués, cf. topo validé. */
.ch-stack-title{font-family:var(--fm);font-size:max(10px,.52rem);font-weight:700;color:var(--tx3);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px}
/* Même contour que .vcard (bordure pleine var(--bd), fond var(--sf), radius var(--r)) — avant ce correctif
   #chStackBuilder n'avait qu'un border-top 1px (simple séparateur), visuellement plus "clair"/moins défini
   qu'une carte de diagramme. gap:10px identique à .vgrid pour que l'espacement vers la grille au-dessus
   soit cohérent avec l'espacement entre cartes de diagrammes. */
/* margin-top:0 (pas 10px) : en layout 2-colonnes (#voicingGrid/#chStackBuilder côte à côte, cf. .chords-cols
   plus haut), un margin-top résiduel désalignait le haut de cette carte par rapport aux cartes de
   diagrammes de la colonne voisine (bug rapporté : l'empileur démarrait plus bas que les diagrammes). Les
   deux colonnes partagent déjà .chords-cols{align-items:flex-start}, donc un simple margin-top:0 suffit à
   les faire démarrer exactement à la même hauteur — l'ancien 10px n'avait de sens qu'en flux vertical
   (espace entre grille et empileur empilés), plus pertinent une fois les deux côte à côte. */
#chStackBuilder{background:var(--sf);border:1px solid var(--bd);border-radius:var(--r);margin-top:0;box-sizing:border-box}
/* "Tous les accords" (S.chordAll) : l'empileur reste visible et cliquable (jamais pointer-events:none —
   c'est justement en interagissant depuis cet état grisé qu'on quitte "Tous" et sélectionne un accord
   concret, cf. chStackPick→pickChordType qui repasse S.chordAll à false), juste visuellement atténué pour
   signaler qu'aucun accord unique n'est actuellement sélectionné. Demande explicite (remplace l'ancien
   comportement où le panneau disparaissait entièrement en mode "Tous"). PAS d'opacity (ancien effet, retiré
   suite à un rapport explicite : sur mobile, #chStackBuilder est en position:fixed par-dessus le contenu de
   page — cf. règle @media(max-width:1023px) plus bas — et l'opacity y laissait deviner ce contenu à travers
   le panneau, rendu jugé indésirable ; retiré aussi sur desktop pour un comportement cohérent des deux
   côtés). Le signal visuel passe maintenant par une bordure en tirets + un texte légèrement assourdi,
   jamais par une transparence qui expose ce qu'il y a derrière. */
#chStackBuilder.ch-stack-disabled{border-style:dashed;color:var(--tx3)}
#chStackBuilder.ch-stack-disabled:hover{color:var(--tx2)}
/* Desktop : PAS de position:sticky. Déplié par défaut (cf. S.chStackExpanded=window.innerWidth>=1300 dans
   app.js), le bloc peut dépasser 350px de haut pour un accord riche en extensions — en sticky, il pouvait
   alors occuper plus de la moitié d'une fenêtre de hauteur modeste, ne laissant presque plus de place
   visible pour le dernier clavier même après avoir scrollé au maximum (bug constaté : le 3e clavier
   restait coupé, la barre semblait "cacher" le clavier alors qu'il n'y avait techniquement aucun
   chevauchement de pixels — juste plus assez d'espace visible pour les deux en même temps). Flux normal :
   le bloc apparaît simplement après le dernier clavier, on scrolle à travers comme n'importe quel contenu. */
/* Mobile (<1300px) : c'est le document/.pages-container qui scrolle, pas .chords-main (aucune règle mobile
   ne lui donne une hauteur contrainte + overflow:auto) — un position:sticky sur #chStackBuilder n'a donc
   aucun ancêtre scrollable pertinent et reste inerte : au fil du scroll de page, il défile comme n'importe
   quel autre bloc et sort de l'écran dès que le nombre de diagrammes (donc la hauteur de page) change.
   position:fixed ancré au viewport règle ça (même stratégie que #page-rhythm.page.active plus haut dans ce
   fichier) : le bloc reste à l'écran quel que soit le nombre de diagrammes au-dessus. .chords-main reçoit un
   padding-bottom compensatoire pour que le dernier diagramme ne soit jamais masqué derrière. */
@media(max-width:1023px){
  #chStackBuilder{position:fixed;left:8px;right:8px;bottom:8px;z-index:50;box-shadow:0 4px 16px var(--sh);max-height:70vh;overflow-y:auto}
  /* Avec une image de fond de module, la règle générique de verre dépoli gagne sur le background opaque
     défini plus haut pour #chStackBuilder. C'est souhaitable en desktop, où l'empileur est une carte dans le
     flux, mais pas ici : en position:fixed il survole les diagrammes et leur contenu restait visible à travers
     la poignée et le panneau déplié. Conserver un fond plein sur ce seul overlay mobile et retirer son blur ;
     les sous-blocs semi-transparents continuent alors de se composer sur var(--sf), jamais sur les diagrammes. */
  .page.module-bg-enabled #chStackBuilder{background:var(--sf);-webkit-backdrop-filter:none;backdrop-filter:none}
}
.ch-stack-handle{display:flex;align-items:center;justify-content:space-between;gap:8px;cursor:pointer;-webkit-tap-highlight-color:transparent;padding:8px 12px}
.ch-stack-chevron{font-family:var(--fm);font-size:.7rem;color:var(--tx3);transition:transform var(--tr);flex-shrink:0}
#chStackBuilder.expanded .ch-stack-chevron{transform:rotate(180deg)}
.ch-stack-body{overflow:hidden;max-height:0;transition:max-height var(--tr);padding:0 12px}
/* max-height:2000px (pas 400px) : un plafond fixe trop bas tronquait la dernière ligne ("fondamentale", en
   bas de la colonne column-reverse) dès que le contenu réel (titre + 4 étages + fondamentale) dépassait
   400px de haut — bug rapporté. 2000px est un plafond très généreux, jamais atteint par ce contenu, qui ne
   sert plus qu'à permettre l'animation d'ouverture (transition max-height) ; #chStackBuilder lui-même a son
   propre plafond réaliste (max-height:calc(100vh-200px);overflow-y:auto, cf. règle desktop dédiée) qui
   prend le relais comme véritable limite visuelle avec scroll interne si le contenu est vraiment trop haut
   pour l'écran — cette valeur ici n'a plus vocation à contraindre quoi que ce soit en pratique. */
#chStackBuilder.expanded .ch-stack-body{max-height:2000px;padding:0 12px 12px}
.ch-stack-col{display:flex;flex-direction:column-reverse;gap:3px;max-width:460px;padding-top:8px}
/* Tour d'intervalles empilés : chaque étage = une "touche" colorée (la note réelle obtenue, cf.
   NOTE_COLORS) à gauche + sa légende (nom de l'étage, sélecteur de crans) à droite — reprend la métaphore
   demandée d'un empilement fondamentale-en-bas/extension-en-haut avec la légende sur le côté, plutôt que
   l'ancienne rangée uniforme texte+pilules+note. Toutes les lignes (fondamentale incluse) partagent la
   même hauteur — min-height fixe plutôt qu'un padding qui varie avec le nombre de lignes de
   .ch-stack-tier-lbl (ex. "Sixte / Septième" sur 2 lignes), pour que la tour ait un contour régulier. */
.ch-stack-tower-row{display:flex;align-items:stretch;gap:0;min-height:38px;box-sizing:border-box}
.ch-stack-key{width:48px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-family:var(--fd);font-weight:800;font-size:.8rem;border:1px solid;border-radius:var(--r) 0 0 var(--r);box-sizing:border-box}
.ch-stack-key-empty{background:var(--s2);border-color:var(--bd);color:var(--tx3);font-weight:400;font-size:.72rem}
.ch-stack-legend{flex:1;display:flex;align-items:center;gap:8px;padding:4px 8px;border-radius:0 var(--r) var(--r) 0;background:var(--s2);border:1px solid var(--bd);border-left:none;min-width:0}
.ch-stack-tier-lbl{font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3);text-transform:uppercase;letter-spacing:.04em;width:88px;flex-shrink:0;line-height:1.25}
.ch-stack-rail{display:flex;gap:2px;flex:1;flex-wrap:wrap}
.ch-stack-cran{padding:3px 7px;border-radius:999px;border:1px solid var(--bd);background:transparent;color:var(--tx2);font-family:var(--fb);font-size:.6rem;cursor:pointer;transition:background-color var(--tr),border-color var(--tr),color var(--tr);white-space:nowrap}
.ch-stack-cran:hover:not(.disabled):not(.active){background:var(--s3);color:var(--tx)}
/* Repli neutre (gris) pour les crans "Aucune" qui n'ont pas de couleur d'intervalle propre (IV_COL) — les
   crans avec une hauteur/qualité réelle reçoivent leur couleur d'intervalle en style inline (cf.
   chStackRenderSlider), qui prévaut sur ce repli. */
.ch-stack-cran.active{background:var(--tx2);border-color:var(--tx2);color:var(--bg);font-weight:700}
.ch-stack-cran.disabled{opacity:.3;cursor:not-allowed}
.ch-stack-result{font-family:var(--fd);font-size:1.05rem;font-weight:800;display:flex;align-items:baseline;gap:8px}
.ch-stack-result-qual{font-family:var(--fm);font-size:.62rem;font-weight:400;color:var(--tx3)}
/* Diatonic panel */
.diat-title{font-family:var(--fm);font-size:max(10px,.55rem);font-weight:700;color:var(--tx3);text-transform:uppercase;letter-spacing:.06em;padding:4px 2px 2px}
.diat-row{display:flex;align-items:center;gap:6px;padding:5px 8px;border-radius:var(--r);border:1px solid var(--bd);background:var(--sf);cursor:pointer;transition:all var(--tr);-webkit-tap-highlight-color:transparent}
.diat-row:hover{border-color:var(--bd2);background:var(--s2)}
/* width fixe (pas seulement min-width) : "vii°" est plus large que les autres degrés (I, ii, V, vi...) et
   débordait de l'ancien min-width:28px, décalant tout ce qui suit dans le flux flex (bouton play, nom...) sur
   cette seule ligne — width fixe + text-align:center garantit un alignement identique sur toutes les lignes. */
/* width:40px (au lieu de 32px) : les degrés altérés (bIII, bVII...) débordaient de la boîte de 32px sur
   desktop (racine 26px, .62rem ≈ 16px de police réelle contre ~11px en mobile) — le débordement de
   contenu n'est pas contenu par le gap:6px du parent flex (.diat-row), donc le texte venait toucher
   visuellement le bouton ♪ malgré cet espacement déclaré (bug rapporté). */
.diat-roman{font-family:var(--fm);font-size:.62rem;font-weight:700;width:40px;flex-shrink:0;text-align:center;color:var(--tx3)}
.diat-name{font-family:var(--fd);font-size:.88rem;font-weight:800;flex:1}
.diat-quality{font-family:var(--fm);font-size:max(10px,.52rem);color:var(--tx3);min-width:32px;text-align:right}
.diat-fn{font-family:var(--fm);font-size:max(10px,.48rem);padding:1px 5px;border-radius:3px;border:1px solid}
.diat-fn.T{border-color:#3d9e7888;color:#3d9e78}
.diat-fn.SD{border-color:#e0a03088;color:#e0a030}
.diat-fn.D{border-color:#e0505088;color:#e05050}
/* Alias de .vcard-play (module Accords, cf. index.html #genActions/vcards) : même bouton rond ♪ partout
   dans l'appli (diagrammes d'accords, accords diatoniques du module Gammes...), une seule définition de
   style pour les deux — auparavant dupliquée à l'identique ici, ce qui obligeait à répercuter tout
   ajustement de lisibilité aux deux endroits séparément (risque d'oubli). .diat-play n'ajoute donc plus
   qu'un correctif : .vcard-play est positionné en absolute (pensé pour flotter dans le coin d'une .vcard),
   alors que le bouton diatonique vit dans le flux normal d'une rangée (row.appendChild, pas de .vcard
   parent) — sans ce override, il hérite du position:absolute et disparaît du flux (bug constaté : bouton
   introuvable après la fusion des deux classes).
   Sélecteur .vcard-play.diat-play (spécificité 0-2-0, pas 0-1-0) : nécessaire pour battre .vcard-play seul
   (styles.css ~ligne 1199, spécificité égale mais déclarée APRÈS ce bloc-ci — au cascade tie, la règle la
   plus tardive gagne, donc un simple .diat-play{position:static} ici était silencieusement écrasé). Voir
   aussi la media query desktop plus haut (.vcard-play:not(...){top:8px!important;right:10px!important})
   qui matche aussi .diat-play via sa classe vcard-play partagée — exclue explicitement là-bas. */
.vcard-play.diat-play{position:static}
/* Glyphe ⊞ (bouton "Voir dans Accords" des accords diatoniques, cf. app.js buildDiatonicPanel) — HISTORIQUE :
   plusieurs correctifs successifs de transform:translate en em sur un <span> texte (décalage signalé "trop
   bas", puis "haut-droite" à 100% zoom, puis "haut-gauche" après correction, donc un décalage qui inversait
   de sens selon le zoom navigateur). Aucune valeur em/px fixe ne peut compenser ça de façon stable : c'est
   un problème de rendu de police (hinting/subpixel dépendant plateforme+zoom), pas un simple offset constant
   — chaque correctif ne faisait que déplacer le problème plutôt que le résoudre. Remplacé par un <svg>
   inline (cf. app.js) : géométrie exacte via viewBox, centré par le flex du bouton (.vcard-play), aucune
   dépendance au rendu de police. Plus aucune règle de centrage nécessaire ici — .vcard-play{display:flex;
   align-items:center;justify-content:center} suffit, comme pour tous les autres glyphes texte de l'app.
/* Scale suggestions in analyser */
.az-scale-title{font-family:var(--fm);font-size:max(10px,.55rem);font-weight:700;color:var(--tx3);text-transform:uppercase;letter-spacing:.06em;padding:10px 2px 4px}
/* .drone-col (cf. index.html #droneToneCol/#droneFocusCol) est un enfant direct de .drone-wrap, lui-même
   flex-column + align-items:center (cf. index.html) — sur mobile, sans width explicite, align-items:center
   rétrécit .drone-col à sa largeur de contenu au lieu de le laisser occuper toute la largeur disponible
   comme un <div> bloc normal l'aurait fait, cassant l'alignement avec les cards du drone tonal juste
   au-dessus (bug rapporté : la section "Bruit ambiant" plus étroite que le reste sur mobile). width:100%
   ici (base, hors media query desktop) corrige ça ; la règle desktop (flex:1 1 0, plus bas) prend le relais
   au-delà de 1150px. */
.drone-col{width:100%}
/* Cartes de gammes compatibles alignées sur le rendu du module Harmonie/Notes (.srow-h/.srow-badge/.npill,
   cf. hnRenderResults dans app.js) plutôt qu'un format propre à l'Analyseur — demande utilisateur, cette
   présentation montre mieux les notes présentes/absentes de la gamme et leur degré. Voir azSuggestScales()
   pour le détail du rendu ; .az-scale-title/.az-scale-rel restent propres à l'Analyseur (pas d'équivalent
   direct côté Harmonie/Notes). */
/* Gammes "relatives" (même ensemble exact de pitch-classes qu'une autre carte de ce classement, racine
   différente — ex. Do majeure / La min. naturelle) : lien vers la 1ère occurrence, cf. azSuggestScales()
   dans app.js — survolable/cliquable (souligné au survol) pour surligner la carte référencée ailleurs dans
   la liste (.az-scale-rel-target), pas juste un texte informatif isolé. */
.az-scale-rel{font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3);font-style:italic;padding-left:2px;cursor:pointer;text-decoration:underline;text-decoration-color:transparent;transition:text-decoration-color var(--tr),color var(--tr)}
.az-scale-rel:hover{color:var(--tx2);text-decoration-color:var(--tx3)}
/* Surbrillance de la carte référencée par un lien "relative de" survolé/cliqué ailleurs dans la liste —
   transition rapide pour un effet net perceptible sans être criard, cohérent avec .srow-h.top (bordure
   couleur d'accent) déjà utilisée pour distinguer le meilleur résultat. */
.srow-h.az-scale-rel-target{border-color:var(--c-root);background:var(--sf);box-shadow:0 0 0 1px var(--c-root)}
/* Mode toggle actif */
.tbtn:active{transform:scale(.88);transition:transform 60ms}
.rbtn:active{transform:scale(.88);transition:transform 60ms}
.tab:active{transform:scale(.93);transition:transform 60ms}
.hn-mpill:active,.hn-add-btn:active,.str-add-btn:active{transform:scale(.9);transition:transform 60ms}
.rrow button:active{transform:scale(.86);transition:transform 60ms}

/* CIRCLE */
.cof-page{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;padding:2px;overflow:auto;gap:4px;width:100%}
/* Rotation/Son déplacés de la subbar (qui devenait chargée) vers des boutons flottants ancrés aux coins
   supérieurs du canvas — cf. #cofRotBtn/#cofSndBtn dans index.html. .cof-canvas-wrap sert d'ancre
   position:relative pour ces boutons position:absolute. */
/* width:100% (pas inline-flex shrink-to-fit) en mobile : drawCoF() lit canvas.parentElement.clientWidth
   comme largeur disponible (cf. app.js) — un wrapper qui rétrécit à son contenu créerait une référence
   circulaire (la taille du canvas dépendrait de la taille du canvas). Le layout desktop (>=1300px)
   redéfinit explicitement une largeur (50%) plus bas, donc ce 100% ne s'applique qu'en mobile. */
.cof-canvas-wrap{position:relative;display:block;width:100%;max-width:100%}
.cof-float-btn{position:absolute;top:6px;width:60px;height:60px;border-radius:50%;border:1px solid var(--bd);background:var(--pbg);color:var(--tx2);font-size:1.6rem;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all var(--tr);z-index:5;-webkit-tap-highlight-color:transparent}
.cof-float-btn:hover{border-color:var(--bd2);color:var(--tx);background:var(--phov)}
.cof-float-btn.active{background:var(--c-root);border-color:var(--c-root);color:#111}
.cof-float-tl{left:6px}
.cof-float-tr{right:6px}
/* .cof-float-bl : même style que .cof-float-tl/tr (top:6px hérité de .cof-float-btn), positionné en bas à
   gauche à la place — bouton verrou, demandé explicitement à cet emplacement plutôt qu'un 3e bouton en
   haut, pour ne pas surcharger visuellement le haut du cercle. */
.cof-float-bl{left:6px;top:auto;bottom:6px}
/* 4e coin (bas-droite), seul encore libre — superposition de la gamme active (cf. S.cofScaleOverlay). */
.cof-float-br{right:6px;top:auto;bottom:6px}
/* Petite portée d'armure dans la chip d'info (cf. _cofKeySigSvg dans app.js) — alignée sur le texte qui
   la précède, sans casser la hauteur de ligne de la chip. */
.cof-ksig{vertical-align:middle;margin-left:6px}
/* Icônes en SVG inline (pas des glyphes Unicode/police) : rendu identique sur toutes les plateformes — un
   glyphe texte (⟳/♪/⊘) dépend de la police de secours du système pour ces caractères, qui diffère entre
   desktop et mobile et rendait le centrage visuellement incohérent d'une plateforme à l'autre. */
.cof-chip{padding:4px 11px;border-radius:999px;border:1px solid var(--bd);font-family:var(--fm);font-size:.65rem;background:var(--s2);color:var(--tx2)}
.cof-chip strong{color:var(--tx)}
.cof-chip.hi{border-color:var(--c-root);color:var(--c-root)}
.cof-deg-table{display:flex;flex-direction:column;gap:5px;padding:6px 4px 4px;width:100%;max-width:480px;margin:0 auto}
.cof-deg-row{display:flex;gap:5px;align-items:center;justify-content:center;flex-wrap:nowrap}
.cof-deg-label{font-family:var(--fm);font-size:max(10px,.55rem);font-weight:700;color:var(--tx2);width:38px;text-align:right;flex-shrink:0;padding-right:2px;white-space:nowrap}
.cof-deg-cell{display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:50%;width:42px;height:42px;flex-shrink:0;box-shadow:0 2px 6px rgba(0,0,0,.35)}
.cof-deg-roman{font-family:var(--fm);font-size:max(10px,.45rem);font-weight:700;opacity:.80;line-height:1.3}
.cof-deg-note{font-family:var(--fd);font-size:max(10px,.58rem);font-weight:800;line-height:1.2}

/* Manche/clavier verrouillés en haut, résultats seuls scrollables en dessous (demandé) : .az-wrap lui-même
   ne scrolle plus (overflow:hidden) — #azGuitarSection/#azPianoSection restent à taille naturelle
   (flex-shrink:0, cf. plus bas), seule .az-rescard reçoit flex:1+overflow-y:auto et devient l'unique
   région de scroll. Avant ce correctif, .az-wrap entier scrollait d'un bloc : le manche défilait hors
   champ avec les résultats, obligeant à remonter pour le retrouver. */
.az-wrap{display:flex;flex-direction:column;gap:10px;padding:8px;flex:1;overflow:hidden;min-height:0}
.az-analysis-modes{flex-shrink:0;gap:12px}
#page-analyzer.az-rhythmic>#azHeader,#page-analyzer.az-rhythmic>.az-wrap{display:none}
#azRhythmPanel[hidden]{display:none}
#azRhythmPanel{display:flex;flex-direction:column;gap:12px;overflow:hidden;min-height:0;padding:16px;flex:1}
.az-rt-panel h2{font-family:var(--fd);font-size:1.15rem;margin:0}
.az-rt-panel p{margin:0;color:var(--tx2);font-size:.85rem;line-height:1.5;max-width:950px}
.az-rt-pads{display:grid;grid-template-columns:1fr 1fr;gap:10px;flex-shrink:0}
.az-rt-pad{min-height:115px;padding:16px 10px;border:2px solid var(--bd2);border-radius:var(--r);background:var(--s2);color:var(--tx);font:700 1rem var(--fb);cursor:pointer;touch-action:manipulation;user-select:none;-webkit-user-select:none}
.az-rt-pad small{display:block;margin-top:8px;font-size:.72rem;font-weight:400;color:var(--tx2)}
.az-rt-pad[hidden],.az-rt-reading[hidden],#azRhythmFeedback[hidden],#azRhythmFile[hidden]{display:none}
.az-rt-pad.az-rt-weak{grid-column:1/-1;min-height:44px;padding:8px}
.az-rt-weak small{display:inline;margin-left:8px}
.az-rt-pad:disabled{opacity:.55;cursor:default}
.az-rt-pad.az-rt-strong{border-color:var(--c-root)}
.az-rt-pad:active{background:var(--s3);transform:scale(.98)}
.az-rt-pad:focus-visible,.az-rt-card:focus-visible{outline:2px solid var(--c-root);outline-offset:3px}
.az-rt-toolbar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;flex-shrink:0;font-size:.78rem;color:var(--tx2)}
.az-rt-timeline{display:block;width:100%;height:76px;flex-shrink:0;background:var(--s2);border:1px solid var(--bd);border-radius:var(--r)}
.az-rt-grid{stroke:var(--bd);stroke-width:1}
.az-rt-bar{stroke:var(--tx3);stroke-width:2}
.az-rt-tap{stroke:var(--tx);stroke-width:4}
.az-rt-accent-3{stroke:var(--c-root);stroke-width:6}
.az-rt-accent-1{stroke:var(--tx3);stroke-width:3}
.az-rt-results{display:grid;gap:8px;flex-shrink:0}
.az-rt-card{display:flex;flex-direction:column;gap:6px;text-align:left;border:1px solid var(--bd);border-radius:var(--r);background:var(--sf);color:var(--tx);padding:12px;font:inherit;cursor:pointer}
.az-rt-card.active{border-color:var(--c-root);box-shadow:inset 3px 0 var(--c-root)}
.az-rt-card strong{font-size:.95rem}
.az-rt-card span{font-size:.78rem;line-height:1.4;color:var(--tx2)}
.az-rt-panel .az-rt-note{font-size:.75rem}
.az-rt-intro-short{display:none}
.az-rt-transport{padding:8px;border:1px solid var(--bd);border-radius:var(--r);background:var(--sf)}
#azRhythmTransport #azRhythmMetro{border-color:var(--c-root);font-weight:700}
#azRhythmPlaybackSummary{color:var(--tx);font-family:var(--fm)}
#azRhythmBeat:empty{display:none}
.az-rt-tap-options summary{font-size:.72rem;color:var(--tx2);cursor:pointer;padding:5px 0}
.az-rt-tap-options .az-rt-weak-toggle{padding:6px 0}
.az-rt-heading{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}
.az-rt-mode{font:500 .72rem var(--fm);padding:4px 8px;border:1px solid var(--bd);border-radius:999px;color:var(--tx2)}
.az-rt-heading,.az-rt-panel>p{flex-shrink:0}
.az-rt-input{display:grid;gap:10px;flex-shrink:0}
.az-rt-output{display:flex;flex-direction:column;gap:10px;flex:1;min-height:0;overflow:auto;padding:3px}
.az-rt-pad.is-hit{background:var(--s3);border-color:var(--c-root);box-shadow:inset 0 0 0 1px var(--c-root)}
.az-rt-toolbar [hidden],.az-rt-details[hidden]{display:none}
.az-rt-toolbar .tbtn{width:auto;height:auto;min-height:36px;padding:7px 12px;font-size:.75rem;touch-action:manipulation;white-space:normal;text-align:center}
.az-rt-toolbar .tbtn:disabled{opacity:.45;cursor:default}
.az-rt-card .az-rt-tempo{font:700 1.15rem var(--fm);color:var(--tx)}
.az-rt-pattern{display:flex;flex-wrap:wrap;gap:8px;flex-shrink:0}
.az-rt-group{display:flex;gap:3px;padding:5px;border:1px solid var(--bd);border-radius:var(--r);background:var(--s2)}
.az-rt-step{display:flex;align-items:center;justify-content:center;min-width:22px;height:28px;font:600 .8rem var(--fm);color:var(--tx3);border-radius:4px}
.az-rt-step.strong{color:var(--c-root);font-weight:800}
.az-rt-step.secondary{color:var(--tx)}
.az-rt-step.is-playing{background:var(--c-root);color:var(--bg);outline:1px solid var(--tx2)}
.az-rt-outlier{stroke-dasharray:3 4;opacity:.6}
.az-rt-details{flex-shrink:0;border:1px solid var(--bd);border-radius:var(--r);padding:10px 12px}
.az-rt-details summary{font-size:.8rem;cursor:pointer;color:var(--tx2);padding:3px 0}
.az-rt-details p{font-size:.78rem;margin-top:8px}
.az-rt-reading{display:flex;gap:8px;align-items:center;flex-wrap:wrap;flex-shrink:0;color:var(--tx2);font-size:.78rem}
.az-rt-reading select{max-width:100%;min-width:0;height:auto;padding:6px;font-size:.75rem}
.az-rt-weak-toggle{display:flex;align-items:center;gap:5px;font-size:.72rem;cursor:pointer}
.az-rt-label{display:block;color:var(--tx2);font-size:.78rem;margin:10px 0 5px}
#azRhythmLabel{width:100%;box-sizing:border-box;background:var(--s2);color:var(--tx);border:1px solid var(--bd);border-radius:var(--r);padding:8px;font:inherit;font-size:.8rem}
#azRhythmNotice:empty{display:none}
#azRhythmSession .az-rt-toolbar{margin-top:10px}
@media(min-width:900px){.az-rt-results{grid-template-columns:repeat(3,minmax(0,1fr))}.az-rt-card{justify-content:flex-start;min-width:0}}
@media(max-width:1023px){#page-analyzer.active.az-rhythmic{position:fixed;left:0;right:0;bottom:0;top:var(--rh-top,96px);height:auto;z-index:5;overflow:hidden}}
@media(max-height:550px){#azRhythmPanel{overflow:auto}.az-rt-output{flex:none;overflow:visible}}
@media(max-width:600px){
  #azRhythmPanel{padding:10px;gap:10px}
  .az-rt-pads{grid-template-columns:1fr 1fr}
  .az-rt-pad{min-height:100px;font-size:.9rem}
  .az-rt-weak{grid-column:1/-1;min-height:56px}
  .az-rt-weak small{display:inline;margin-left:8px}
  .az-rt-pad{min-height:82px;padding:10px 6px}
  .az-rt-weak{min-height:44px}
  .az-rt-timeline{height:48px}
  .az-rt-panel>p{font-size:.75rem}
  .az-rt-heading{gap:5px}
  .az-rt-input{gap:6px}
  .az-rt-panel h2{font-size:.95rem}
  .az-rt-mode{font-size:.62rem;padding:3px 6px}
  .az-rt-intro-full{display:none}
  .az-rt-intro-short{display:inline}
  .az-rt-panel>.az-rt-intro{font-size:.68rem}
  .az-rt-take-tools{display:grid;grid-template-columns:auto auto minmax(0,1fr);gap:6px}
  .az-rt-take-tools .tbtn{font-size:.68rem;padding:6px 8px;min-height:36px}
  #azRhythmCount{font-size:.66rem;text-align:right}
  .az-rt-transport{display:grid;grid-template-columns:1fr 1fr;gap:5px;padding:6px}
  .az-rt-transport .tbtn{width:100%;min-height:44px;padding:6px;font-size:.72rem}
  #azRhythmPlaybackSummary,#azRhythmBeat{grid-column:1/-1;font-size:.68rem;line-height:1.3}
}
@media(max-width:600px) and (max-height:740px){
  #azRhythmPanel{padding:8px;gap:7px}
  .az-rt-panel>p{display:none}
  .az-rt-panel h2{font-size:.95rem}
  .az-rt-pad{min-height:64px;padding:7px 5px}
  .az-rt-pad small{margin-top:4px;font-size:.64rem}
  .az-rt-timeline{height:32px}
  .az-rt-toolbar:not(.az-rt-transport) .tbtn{min-height:32px;font-size:.68rem;padding:5px 8px}
}
@media(max-width:600px) and (max-height:650px){
  .az-rt-pad:not(.az-rt-weak){min-height:60px}
  .az-rt-pad:not(.az-rt-weak) small{display:none}
}
@media(max-width:360px){
  .az-rt-heading{flex-wrap:nowrap}
  .az-rt-panel h2{font-size:.85rem}
  .az-rt-mode{font-size:.56rem;padding:3px 4px;white-space:nowrap}
}
#azGuitarSection,#azPianoSection{flex-shrink:0}
/* Poignée replier/déplier manche/clavier (cf. index.html #azInputCollapseHandle) — même pattern que
   .ch-stack-handle/.ch-stack-chevron (module Accords, empileur d'intervalles) : chevron qui pivote à 180°
   en état replié, bande fine cliquable sur toute la largeur. Repliée (.az-wrap.input-collapsed), le manche/
   clavier passe à hauteur nulle avec overflow masqué plutôt que display:none — permet une transition
   animée (max-height) au lieu d'un basculement instantané, cohérent avec le reste des replis de l'app.
   .az-rescard hérite alors de tout l'espace vertical libéré (flex:1 déjà en place, cf. règle plus bas). */
.az-input-collapse-handle{display:flex;align-items:center;justify-content:center;height:14px;flex-shrink:0;cursor:pointer;-webkit-tap-highlight-color:transparent;margin:-4px 0}
.az-input-collapse-chevron{font-family:var(--fm);font-size:.6rem;color:var(--tx3);transition:transform var(--tr)}
.az-wrap.input-collapsed .az-input-collapse-chevron{transform:rotate(180deg)}
/* max-height:2000px (pas none) en base : une transition CSS sur max-height exige une valeur de départ
   définie pour s'animer (auto→0 ne s'anime jamais) — même technique déjà utilisée pour .ch-stack-body
   (module Accords, empileur d'intervalles). 2000px est un plafond très généreux, jamais atteint par le
   manche/clavier réels, qui ne sert qu'à permettre l'animation. */
#azGuitarSection,#azPianoSection{max-height:2000px;overflow:hidden;transition:max-height var(--tr),padding var(--tr),margin var(--tr)}
/* padding:8px 0 4px déplacé ici depuis un style inline HTML (index.html) : un style inline a une priorité
   plus forte que n'importe quelle règle CSS de classe/id sans !important — la règle repliée juste en
   dessous (padding:0) ne pouvait donc jamais l'emporter, laissant ce padding vertical (12px au total)
   visible même à max-height:0 (bug rapporté : "il reste quelques pixels de clavier visibles"). */
.az-piano-section{padding:8px 0 4px}
.az-wrap.input-collapsed #azGuitarSection,.az-wrap.input-collapsed #azPianoSection{max-height:0;padding:0;border-width:0;margin:0}
.az-fbcard{background:var(--sf);border:1px solid var(--bd);border-radius:var(--rl);padding:12px;overflow-x:auto}
.az-fbtools{display:flex;align-items:center;gap:4px;margin-bottom:8px}
.az-fretboard{position:relative}
.az-string-row{display:flex;align-items:center;height:44px;position:relative}
.az-s-label{flex-shrink:0;text-align:right;padding-right:6px;font-family:var(--fm);font-size:.7rem;color:var(--c-root);font-weight:700;width:26px}
.az-nut{flex-shrink:0;width:7px;align-self:stretch;background:#b0b0b0;border-radius:2px 0 0 2px;margin:6px 0;z-index:2}
.az-open-col{flex-shrink:0;display:flex;align-items:center;justify-content:center;z-index:3}
.az-open-dot{width:26px;height:26px;border-radius:50%;border:1.5px solid var(--bd2);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all var(--tr);background:var(--sf);font-family:var(--fm);font-size:.62rem;user-select:none;-webkit-tap-highlight-color:transparent;outline:none}
.az-open-dot.st-open{border-color:var(--c-root)}
.az-open-dot.st-mute{border-color:#e07070;color:#e07070}
.az-s-line{position:absolute;top:50%;transform:translateY(-50%);height:1.5px;background:#aaa;pointer-events:none;z-index:0}
.az-fret-cells{display:flex;flex:1;height:100%;z-index:1}
div:focus,span:focus,button:focus,a:focus,input:focus,select:focus,textarea:focus,canvas:focus{outline:none !important}
*{-webkit-tap-highlight-color:transparent !important}
*:focus-visible{outline:none !important}
button,input,select,textarea,a,[onclick]{-webkit-tap-highlight-color:transparent;outline:none}
button:active,input:active,select:active,textarea:active{outline:none}
.az-fret-cell{flex:1;height:100%;border-right:2px solid var(--bd);display:flex;align-items:center;justify-content:center;cursor:pointer;-webkit-tap-highlight-color:transparent;outline:none}
#strCanvas{-webkit-tap-highlight-color:transparent;}
#page-drone button, #page-drone .pill{-webkit-tap-highlight-color:transparent}
#page-analyzer *{-webkit-tap-highlight-color:transparent}
.az-note-dot{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:opacity .12s,transform .12s;z-index:3;opacity:0;transform:scale(.35);pointer-events:none}
.az-note-dot.on{opacity:1;transform:scale(1);pointer-events:auto;cursor:pointer}
.az-note-dot span{font-family:var(--fm);font-size:max(10px,.56rem);color:#111;font-weight:700;pointer-events:none}
/* Fondamentale présumée en mode Gammes (1ère note posée depuis le dernier "Effacer", cf. azScaleTonicPc) —
   anneau distinctif superposé au remplissage coloré normal de la note. Blanc plutôt que la couleur d'accent (var(--c-root)) : l'accent est déjà utilisé ailleurs sur la page
   (badge de mode, bordures actives...), un anneau blanc se détache plus nettement sur les pastilles de
   note elles-mêmes (dont le fond est déjà une des couleurs NOTE_COLORS, potentiellement proche de l'accent
   pour certaines notes) — signalé pour améliorer la lisibilité de la fondamentale.  */
.az-note-dot.az-tonic,.az-open-dot.az-tonic{box-shadow:0 0 0 3px #fff,0 0 0 4.5px var(--sf)}
/* Prévisualisation d'une gamme compatible (survol/tap sur une carte de résultat, cf. azSetPreviewScale) —
   anneau creux en pointillés plutôt qu'un remplissage plein : doit se lire immédiatement comme "gamme
   suggérée, pas encore posée par vous" et jamais se confondre avec une vraie note cliquée par
   l'utilisateur (remplissage plein + halo, cf. règles .on ci-dessus). Couleur = celle de la note
   (NOTE_COLORS, posée en inline via style.borderColor), seul le traitement creux/pointillé change. */
.az-note-dot.preview{opacity:1;transform:scale(.68);background:transparent;border:1.5px dashed var(--bd2);pointer-events:none}
.az-open-dot.preview{border:1.5px dashed var(--bd2);background:transparent;color:transparent}
.az-fret-header,.az-fret-foot{display:flex;align-items:center;margin:2px 0}
.az-fret-num{flex:1;text-align:center;font-family:var(--fm);font-size:max(10px,.48rem);color:var(--tx3)}
.az-marker-cell{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;height:7px}
.az-marker-dot{width:4px;height:4px;border-radius:50%;background:var(--bd2)}
.az-marker-dot-dbl{width:4px;height:4px}
.az-rescard{background:var(--sf);border:1px solid var(--bd);border-radius:var(--rl);padding:10px;flex:1;min-height:0;overflow-y:auto}
.az-results-top{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin-bottom:.7rem}
.az-res-lbl{font-family:var(--fm);font-size:max(10px,.5rem);text-transform:uppercase;letter-spacing:.1em;color:var(--tx3)}
.az-notes-row{display:flex;gap:.25rem;flex-wrap:wrap}
/* Cliquable : réassigne la fondamentale présumée (azScaleTonicPc) sur cette pitch-class — la détection
   automatique (1ère note posée) n'est qu'un point de départ, jamais un verrou (demande utilisateur).
   border transparent au repos (pas "none") pour que l'anneau .tonic n'ajoute aucun saut de taille au clic.
   Blanc et plus épais (3px) plutôt que la couleur d'accent : même logique que l'anneau .az-tonic du manche
   (cf. plus haut) — un contour blanc se détache plus nettement du fond coloré (NOTE_COLORS) de la pastille
   que l'accent, potentiellement proche de certaines couleurs de notes (lisibilité signalée par l'utilisateur). */
.az-note-pill{border-radius:999px;padding:2px 8px;font-family:var(--fm);font-size:.62rem;font-weight:700;cursor:pointer;border:2px solid transparent;box-sizing:border-box;transition:border-color var(--tr)}
.az-note-pill:hover{border-color:var(--bd2)}
.az-note-pill.tonic{border:3px solid #fff}
.az-mode-badge{flex-shrink:0;padding:3px 9px;border-radius:999px;border:1px solid var(--bd);background:var(--s2);color:var(--tx2);font-family:var(--fm);font-size:max(10px,.56rem);font-weight:600;white-space:nowrap;cursor:pointer;-webkit-tap-highlight-color:transparent;transition:all var(--tr)}
.az-mode-badge:hover{border-color:var(--bd2);color:var(--tx)}
.az-mode-badge.scale{border-color:var(--c-root);color:var(--c-root)}
/* Mode forcé (pas "Auto") : fond plein plutôt qu'un simple contour, pour que "figé" se distingue nettement
   de "déduit" au premier coup d'œil, pas seulement via le texte "Auto ·" qu'on pourrait ne pas lire. */
.az-mode-badge.forced{background:var(--c-root);color:#111;border-color:var(--c-root)}
.az-mode-badge.forced.scale{background:var(--c-root);color:#111}
/* Accords/Gammes empilés par défaut (mobile, mais aussi desktop en dessous de DESKTOP_BREAKPOINT) — chaque
   colonne garde sa largeur naturelle pleine largeur, comme avant l'ajout de ce conteneur. */
.az-res-cols{display:flex;flex-direction:column;gap:.4rem}
.az-col-title{font-family:var(--fm);font-size:max(10px,.55rem);font-weight:700;color:var(--tx3);text-transform:uppercase;letter-spacing:.06em;padding:2px 2px 4px}
@media(min-width:1024px){
  /* Côte à côte sur desktop : accords à gauche, gammes à droite (demandé) — 1fr/1fr plutôt qu'une largeur
     fixe, les deux colonnes se partagent l'espace disponible également quel que soit le nombre de cartes. */
  .az-res-cols{flex-direction:row;align-items:flex-start;gap:14px}
  .az-res-col{flex:1;min-width:0}
}
/* auto-fit (pas auto-fill) : avec auto-fill, la grille réserve autant de colonnes de 140px que la largeur
   totale le permet même quand il n'y a qu'1-2 cartes à afficher, plafonnant leur largeur réelle à ~140-184px
   au lieu de les laisser s'étirer — trop étroit pour que le nom de l'accord + Formes/+ Prog./♪ tiennent sur
   une seule ligne (condensation demandée ne s'appliquait donc jamais en pratique, cf. carte agrandie testée
   à 1900px de large qui restait pourtant plafonnée à ~184px). auto-fit laisse les colonnes EXISTANTES
   s'étirer pour remplir l'espace disponible quand il y a moins de cartes que de colonnes possibles. */
.az-chord-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:.5rem;margin-bottom:.7rem}
.az-chord-card{background:var(--s2);border:1px solid var(--bd);border-radius:var(--r);padding:.65rem .75rem;transition:border-color var(--tr)}
.az-chord-card.primary{border-color:var(--c-root);background:var(--sf)}
.az-chord-name{font-family:var(--fd);font-size:1.15rem;font-weight:800;color:var(--tx);line-height:1}
.az-chord-name em{color:var(--c-root);font-style:normal}
.az-chord-card.primary .az-chord-name{font-size:1.45rem}
.az-chord-notes{font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx2);margin:.2rem 0 .1rem}
.az-chord-ivals{font-family:var(--fm);font-size:max(10px,.52rem);color:var(--tx3)}
.az-chord-bass{font-family:var(--fm);font-size:max(10px,.55rem);color:#e07070;margin-top:.15rem}
.az-no-result{font-family:var(--fm);font-size:.68rem;color:var(--tx3);padding:1.2rem;text-align:left;border:1px dashed var(--bd2);border-radius:var(--r);line-height:1.7}
.az-ival-row{display:flex;gap:.3rem;flex-wrap:wrap;align-items:center;margin-top:.25rem}
.az-ival-lbl{font-family:var(--fm);font-size:max(10px,.52rem);color:var(--tx3)}
.az-ival-tag{background:var(--s2);border:1px solid var(--bd);border-radius:4px;padding:.1rem .35rem;font-family:var(--fm);font-size:max(10px,.54rem);color:var(--tx3)}
.az-ival-tag.r{border-color:var(--c-root);color:var(--c-root)}
.az-ival-tag.t3{border-color:#4daacc;color:#4daacc}
.az-ival-tag.p5{border-color:#70b870;color:#70b870}
.az-ival-tag.s7{border-color:#a080cc;color:#a080cc}
/* NAV DRAWER */
.nav-overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:6999;opacity:0;pointer-events:none;transition:opacity .22s ease,visibility 0s linear .22s;visibility:hidden}
.nav-overlay.open{opacity:1;pointer-events:auto;visibility:visible;transition:opacity .22s ease,visibility 0s}
/* visibility:hidden une fois FERMÉS (overlay, tiroir, .hint) — bug "flashs noirs au glissé rapide" sur Surface
   (Mi 11 / OnePlus Pad) : ces calques plein écran à opacité 0 / hors champ restaient peints PAR-DESSUS la
   grille ; dès qu'une case lançait une animation composée (flash, ripple), Chrome devait les promouvoir en
   urgence en calques GPU (raison "Overlap"), ~20 Mo de tuiles à DPR 3 alloués/libérés à chaque frappe. Sur
   un budget GPU mobile, des tuiles de la grille étaient évincées et s'affichaient en noir. Mesuré via
   side_dev/dev/gs_layers.js (LayerTree CDP) + gs_blackflash.js (--force-gpu-mem-available-mb). */
/* overscroll-behavior:contain (demande explicite : "quand on scroll dans la sidebar mobile, ne pas
   scroller les modules, uniquement la sidebar") — sans elle, atteindre le haut/bas du scroll interne de
   ce tiroir laisse le NAVIGATEUR propager le reste du geste au conteneur scrollable suivant (.pages-
   container derrière), scrollant visiblement les modules par-dessous alors que le tiroir est ouvert
   ("scroll chaining", comportement par défaut du navigateur, pas une régression de ce fichier). */
/* max-width élargi de 200px à 216px (demande explicite : "sur mobile, élargir légèrement la sidebar pour
   que le bouton recherche (loupe) ne soit pas masqué par la barre de défilement") — ce tiroir a
   overflow-y:auto (nécessaire, son contenu peut dépasser la hauteur d'écran), et certains navigateurs
   Android (pas tous, scrollbar non superposée/overlay contrairement à desktop) réservent une largeur de
   scrollbar PRISE SUR le contenu plutôt qu'en plus — la loupe posée en flex-shrink:0 dans le header
   (#cmdPaletteDrawerBtn, cf. index.html) se retrouvait alors partiellement sous cette scrollbar. */
.nav-popup{position:fixed;top:0;left:0;bottom:0;width:auto;min-width:140px;max-width:216px;z-index:7000;background:var(--sf);border-right:1px solid var(--bd);display:flex;flex-direction:column;box-shadow:4px 0 24px rgba(0,0,0,.3);transform:translateX(-100%);transition:transform .22s ease,visibility 0s linear .22s;visibility:hidden;overflow-y:auto;overscroll-behavior:contain}
.nav-popup.open{transform:translateX(0);visibility:visible;transition:transform .22s ease,visibility 0s}
.nav-drawer-header{padding:14px 14px 8px;font-family:var(--fd);font-size:.75rem;font-weight:800;color:var(--tx);border-bottom:1px solid var(--bd);flex-shrink:0}
.nav-drawer-header em{color:var(--c-root);font-style:normal}
/* padding vertical réduit une 3e fois (10px→7px→5px→3px, demande explicite : "réduire encore un peu
   l'espacement entre les onglets, mobile et desktop") — .nav-item n'existe que dans .nav-popup, le tiroir
   de nav mobile (le sidebar desktop est un élément .tab séparé, jamais concerné, cf. sa propre réduction
   dans .sidebar-desktop .tab plus haut). */
.nav-item{display:flex;align-items:center;gap:10px;padding:3px 14px;cursor:pointer;-webkit-tap-highlight-color:transparent;transition:background var(--tr);border-left:2px solid transparent}
.nav-item:hover{background:var(--s2)}
.nav-item.active{background:var(--s2);border-left-color:var(--c-root)}
.nav-item.active .nav-item-label{color:var(--c-root);font-weight:700}
.nav-item-icon{font-size:.9rem;width:20px;text-align:center;color:var(--tx2);flex-shrink:0}
.nav-item.active .nav-item-icon{color:var(--c-root)}
.nav-item-icon .ft-ico{width:18px;height:18px;display:block;margin:0 auto}
.nav-item-label{font-family:var(--fb);font-size:.72rem;color:var(--tx2)}
.nav-footer-sep{height:1px;background:var(--bd);margin:6px 14px;flex-shrink:0}
.settings-modal{position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.75);display:flex;align-items:center;justify-content:center;padding:12px;transition:background .15s}
/* BUG CORRIGÉ (rapporté avec screenshot : "le module visible en arrière-plan doit être plus visible (moins
   assombri que par défaut)") — .settings-box-preview (posée sur .settings-box, cf. son historique) ne
   touchait QUE le fond de la boîte elle-même, jamais celui de .settings-modal : cet overlay plein écran
   (rgba(0,0,0,.75), TOUJOURS aussi sombre) reste entre l'utilisateur et le module quel que soit l'état de
   la boîte — même une boîte 100% transparente resterait voilée par ce scrim à 75% opaque par-dessus. Fix :
   #settingsModal reçoit la MÊME classe .settings-box-preview (posée par _modZoomPreviewStart dans app.js,
   qui la bascule désormais sur les 2 éléments) et voit son propre fond chuter à un voile très léger pendant
   l'aperçu, laissant le module transparaître beaucoup plus nettement. */
.settings-modal.settings-box-preview{background:rgba(0,0,0,.15)}
/* .bg-preview-invisible : variante du live-preview posée par _bgPreviewStart (cf. app.js) pour les sliders
   qui affectent l'aspect du fond (opacité/flou/assombrissement/saturation/position/rotation, module ET
   texture d'interface) — demande explicite : "une transparence à 0% pour bien voir l'assombrissement, flou,
   etc.", ces réglages étant justement peu lisibles à travers un panneau encore visible à 20% (le
   comportement par défaut de .settings-box-preview, inchangé pour le zoom par module). N'affecte que les
   éléments concernés par cette classe, jamais le zoom par module qui garde son propre rendu. */
/* opacity:0 sur .settings-box lui-même écraserait aussi la ligne active en cascade (piège de compositing déjà
   documenté plus haut pour .zoom-preview-active — un ancêtre à opacity:X plafonne tous ses descendants à X,
   quoi qu'ils déclarent localement) : le fond devient transparent ET chaque enfant direct passe à opacity:0
   individuellement (comme .settings-header/.settings-tabs/.settings-footer déjà dimés, mais à 0 pas .08 ici),
   sans toucher à l'opacité de .settings-box elle-même — la ligne active garde ainsi sa propre opacité:1 sans
   ancêtre qui la plafonne. */
/* BUG CORRIGÉ (rapporté : "les sliders avec aperçu transparent doivent rester visibles... comme pour les
   zoom modules") — >*{opacity:0} ciblait TOUS les enfants directs de .settings-box, y compris .settings-body
   (piège de compositing déjà documenté ailleurs dans ce fichier : un ancêtre à opacity:0 plafonne tous ses
   descendants à 0, quoi qu'ils déclarent localement) — .settings-row.zoom-preview-active vit à l'intérieur de
   .settings-body>.settings-section>.settings-row, donc plusieurs niveaux sous cet ancêtre à 0, invisible
   malgré sa propre règle opacity:1. Fix : cibler explicitement .settings-header/.settings-tabs/.settings-
   footer (jamais ancêtres de la ligne active) au lieu du générique >*, .settings-body lui-même reste à
   opacity:1 — le dimming à l'intérieur (sections non actives) continue de fonctionner normalement. */
.settings-modal.bg-preview-invisible{background:transparent!important}
.settings-box.bg-preview-invisible{background:transparent!important}
.settings-box.bg-preview-invisible>.settings-header,
.settings-box.bg-preview-invisible>.settings-tabs,
.settings-box.bg-preview-invisible>.settings-footer{opacity:0}
.settings-box.bg-preview-invisible .settings-section:not(.zpa-in){opacity:0}
.settings-box.bg-preview-invisible .settings-section.zpa-direct>*:not(.zoom-preview-active){opacity:0}
.settings-box.bg-preview-invisible .settings-row.zoom-preview-active{opacity:1;pointer-events:auto}
/* height fixe (pas seulement max-height) : la boîte se redimensionnait et se recentrait à chaque
   changement d'onglet Paramètres (chaque section a un nombre de lignes différent), un mouvement brusque et
   déroutant. Une hauteur stable + le scroll interne de .settings-body absorbe la différence de contenu
   entre onglets sans bouger la boîte elle-même. min() avec 90vh pour rester dans l'écran sur petit viewport. */
.settings-box{background:var(--sf);border:1px solid var(--bd);border-radius:var(--rl);width:100%;max-width:480px;height:min(1020px,90vh);display:flex;flex-direction:column;overflow:hidden;transition:opacity .15s}
.settings-modal.surface-active.settings-box-preview{
  background:rgba(0,0,0,.15)!important;
}
.settings-modal.surface-active.settings-box-preview .settings-box{
  background:color-mix(in srgb,var(--sf) 20%,transparent)!important;
}
.settings-modal.surface-active.settings-box-preview .settings-body>.settings-section[data-settings-tab="surface"]{
  opacity:1!important;
}
#settingsModal.settings-box-preview #gsSettingsPanel.gs-settings-preview{
  background:color-mix(in srgb,var(--sf) 20%,transparent)!important;
}
#settingsModal.settings-box-preview #gsSettingsPanel.gs-settings-preview .gs-settings-group:not(.gs-settings-preview-active){
  opacity:.35;
}
#settingsModal.settings-box-preview #gsSettingsPanel.gs-settings-preview .gs-settings-group.gs-settings-preview-active{
  position:relative;background:var(--sf);border-radius:6px;box-shadow:0 0 0 1px var(--bd);opacity:1;
}
#settingsModal.settings-box-preview #gsSettingsPanel.gs-settings-preview .gs-settings-section:not(:has(.gs-settings-preview-active)){
  opacity:.35!important;
}
#settingsModal.settings-box-preview #gsSettingsPanel.gs-settings-preview .gs-settings-section:has(> .gs-settings-embed#gsChordPanel),
#settingsModal.settings-box-preview #gsSettingsPanel.gs-settings-preview .gs-settings-section:has(> .gs-settings-embed#gsParams){
  opacity:.35!important;
}
#settingsModal.settings-box-preview #gsSettingsPanel.gs-settings-preview .gs-settings-preview-dim{
  opacity:.35!important;
}
#settingsModal.settings-box-preview .settings-close{
  opacity:.08!important;
}
#settingsModal.settings-box-preview .settings-footer,
#settingsModal.settings-box-preview .settings-footer button{
  opacity:.08!important;
}
/* Aperçu en direct du zoom par module (demande explicite : "au toucher d'un slider de zoom par module,
   basculer vers le module concerné et rendre la page paramètres translucide pour voir l'effet du zoom en
   temps réel", affinée ensuite en 2 temps : "ne garder que le slider de zoom actif, rendre le reste encore
   plus transparent", PUIS "le slider du module actif est transparent aussi... j'aimerais qu'il reste opaque
   et que l'input box soit opaque aussi pour lire le pourcentage en cours").
   BUG CORRIGÉ — 1er jet posait opacity sur .settings-box LUI-MÊME (cf. l'historique de cette règle) : la CSS
   `opacity` n'a AUCUN mécanisme pour être "annulée" par un enfant, même avec opacity:1!important dessus
   (contrairement à `zoom`/`transform`, dont l'inverse exact multiplié redonne 1) — un ancêtre à opacity:.1
   rend TOUJOURRS ses descendants au maximum à .1 de leur propre opacité, quoi qu'ils déclarent. Vérifié :
   .zoom-preview-active affichait bien opacity:1 en style calculé tout en restant visuellement délavé à
   l'écran, exactement le symptôme rapporté. Fix : .settings-box reste TOUJOURS à opacity:1 (jamais
   translucide lui-même) ; c'est désormais chaque ENFANT DIRECT de la boîte (onglets, en-tête, sections)
   qui est individuellement rendu translucide via ce sélecteur, à l'exception de la section "Modules
   visibles" active ET de la ligne du module en cours de réglage à l'intérieur — chacun de CES éléments a sa
   propre opacité indépendante (pas de relation ancêtre/descendant transparente entre eux), donc la ligne
   active peut légitimement rester à 1 pendant que ses voisins tombent à .08. */
/* BUG CORRIGÉ (2e ronde) — la 1re version de ce correctif dimait TOUT enfant direct de .settings-box, y
   compris .settings-body : dimer .settings-body lui-même re-multipliait son opacité avec celle de SES
   propres enfants (dont la section "Modules visibles" qu'on cherchait justement à exempter), exactement le
   même problème de compositing en cascade que le tout 1er jet. Fix : ne JAMAIS dimer un conteneur qui a
   lui-même un descendant qu'on veut garder opaque — seuls des éléments SANS lien ancêtre/descendant avec
   .modvis-item.zoom-preview-active sont ciblés ici (en-tête, onglets, pied de page, ET les .settings-section
   qui ne sont pas "modules", ET dans "Modules visibles"/"Ordre des modules" tout ce qui n'est pas la ligne
   active elle-même). */
/* BUG CORRIGÉ (3e ronde, rapporté : "toute la fenêtre de paramètres est plus ou moins opaque, elle était
   plus translucide avant") — .settings-box a un fond COULEUR PLEINE (background:var(--sf), cf. sa
   définition), jamais concerné par le dimming ci-dessus qui ne touche que des ÉLÉMENTS À L'INTÉRIEUR (texte/
   contrôles) : le panneau restait donc opaque dans son ensemble malgré des lignes de texte translucides
   par-dessus, exactement l'effet "encore plus ou moins opaque" décrit — l'ancienne version (opacity sur
   .settings-box entier) rendait TOUT translucide, fond compris, d'où le vrai effet vitre recherché. Fix :
   rendre le FOND lui-même translucide (background, pas opacity) pendant l'aperçu, en plus du dimming
   existant sur les éléments non-actifs — les 2 se cumulent : le panneau entier devient une vitre, la ligne
   active reste nette dessus. */
.settings-box-preview{background:color-mix(in srgb,var(--sf) 20%,transparent)}
/* BUG CORRIGÉ (rapporté avec screenshot : "onglets paramètres toujours trop visibles pendant live-preview")
   — .settings-tabs (la rangée Général/Instrument/Son/.../Modules) n'était JAMAIS incluse dans le dimming :
   seuls .settings-header et .settings-footer l'étaient, laissant cette rangée pleinement opaque à
   chevaucher visuellement la barre d'outils du module qui transparaît juste derrière (constaté : les 2
   rangées de texte se superposaient, illisibles l'une comme l'autre). */
.settings-box-preview>.settings-header,
.settings-box-preview>.settings-tabs,
.settings-box-preview>.settings-footer{opacity:.08}
/* :has(.zoom-preview-active) exempté (évolution, demande explicite : "zoom modules (général) : utiliser
   aussi le live preview") — le zoom général vit dans la section "general", pas "modules" : la règle
   d'origine ne testait QUE data-settings-tab="modules", ce qui aurait dimé la section "general" (et donc SA
   ligne active) même quand c'est ELLE qui contient le slider en cours de réglage. :has() généralise
   l'exemption à N'IMPORTE QUELLE section contenant la ligne active, sans lister chaque tab possible. */
.settings-box-preview .settings-body>.settings-section:not([data-settings-tab="modules"]):not(.zpa-in){opacity:.08}
/* BUG CORRIGÉ (5e ronde, rapporté avec screenshot : le panneau de réglages disparaissait ENTIÈREMENT
   pendant le live-preview au lieu de ne garder que la ligne active nette) — #modVisList n'est pas un
   enfant DIRECT de .settings-section[data-settings-tab="modules"] : il est lui-même imbriqué dans un
   .settings-row (qui porte aussi le texte d'aide + les presets, cf. index.html). La règle précédente
   ciblait uniquement les enfants directs "qui ne sont pas #modVisList", ce qui dimait CE .settings-row
   entier (donc TOUT ce qu'il contient, #modVisList inclus) puisque .settings-row lui-même n'est pas
   #modVisList — même bug de compositing en cascade que d'habitude, une nouvelle fois via un niveau
   d'imbrication non prévu. Fix : cibler explicitement les descendants (pas seulement les enfants directs)
   qui ne sont ni #modVisList ni un ancêtre de #modVisList — en pratique, dimer le hint/presets par leur
   sélecteur propre plutôt que par élimination des enfants directs. */
.settings-box-preview .settings-section[data-settings-tab="modules"]>.settings-row>:not(#modVisList){opacity:.08}
.settings-box-preview .settings-section[data-settings-tab="modules"]>*:not(.settings-row):not(.zpa-in){opacity:.08}
.settings-box-preview #modVisList>.modvis-item:not(.zoom-preview-active){opacity:.08}
/* Section "general" avec la ligne active dedans (zoom général en cours de réglage) : dimer tout SAUF cette
   ligne, même principe que "modules"/#modVisList ci-dessus mais visant .settings-row.zoom-preview-active
   directement (le zoom général n'a pas de sous-liste comme #modVisList, sa ligne est un .settings-row de
   la section elle-même). */
.settings-box-preview .settings-section.zpa-direct>*:not(.zoom-preview-active){opacity:.08}
/* BUG CORRIGÉ (4e ronde, rapporté : "augmenter l'opacité du slider et de la box texte qui sont presque
   invisibles (laisser à 80%)") — le fond translucide de .settings-box-preview (cf. ci-dessus, 20% opaque)
   s'applique aussi DERRIÈRE la ligne active : son opacity:1 la garde nette sur son PROPRE contenu (texte,
   bordures), mais elle n'a pas de fond à elle qui la distingue visuellement du manche/module chargé qui
   transparaît juste derrière — au final le "125 %" se lisait à peine, noyé dans les cercles de notes
   colorés du module en arrière-plan.
   1er correctif insuffisant : var(--sf) (fond du panneau) est une teinte quasi-noire très proche du fond du
   thème sombre de l'app lui-même — même à 80% d'opacité, elle ne tranche pas assez contre le manche sombre
   qui transparaît derrière (mesuré via capture recadrée précisément sur cette ligne : toujours illisible).
   2e essai (var(--s2)) à peine plus clair (#242432 vs #1c1c26 pour --sf), écart encore insuffisant. 3e essai
   (var(--s3) à 80%) fonctionnait tant que le module derrière restait sombre — mais une fois le scrim
   #settingsModal lui-même allégé (cf. son propre correctif, "le module doit être plus visible"), le module
   redevient plein de couleurs VIVES (cercles de notes) qui reprennent le dessus sur un fond encore à 80%
   translucide. Relevé à 95% (quasi opaque) + une bordure dédiée : la ligne active doit rester lisible qu'il
   qu'il y ait derrière un manche sombre OU des cercles colorés vifs, elle ne peut plus se permettre de
   rester elle-même translucide comme le reste du panneau (qui, lui, PEUT le rester : rien à y lire pendant
   l'aperçu). */
.zoom-preview-active{position:relative;z-index:1;background:color-mix(in srgb,var(--s3) 95%,transparent);border:1px solid var(--bd);border-radius:6px}
/* BUG CORRIGÉ (rapporté avec screenshot : "le slider de zoom actif doit être plus visible, il est très
   translucide") — CE N'ÉTAIT PAS le fond de la ligne (.zoom-preview-active ci-dessus, déjà correctif) : le
   <input type="range"> lui-même (.settings-slider) n'a JAMAIS eu de piste stylée nulle part dans le fichier,
   seulement accent-color (qui ne peint que le thumb + le segment REMPLI, jamais le segment vide) — son
   rendu natif par défaut du navigateur est fin/pâle sur fond sombre, un défaut préexistant sur TOUT
   .settings-slider, juste plus flagrant ici une fois isolé sur un fond qui varie peu. Piste peinte
   explicitement ICI (fond clair + bordure) pour cette ligne active spécifiquement plutôt que partout (une
   modification globale de .settings-slider est un changement plus large, hors du périmètre de cette
   demande) — AJUSTER ICI si besoin de plus/moins de contraste (cf. les 4 valeurs commentées ci-dessous). */
/* BUG CORRIGÉ (2e ronde, rapporté : "le slider est plus visible que les autres mais pas assez, j'estime son
   opacité à 15%, je voudrais 80%") — accent-color (posé par .settings-slider de base, JAMAIS retiré ici)
   continue de peindre le thumb natif PAR-DESSUS le thumb custom ci-dessous dans certains navigateurs/
   contextes, à sa propre opacité par défaut, ce qui pouvait donner cette impression de superposition pâle.
   accent-color:auto le neutralise explicitement ICI. Couleurs remplacées par des valeurs LITTÉRALES semi-
   opaques (rgba blanc/accent) plutôt que les tokens de thème (var(--tx2) etc., qui restent proches du fond
   sombre ambiant) — AJUSTER LES 2 VALEURS D'ALPHA ci-dessous (0.8 track, 1 thumb) si besoin de plus/moins. */
/* BUG CORRIGÉ (3e ronde, rapporté : "toujours trop transparent, aucun changement") — cette styling ne
   s'appliquait QUE pendant un glisser actif (sélecteur .zoom-preview-active .settings-slider, classe posée
   par _modZoomPreviewStart au pointerdown et retirée au pointerup) : dans une capture d'écran prise HORS
   glisser (l'état "normal" du panneau, ce que montrait le screenshot rapporté), aucun module n'a cette
   classe et TOUS les sliders retombent sur le rendu natif pâle de .settings-slider de base — d'où
   l'impression que "rien n'a changé" en dehors du geste lui-même. Fix : cette piste stylée devient la piste
   PAR DÉFAUT de tous les sliders de zoom (.modvis-zoom-row et #setModuleZoomSlider), visible en permanence
   plutôt que seulement pendant les 100-200ms d'un glisser actif. */
/* RÉTABLI (demande explicite : "visibilité du slider actif : c'est réglé, tu peux rendre aux sliders
   de zoom leur apparence précédente") — le fix d'opacité de la ligne active (.zoom-preview-active,
   cf. plus haut) suffit maintenant à rendre le slider actif lisible pendant le live-preview ; ce style
   custom blanc/orange forcé en permanence sur TOUS les sliders de zoom n'est donc plus nécessaire.
   Les sliders retombent sur le rendu de base .settings-slider (accent-color natif, cf. sa définition). */
/* Desktop : boîte plus large pour que les onglets (7 désormais, dont "Télécommande" — demande
   explicite : "élargir la fenêtre des paramètres à 1200px de large") tiennent sans scroll horizontal
   sur .settings-tabs — mobile garde 480px, où la largeur d'écran impose de toute façon un défilement
   des onglets. Revoir cette largeur si un futur onglet est ajouté sans l'élargir en conséquence. */
@media(min-width:1024px){
  .settings-box{max-width:1200px}
}
.settings-header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid var(--bd);flex-shrink:0}
.settings-header-tools{display:flex;align-items:center;gap:10px}
.settings-mode-toggle{display:flex;gap:0;padding:0;border:1px solid var(--bd);border-radius:5px;overflow:hidden;background:var(--s2);color:var(--tx3);font:var(--fm);font-size:max(10px,.56rem);text-transform:none;cursor:pointer}
.settings-mode-toggle span{padding:5px 7px}.settings-mode-toggle span:first-child{color:var(--tx)}
.settings-mode-toggle.advanced span:first-child{color:var(--tx3)}.settings-mode-toggle.advanced span:last-child{background:var(--c-root);color:#000}
.settings-search-wrap{display:flex;align-items:center;gap:7px;flex:1 1 auto;margin:0 12px;padding:0 9px;height:30px;border:1px solid var(--bd);border-radius:6px;background:var(--s2);color:var(--tx3);font-family:var(--fm);font-size:.8rem}
.settings-search-wrap input{min-width:0;flex:1;border:0;outline:0;background:transparent;color:var(--tx);font:inherit;font-size:.68rem}
.settings-search-clear{display:flex;align-items:center;justify-content:center;width:18px;height:18px;padding:0;border:0;border-radius:50%;background:var(--bd);color:var(--tx2);font:700 .8rem/1 var(--fm);cursor:pointer}
.settings-search-clear:hover{background:var(--c-root);color:#000}
.settings-searching .settings-body .settings-search-match{cursor:pointer}
.settings-row-flash{animation:settingsRowFlash 2.2s ease-out}
@keyframes settingsRowFlash{0%,60%{outline:2px solid var(--c-root);outline-offset:2px;background:color-mix(in srgb,var(--c-root) 22%,transparent)}100%{outline:2px solid transparent;outline-offset:2px;background:transparent}}
.settings-title{font-family:var(--fd);font-size:1rem;font-weight:700;color:var(--tx)}
/* Style visuel commun désormais dans .modal-close-btn (cf. définition principale, ~ligne 642) — cette
   classe reste posée dans le HTML pour ne rien casser mais ne porte plus aucune propriété propre. */
.settings-tabs{display:flex;gap:2px;padding:0 10px;border-bottom:1px solid var(--bd);flex-shrink:0;overflow-x:auto}
.settings-tab{padding:9px 10px;border:none;border-bottom:2px solid transparent;background:transparent;color:var(--tx2);font-family:var(--fb);font-size:.66rem;font-weight:600;cursor:pointer;white-space:nowrap;transition:all var(--tr)}
.settings-tab:hover{color:var(--tx)}
.settings-tab.active{color:var(--c-root);border-bottom-color:var(--c-root)}
.settings-body{overflow-y:auto;overflow-x:hidden;scrollbar-gutter:stable;padding:12px 16px;display:flex;flex-direction:column;gap:16px}
.settings-body .settings-section{display:none}
.settings-body .settings-section.settings-tab-active{display:flex}
/* Une recherche porte sur tous les paramètres, pas seulement sur l'onglet courant. */
.settings-searching .settings-body>.settings-section{display:none}
.settings-searching .settings-body>.settings-section.zsm-in{display:flex}
.settings-section{display:flex;flex-direction:column;gap:10px}
#settingsModal .settings-section[data-settings-tab="surface"]{padding:0}
#settingsModal #gsSettingsPanel.gs-settings-page{
  display:flex!important;flex-direction:column;position:static;inset:auto;width:100%;max-width:none;min-height:0;
  padding:0;background:transparent;border:0;border-radius:0;box-shadow:none;overflow:visible;
}
#settingsModal #gsSettingsPanel .gs-settings-close{display:none}
#settingsModal #gsSettingsPanel.gs-settings-page{gap:0;align-self:stretch;text-align:left}
#settingsModal #gsSettingsPanel .gs-settings-col{flex:1 1 0;min-width:0;display:flex;flex-direction:column}
#settingsModal #gsSettingsPanel .gs-settings-section{width:100%;margin-bottom:14px}
#settingsModal #gsSettingsPanel .gs-settings-section+.gs-settings-section{padding-top:14px;border-top:2px solid var(--bd)}
#settingsModal #gsSettingsPanel .gs-settings-section-title{font-family:var(--fm);font-size:max(10px,.56rem);font-weight:700;color:var(--c-root);text-transform:uppercase;letter-spacing:.05em;margin-bottom:2px}
#settingsModal #gsSettingsPanel .gs-settings-group{display:flex;align-items:center;flex-wrap:wrap;gap:5px;padding:6px 10px;margin:0 -10px;border-radius:6px}
#settingsModal #gsSettingsPanel .gs-settings-group:nth-child(odd of .gs-settings-group){background:rgba(255,255,255,.025)}
#settingsModal #gsSettingsPanel .gs-settings-group:nth-child(even of .gs-settings-group){background:rgba(255,255,255,.055)}
#settingsModal #gsSettingsPanel .gs-midi-lbl{font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3);white-space:nowrap}
#settingsModal #gsSettingsPanel .gs-settings-group>.gs-midi-lbl{flex:0 0 100%;font-size:max(10px,.52rem);text-transform:uppercase;letter-spacing:.03em}
#settingsModal #gsSettingsPanel .gs-settings-group>.dpk-select-btn{flex:1 1 auto;width:auto;min-width:0}
#settingsModal #gsSettingsPanel .gs-volume-slider{flex:1 1 auto;min-width:0;height:20px;-webkit-appearance:none;appearance:none;border:1px solid var(--bd);background:var(--s3);border-radius:4px;accent-color:var(--c-root);cursor:pointer}
#settingsModal #gsSettingsPanel .gs-vol-val-wrap{position:relative;display:inline-flex;flex-shrink:0}
#settingsModal #gsSettingsPanel .gs-vol-val-wrap::after{content:"%";position:absolute;right:6px;top:50%;transform:translateY(-50%);font-family:var(--fm);font-size:.6rem;color:var(--tx3);pointer-events:none}
#settingsModal #gsSettingsPanel .gs-vol-val-wrap-px::after{content:"px"}
#settingsModal #gsSettingsPanel .gs-vol-val-wrap-ms::after{content:"ms"}
#settingsModal #gsSettingsPanel .gs-vol-val-wrap-plain::after{content:none}
#settingsModal #gsSettingsPanel .gs-vol-val-wrap .settings-val-input{padding-right:16px;width:52px}
#settingsModal #gsSettingsPanel .gs-vol-val-wrap-px .settings-val-input{padding-right:24px;width:66px}
#settingsModal #gsSettingsPanel .gs-vol-val-wrap-ms .settings-val-input{padding-right:24px;width:72px}
#settingsModal #gsSettingsPanel .gs-vol-val-wrap-plain .settings-val-input{padding-right:6px}
#settingsModal #gsSettingsPanel .gs-midi-bar{display:flex;align-items:center;flex-wrap:wrap;gap:5px}
#settingsModal #gsSettingsPanel .gs-midi-bar+.gs-midi-bar{margin-top:6px}
#settingsModal #gsSettingsPanel .gs-settings-embed{max-height:none;overflow:visible;padding:0;background:transparent;border-bottom:none}
#settingsModal #gsSettingsPanel .gs-settings-embed{
  font-family:var(--fm);font-size:max(10px,.58rem);line-height:1.25;color:var(--tx2);
}
#settingsModal #gsSettingsPanel .gs-settings-embed button,
#settingsModal #gsSettingsPanel .gs-settings-embed select,
#settingsModal #gsSettingsPanel .gs-settings-embed input{
  font-family:var(--fm);font-size:max(10px,.55rem);line-height:1.2;
}
#settingsModal #gsSettingsPanel .gs-mini-btn{
  background:var(--s2);border:1px solid var(--bd);border-radius:5px;color:var(--tx2);
  font-family:var(--fm);font-size:max(10px,.52rem);padding:4px 7px;cursor:pointer;white-space:nowrap;
}
#settingsModal #gsSettingsPanel .gs-mini-btn:hover{border-color:var(--c-root);color:var(--tx)}
#settingsModal #gsSettingsPanel .gs-mini-btn.on{background:var(--c-root);color:#111;border-color:var(--c-root)}
#settingsModal #gsSettingsPanel .gs-mini-btn.gs-mini-btn-contour-on{background:transparent;border-color:var(--c-root);color:var(--c-root)}
#settingsModal #gsSettingsPanel .gs-oct-group{display:inline-flex;align-items:center;gap:4px}
#settingsModal #gsSettingsPanel .gs-oct-group .tbtn.gs-oct-btn{width:30px;height:28px;padding:0;font-size:.9rem}
#settingsModal #gsSettingsPanel .gs-midi-note{font-family:var(--fm);font-size:max(10px,.52rem);color:var(--tx3);cursor:help}
#settingsModal #gsSettingsPanel .gs-midi-status{
  font-family:var(--fm);font-size:max(10px,.56rem);line-height:1.25;color:var(--tx3);white-space:normal;
}
#settingsModal #gsSettingsPanel .gs-midi-status.on{color:var(--c-root)}
#settingsModal #gsSettingsPanel .gs-mode-btn{
  width:auto;min-width:0;height:28px;padding:0 9px;font-size:max(10px,.58rem);white-space:nowrap;
}
#settingsModal #gsSettingsPanel .gs-oct-val{font-family:var(--fm);font-size:max(10px,.58rem);font-weight:700;color:var(--tx);min-width:32px;text-align:center;pointer-events:none}
#settingsModal #gsSettingsPanel .gs-chord-rows-panel{
  display:flex;flex-wrap:wrap;gap:6px;padding:4px 8px;margin-top:6px;background:var(--s2);border-bottom:1px solid var(--bd);flex-shrink:0;
}
#settingsModal #gsSettingsPanel .gs-chord-row-ctrl{
  display:flex;flex-direction:column;align-items:center;gap:2px;flex-shrink:0;
  padding:3px 6px;border-radius:6px;border:1px solid var(--gs-c);min-width:64px;
}
#settingsModal #gsSettingsPanel .gs-chord-row-label{font-family:var(--fm);font-size:max(10px,.55rem);font-weight:700;color:var(--gs-c);white-space:nowrap}
#settingsModal #gsSettingsPanel .gs-chord-row-ctrl select{font-size:max(10px,.5rem);max-width:70px}
#settingsModal #gsSettingsPanel .gs-params-head{display:flex;align-items:center;justify-content:space-between;gap:6px;flex-wrap:wrap;margin-bottom:5px}
#settingsModal #gsSettingsPanel .gs-params-title{font-family:var(--fm);font-size:.62rem;font-weight:700;color:var(--tx);text-transform:uppercase;letter-spacing:.04em}
#settingsModal #gsSettingsPanel .gs-params-actions{display:flex;gap:4px;flex-wrap:wrap}
#settingsModal #gsSettingsPanel .gs-mini-btn{
  font-family:var(--fm);font-size:max(10px,.52rem);padding:3px 7px;border-radius:5px;
  background:var(--s3);color:var(--tx2);border:1px solid var(--bd);cursor:pointer;white-space:nowrap;
}
#settingsModal #gsSettingsPanel .gs-mini-btn:hover{border-color:var(--c-root);color:var(--tx)}
#settingsModal #gsSettingsPanel .gs-mini-btn.on{background:var(--c-root);color:#111;border-color:var(--c-root)}
#settingsModal #gsSettingsPanel .gs-params-empty{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx3);line-height:1.5;padding:4px 0}
#settingsModal #gsSettingsPanel .gs-params-grid{display:flex;flex-wrap:wrap;gap:6px 10px;justify-content:flex-start}
#settingsModal #gsSettingsPanel .gs-param{display:flex;flex-direction:column;gap:2px;min-width:120px;flex:1 1 120px}
#settingsModal #gsSettingsPanel .gs-param-lbl{font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#settingsModal #gsSettingsPanel .gs-param-val{font-family:var(--fm);font-size:max(10px,.5rem);color:var(--c-root);font-weight:700;white-space:nowrap}
#settingsModal #gsSettingsPanel .gs-param input[type=range]{width:100%;height:14px;accent-color:var(--c-root)}
#settingsModal #gsSettingsPanel .gs-param input[type=range]::-webkit-slider-runnable-track{height:3px;background:var(--bd);border-radius:2px}
#settingsModal #gsSettingsPanel .gs-param input[type=range]::-moz-range-track{height:3px;background:var(--bd);border-radius:2px}
#settingsModal #gsSettingsPanel .gs-param input[type=range]::-webkit-slider-thumb{width:14px;height:14px;border-radius:50%;background:var(--c-root);border:none;cursor:pointer}
#settingsModal #gsSettingsPanel .gs-param input[type=range]::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:var(--c-root);border:none;cursor:pointer}
#settingsModal #gsSettingsPanel .tb303-knob-wrap{display:flex;flex-direction:column;align-items:center;gap:2px;width:64px}
#settingsModal #gsSettingsPanel .tb303-knob-label{
  font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3);text-align:center;
  white-space:normal;line-height:1.15;min-height:2.3em;display:flex;align-items:flex-end;justify-content:center;
  width:100%;box-sizing:border-box;overflow-wrap:break-word;word-break:break-word;min-width:0;
}
#settingsModal #gsSettingsPanel .tb303-knob-val{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--c-root);font-weight:700}
#settingsModal #gsSettingsPanel .tb303-knob-track{fill:none;stroke:var(--s3);stroke-width:4;stroke-linecap:round}
#settingsModal #gsSettingsPanel .tb303-knob-fill{fill:none;stroke:var(--c-root);stroke-width:4;stroke-linecap:round}
#settingsModal #gsSettingsPanel .tb303-knob-dot{fill:var(--tx)}
#settingsModal #gsSettingsPanel .tb303-knob-svg{touch-action:none}
#settingsModal #gsSettingsPanel .gs-preset-row{display:flex;flex-wrap:wrap;gap:4px;margin:0 0 6px}
#settingsModal #gsSettingsPanel .gs-preset{display:inline-flex;align-items:stretch;border:1px solid var(--bd);border-radius:6px;overflow:hidden}
#settingsModal #gsSettingsPanel .gs-preset-load{
  font-family:var(--fm);font-size:max(10px,.54rem);padding:3px 8px;border:none;
  background:var(--s3);color:var(--tx2);cursor:pointer;white-space:nowrap;max-width:150px;
  overflow:hidden;text-overflow:ellipsis;
}
#settingsModal #gsSettingsPanel .gs-preset-load:hover{background:var(--c-root);color:#111}
#settingsModal #gsSettingsPanel .gs-preset-del{
  font-family:var(--fm);font-size:max(10px,.54rem);padding:3px 5px;border:none;border-left:1px solid var(--bd);
  background:var(--s3);color:var(--tx3);cursor:pointer;line-height:1;
}
#settingsModal #gsSettingsPanel .gs-preset-del:hover{background:#a33;color:#fff}
#settingsModal #gsSettingsPanel .gs-sel-inline{width:auto;max-width:180px;min-width:0;flex:0 1 auto}
#settingsModal #gsSettingsPanel .gs-sel-narrow{max-width:56px}
#settingsModal #gsSettingsPanel .gs-settings-group{
  display:flex;align-items:center;flex-wrap:wrap;gap:5px;padding:6px 10px;margin:0 -10px;border-radius:6px
}
#settingsModal #gsSettingsPanel .gs-settings-group:nth-child(odd of .gs-settings-group){background:rgba(255,255,255,.025)}
#settingsModal #gsSettingsPanel .gs-settings-group:nth-child(even of .gs-settings-group){background:rgba(255,255,255,.055)}
#settingsModal #gsSettingsPanel .gs-settings-group>.gs-midi-lbl{flex:0 0 100%;font-size:max(10px,.52rem);text-transform:uppercase;letter-spacing:.03em}
#settingsModal #gsSettingsPanel .gs-settings-group>.dpk-select-btn{flex:1 1 auto;width:auto;min-width:0}
#settingsModal #gsSettingsPanel .gs-volume-slider{flex:1 1 auto;min-width:0;height:20px}
#settingsModal #gsSettingsPanel .gs-volume-val{flex:0 0 40px;text-align:right;font-size:.6rem;color:var(--tx2);font-variant-numeric:tabular-nums}
#settingsModal #gsSettingsPanel #gsFxDrawerPanel{border:none;background:transparent;border-radius:0}
#settingsModal #gsSettingsPanel #gsFxDrawerPanel .ref-table-handle{padding:6px 10px;border-radius:6px;background:rgba(255,255,255,.055)}
#settingsModal #gsSettingsPanel #gsFxDrawerPanel .ref-table-body{border-top:none}
#settingsModal #gsSettingsPanel .gs-chord-edit-segs{display:flex;flex-wrap:wrap;gap:0;border:1px solid var(--bd);border-radius:5px;overflow:hidden}
#settingsModal #gsSettingsPanel .gs-chord-seg{padding:3px 5px;font-family:var(--fm);font-size:max(10px,.52rem);color:var(--tx);background:var(--s2);cursor:pointer}
#settingsModal #gsSettingsPanel .gs-chord-seg+.gs-chord-seg{border-left:1px solid color-mix(in srgb,var(--bd) 60%,transparent)}
#settingsModal #gsSettingsPanel .gs-chord-seg:hover{background:color-mix(in srgb,var(--gs-c) 35%,var(--s1))}
#settingsModal #gsSettingsPanel .gs-chord-seg[data-seg="octave"]{color:var(--tx2);font-size:max(10px,.48rem);letter-spacing:.02em}
#settingsModal #gsSettingsPanel .gs-chord-seg[data-seg="octave"]::before{content:'oct ';opacity:.65}
#settingsModal #gsSettingsPanel .gs-chord-rows-panel{display:flex;flex-wrap:wrap;gap:6px;padding:4px 8px;margin-top:6px;background:var(--s2);border-bottom:1px solid var(--bd);flex-shrink:0}
#settingsModal #gsSettingsPanel .gs-chord-row-ctrl{display:flex;flex-direction:column;align-items:center;gap:2px;flex-shrink:0;padding:3px 6px;border-radius:6px;border:1px solid var(--gs-c);min-width:64px}
#settingsModal #gsSettingsPanel .gs-chord-row-label{font-family:var(--fm);font-size:max(10px,.55rem);font-weight:700;color:var(--gs-c);white-space:nowrap}
#settingsModal #gsSettingsPanel .gs-chord-row-ctrl select{font-size:max(10px,.5rem);max-width:70px}
#settingsModal #gsSettingsPanel .gs-chord-chip-tray{display:flex;gap:6px;flex-wrap:wrap;padding:4px 8px}
#settingsModal #gsSettingsPanel .gs-chord-chip{display:flex;align-items:center;gap:5px;padding:4px 6px;border-radius:6px;border:1px solid var(--gs-c);background:color-mix(in srgb,var(--gs-c) 14%,var(--s2));font-family:var(--fm);font-size:.62rem;font-weight:700;color:var(--gs-c);cursor:grab;touch-action:none;user-select:none;-webkit-user-select:none}
#settingsModal #gsSettingsPanel .gs-chord-chip:active{cursor:grabbing}
#settingsModal #gsSettingsPanel .gs-chord-chip-oct{font-size:max(10px,.5rem);font-weight:400;color:var(--tx3);opacity:.8}
#settingsModal #gsSettingsPanel .gs-chord-chip-del{background:none;border:none;color:var(--tx3);font-size:.7rem;line-height:1;cursor:pointer;padding:0 2px}
#settingsModal #gsSettingsPanel .gs-chord-chip-del:hover{color:var(--tx)}
#settingsModal #gsSettingsPanel .gs-chord-chip-ghost{box-shadow:0 4px 14px var(--sh)}
#settingsModal #gsSettingsPanel.gs-settings-preview .gs-settings-group:not(.gs-settings-preview-active){opacity:.35}
#settingsModal #gsSettingsPanel.gs-settings-preview .gs-settings-group.gs-settings-preview-active{position:relative;background:var(--sf);border-radius:6px;box-shadow:0 0 0 1px var(--bd)}
@media(min-width:700px){
  #settingsModal #gsSettingsPanel.gs-settings-page{flex-direction:row;gap:32px}
}
/* Mobile (< 700 px) : sections repliables + barre de raccourcis + lignes compactes (libellé à gauche du
   contrôle au lieu d'au-dessus), cf. _gsSettingsAccordionInit (surface.js). */
#settingsModal #gsSettingsPanel .gs-settings-jump{display:none}
@media(max-width:699px){
  #settingsModal #gsSettingsPanel .gs-settings-jump{display:flex;gap:6px;overflow-x:auto;scrollbar-width:none;position:sticky;top:-12px;z-index:5;margin:-12px -16px 8px;padding:8px 16px;background:var(--sf);border-bottom:1px solid var(--bd)}
  #settingsModal #gsSettingsPanel .gs-settings-jump::-webkit-scrollbar{display:none}
  #settingsModal #gsSettingsPanel .gs-settings-jump button{flex:none;height:30px;padding:0 11px;border-radius:15px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.6rem;font-weight:700;white-space:nowrap;cursor:pointer}
  #settingsModal #gsSettingsPanel .gs-settings-section{margin-bottom:0}
  #settingsModal #gsSettingsPanel .gs-settings-section+.gs-settings-section{padding-top:0;border-top:1px solid var(--bd)}
  #settingsModal #gsSettingsPanel .gs-settings-section-title{display:flex;align-items:center;min-height:44px;margin:0;font-size:.66rem;cursor:pointer;user-select:none;-webkit-user-select:none}
  #settingsModal #gsSettingsPanel .gs-settings-section-title::after{content:'';margin-left:auto;width:8px;height:8px;border-right:2px solid var(--tx3);border-bottom:2px solid var(--tx3);transform:rotate(45deg) translate(-2px,-2px);transition:transform .15s}
  #settingsModal #gsSettingsPanel .gs-sec-collapsed .gs-settings-section-title::after{transform:rotate(-45deg)}
  #settingsModal:not(.settings-searching) #gsSettingsPanel .gs-sec-collapsed>:not(.gs-settings-section-title){display:none!important}
  #settingsModal #gsSettingsPanel .gs-settings-section:not(.gs-sec-collapsed){padding-bottom:10px}
  #settingsModal #gsSettingsPanel .gs-settings-group{flex-wrap:nowrap;min-height:44px;padding:4px 10px;gap:8px}
  #settingsModal #gsSettingsPanel .gs-settings-group>.gs-midi-lbl{flex:0 0 34%;white-space:normal;line-height:1.25;font-size:max(10px,.52rem)}
  #settingsModal #gsSettingsPanel .gs-settings-group:has(>.gs-midi-lbl+:is(button,.tbtn,.dpk-select-btn,select):last-child)>.gs-midi-lbl{flex:1 1 auto}
  #settingsModal #gsSettingsPanel .gs-settings-group:has(>:nth-child(5)){flex-wrap:wrap}
  /* glisser verticalement sur un curseur fait défiler la page au lieu de changer la valeur */
  #settingsModal #gsSettingsPanel input[type=range]{touch-action:pan-y;height:28px}
  #settingsModal #gsSettingsPanel .gs-vol-val-wrap .settings-val-input{width:46px}
}
.settings-section-title{font-family:var(--fb);font-size:.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--c-root);padding-bottom:4px;border-bottom:1px solid var(--bd)}
.settings-subsection-title{margin:12px 0 4px;font-family:var(--fm);font-size:max(10px,.56rem);font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--tx3)}
/* Fond zébré (demande explicite, avec screenshot : "il est difficile d'identifier quel bouton correspond à
   quel texte... fond avec 2 couleurs alternées (sombre/clair) comme dans un tableau Excel") — chaque
   .settings-row (label+hint à gauche, boutons à droite, cf. plus bas) reçoit un fond de rangée complet
   (padding horizontal négatif compensé pour déborder jusqu'aux bords de .settings-body, cf. margin/padding
   ci-dessous) qui alterne entre 2 teintes très proches de var(--sf) — l'écart doit rester subtil (zébrage
   utilitaire, pas un habillage visuel fort) mais suffisant pour que l'œil rattache visuellement le bloc de
   boutons de droite à SA ligne, plutôt que d'hésiter entre 2 lignes adjacentes proches en hauteur. Rayon de
   bord (border-radius) pour ne pas trancher au ras du texte, cohérent avec le reste de l'UI arrondie. */
.settings-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:6px 10px;margin:0 -10px;border-radius:6px}
.settings-row> :first-child{min-width:0;flex:1}.settings-row> :last-child{flex-shrink:0}.settings-row .settings-control{display:flex;align-items:center;justify-content:flex-end;min-width:180px}
.settings-advanced-badge{display:inline-block;margin-left:6px;color:#ff8a22;font:600 max(10px,.52rem) var(--fm);vertical-align:middle}
/* :nth-child(An+B OF S) (pas un simple :nth-child) — nécessaire car .settings-row n'est pas le SEUL type
   d'enfant direct de .settings-section : .settings-section-title (ex. avant "Modules visibles"/"Ordre des
   modules") s'intercale par endroits entre des .settings-row, ce qui aurait décalé/cassé la parité
   pair/impair d'un :nth-child classique à partir de ce point. La syntaxe "of" ne compte QUE les éléments
   qui correspondent au sélecteur .settings-row, ignorant tout le reste — le zébrage reste donc cohérent
   même autour d'un titre de sous-section intercalé. */
.settings-row:nth-child(odd of .settings-row){background:rgba(255,255,255,.025)}
.settings-row:nth-child(even of .settings-row){background:rgba(255,255,255,.055)}
.settings-simple-hidden{display:none!important}
/* Paramètres > Apparence : lignes sans effet dans l'état courant (cf. _syncAppearanceDeps) et réglages fins repliés */
.settings-dep-hidden{display:none!important}
.settings-fold{margin:4px 0 2px;border-top:1px solid var(--bd)}
.settings-fold>summary{padding:8px 0 4px;cursor:pointer;list-style:none;font-family:var(--fm);font-size:.6rem;font-weight:600;color:var(--tx2);user-select:none}
.settings-fold>summary::-webkit-details-marker{display:none}
.settings-fold>summary::before{content:'';display:inline-block;width:5px;height:5px;margin:0 8px 1px 1px;border-right:1.5px solid var(--tx3);border-bottom:1.5px solid var(--tx3);transform:rotate(-45deg);transition:transform var(--tr)}
.settings-fold[open]>summary::before{transform:rotate(45deg)}
.settings-fold>summary:hover{color:var(--tx)}.settings-searching .settings-search-hidden{display:none!important}
.settings-label{font-family:var(--fb);font-size:.72rem;color:var(--tx)}
.settings-hint{font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx3);line-height:1.3}
/* BUG CORRIGÉ (rapporté avec screenshot : "la page paramètres sur mobile est un peu trop large... le texte
   est mal mis en page") — .settings-row en display:flex SANS wrap ni flex-shrink explicite force ses 2
   enfants (le libellé/hint à gauche, .settings-seg à droite, cf. plus haut) à rester côte à côte même
   quand leur somme dépasse la largeur de .settings-box (480px max, mais un mobile étroit type Xiaomi Mi11
   en a moins) — la ligne de boutons (jusqu'à 6, ex. Taille du texte : S/M/L/XL/XXL/MAX) ne rétrécissait
   jamais, forçant TOUTE la ligne (et donc la page) à déborder horizontalement, tout en compressant le
   texte du libellé dans le peu de place qui restait (d'où le texte "mal mis en page" du screenshot, un mot
   par ligne). Fix : autoriser le retour à la ligne du couple label/seg sous le seuil mobile plutôt que de
   les forcer indéfiniment sur la même rangée — .settings-seg passe alors sur SA PROPRE ligne, pleine
   largeur, où ses boutons peuvent eux-mêmes s'enrouler proprement (flex-wrap posé sur .settings-seg,
   cf. plus bas) sans jamais pousser la page au-delà de sa largeur. */
/* BUG CORRIGÉ (rapporté avec screenshot : "menu paramètres sur mobile : affichage cassé (alignement
   latéral) dans tous les onglets, surtout dans modules") — .settings-row{flex-wrap:wrap} ci-dessous
   s'appliquait AUSSI aux quelques .settings-row utilisant flex-direction:column en style inline (ex.
   #modVisPresets/#modVisList/#tabOrderList, listes empilées verticalement, pas des paires label+contrôle
   horizontales) : flex-wrap sur un conteneur flex-direction:column enroule perpendiculairement à l'axe
   principal, c'est-à-dire EN NOUVELLES COLONNES à droite dès que le contenu dépasse la HAUTEUR du
   conteneur — jamais l'intention ici. Résultat mesuré : les 6 boutons de preset (Modules visibles)
   s'étirent chacun à ~460px de haut et se répartissent sur 3 colonnes hors-champ, poussant tout le panneau
   Paramètres en défilement horizontal (largeur mesurée ~907px sur un écran de 337px) — le bug "aligné
   latéralement" décrit, présent sur TOUS les onglets mais plus flagrant sur Modules (listes les plus
   longues). Fix : le sélecteur exclut désormais les lignes column (`:not([style*="column"])`), qui n'ont de
   toute façon jamais besoin de ce wrap horizontal. */
/* BUG CORRIGÉ (rapporté : "en dessous de 520px, mise en page toujours bancale (boutons centrés
   étrangement)... il faut aligner les boutons à droite") — .settings-seg{justify-content:center} (règle de
   base, cf. sa définition) centrait le dernier rang incomplet de boutons quand .settings-seg passe sur sa
   propre ligne pleine largeur ici (cf. flex:1 1 100% ci-dessous) : au-delà d'1 seule rangée de boutons, ce
   centrage donnait un aspect "flottant au milieu" décalé par rapport au reste du panneau (labels/hints,
   tous alignés à gauche) plutôt qu'un ancrage cohérent à un bord. Aligné à DROITE à la place (justify-
   content:flex-end), cohérent avec la position des boutons/segments en DESKTOP (toujours à droite de leur
   libellé, jamais centrés) — seul le fait de passer à la ligne change sur mobile, pas le bord d'ancrage. */
/* BUG CORRIGÉ (rapporté avec screenshot : "essayer de réduire la hauteur des lignes quand c'est possible —
   par exemple Langue et Thème peuvent utiliser une seule ligne pour le titre et les boutons, le menu
   déroulant Palette peut être sur la même ligne que le titre") — les 3 correctifs précédents forçaient le
   libellé ET le contrôle en flex:1 1 100% INCONDITIONNELLEMENT dès qu'on repasse sous 520px, même quand les
   deux tiennent largement sur une seule ligne (ex. "Langue" + 2 boutons FR/EN) : gaspillait de la hauteur
   sur CHAQUE ligne courte, forçant un défilement bien plus long que nécessaire pour un panneau qui n'a
   pourtant besoin de ce retour à la ligne QUE pour les libellés/contrôles réellement trop larges (ex.
   "Taille du texte" + 6 boutons). Fix : ne plus forcer flex:1 1 100% — laisser flex-wrap:wrap (conservé)
   décider naturellement ligne par ligne selon le contenu réel, avec min-width:0 sur le libellé pour qu'il
   puisse se compresser plutôt que forcer un retour à la ligne prématuré tant que ça tient encore. */
@media(max-width:520px){
  .settings-row:not([style*="column"]){flex-wrap:wrap}
  /* BUG CORRIGÉ (rapporté : "le texte peut s'arrêter juste avant les boutons de Couleur des notes par
     hauteur réelle et Toujours afficher les notes en dièse") — flex-wrap:wrap SEUL faisait tomber le
     libellé/hint sur sa propre ligne pleine largeur dès qu'il ne pouvait plus cohabiter avec les boutons à
     leur taille naturelle (cf. .settings-seg ci-dessous, flex-shrink par défaut à 1 pourtant) : sans un
     flex-basis explicite, un flex-item se redimensionne sur la base de son CONTENU (ici : la largeur du
     texte non contraint), pas sur "l'espace qu'il reste" — le libellé grandissait donc jusqu'à forcer le
     wrap plutôt que se contenter de la largeur disponible. flex:1 1 0 (basis:0, pas auto) force le libellé
     à partir de rien et grandir SEULEMENT avec l'espace restant une fois les boutons soustraits (eux en
     flex:0 0 auto — jamais rétrécis, toujours leur taille naturelle) : les 2 restent sur la même ligne, le
     texte du libellé s'enroule pour ne jamais chevaucher les boutons, exactement le comportement demandé. */
  .settings-row:not([style*="column"])>div:first-child{flex:1 1 0;min-width:120px}
  .settings-row:not([style*="column"])>.settings-seg,.settings-row:not([style*="column"])>.settings-control{flex:0 0 auto}
  .settings-row>.settings-control .settings-slider{flex:1;width:auto;min-width:0}
  .settings-row .settings-seg{justify-content:flex-end}
}
/* ── Tour guidé par module (spotlight) ───────────────────────────────────
   4 couches empilées au clic sur un pas de tour : un overlay plein écran assombri (.mt-overlay), un
   "trou" découpé dessus qui laisse voir l'élément ciblé en pleine lumière (.mt-hole, positionné en JS
   sur le rect réel de la cible), un anneau autour du trou pour le faire ressortir (.mt-ring, même rect),
   et une bulle de texte (.mt-bubble) positionnée à côté de la cible sans jamais sortir du viewport. Le
   trou utilise box-shadow avec un spread énorme plutôt que clip-path/mask — plus simple à animer en
   position (juste top/left/width/height) et fonctionne même si la cible est tout près d'un bord d'écran. */
.mt-overlay{position:fixed;inset:0;z-index:10001;background:transparent;pointer-events:none}
.mt-hole{
  position:fixed;z-index:10001;border-radius:10px;pointer-events:none;
  box-shadow:0 0 0 9999px rgba(0,0,0,.72);
  transition:top .25s ease,left .25s ease,width .25s ease,height .25s ease;
}
.mt-ring{
  position:fixed;z-index:10002;border-radius:10px;pointer-events:none;
  border:2px solid var(--c-root);box-shadow:0 0 0 4px rgba(0,0,0,.2),0 0 16px var(--c-root);
  transition:top .25s ease,left .25s ease,width .25s ease,height .25s ease;
}
.mt-bubble{
  position:fixed;z-index:10003;max-width:280px;background:var(--sf);border:1px solid var(--bd);
  border-radius:var(--rl);padding:14px 16px;box-shadow:0 8px 24px var(--sh);
  transition:top .25s ease,left .25s ease;
}
.mt-bubble-step{font-family:var(--fm);font-size:max(10px,.58rem);color:var(--c-root);font-weight:700;letter-spacing:.06em;text-transform:uppercase;margin-bottom:6px}
.mt-bubble-title{font-family:var(--fb);font-size:.8rem;font-weight:700;color:var(--tx);margin:0 0 6px}
.mt-bubble-body{font-family:var(--fb);font-size:.72rem;color:var(--tx2);line-height:1.5;margin:0 0 12px}
.mt-bubble-foot{display:flex;align-items:center;justify-content:space-between;gap:10px}
.mt-bubble-skip{background:transparent;border:none;color:var(--tx3);font-family:var(--fm);font-size:.6rem;cursor:pointer;text-decoration:underline;padding:2px}
.mt-bubble-next{background:var(--c-root);color:#111;border:none;border-radius:6px;padding:6px 14px;font-family:var(--fb);font-weight:700;font-size:.68rem;cursor:pointer}
/* Ancien FAB flottant du bouton aide — retiré au profit de #mtHelpBtnTopbar (barre du haut, contre le
   bouton plein écran), désormais affiché sur TOUS les viewports, pas seulement mobile. Règle conservée
   uniquement pour forcer display:none au cas où un ancien #mtHelpBtn traînerait encore en DOM (ex. cache
   navigateur sur un HTML plus vieux que ce CSS) — la classe .show ne le réactivera plus. */
.mt-help-btn{display:none!important}
/* Boutons recherche (⌕) et aide (?) intégrés à la barre du haut, contre le bouton plein écran — remplacent
   les anciens FAB flottants séparés (cmdp-mobile-btn / mt-help-btn en position:fixed). #mtHelpBtnTopbar
   suit uniquement la disponibilité d'un tour pour l'onglet courant (.show togglée en JS, cf. switchTab) ;
   la recherche (#cmdPaletteTopbarBtn) est toujours affichée, sur tous les viewports. */
.tbr-mobile-only{display:none}
@media(max-width:1023px){.tbr-mobile-only{display:flex}}
#mtHelpBtnTopbar{display:none}
#mtHelpBtnTopbar.show{display:flex}

/* ── Onboarding (premier lancement) ─────────────────────────────────────── */
.ob-modal{position:fixed;inset:0;z-index:10000;background:rgba(0,0,0,.8);display:flex;align-items:center;justify-content:center;padding:16px}
.ob-box{position:relative;background:var(--sf);border:1px solid var(--bd);border-radius:var(--rl);width:100%;max-width:560px;max-height:90vh;overflow-y:auto;padding:32px 28px 26px}
/* Desktop uniquement : +50% de largeur (560px→840px) pour l'étape 2 ("Ce qu'il faut savoir avant de
   commencer", 9 tips) qui se sentait étriquée avec autant de contenu — les 3 étapes partagent la même
   .ob-box (contenu basculé via display:none/flex, pas une boîte par étape), donc le changement s'applique
   aux 3, mais sans régression : la grille de préréglages de l'étape 1 (.ob-presets, auto-fill) s'étale
   simplement sur plus de colonnes, l'étape 3 (texte + panneau Paramètres en arrière-plan) reste lisible à
   cette largeur. Pas de media query desktop dédiée existante pour ce modal (contrairement à .settings-modal
   qui en a une) — min-width:1300px choisi pour matcher le seuil sidebar desktop déjà utilisé ailleurs. */
@media(min-width:1024px){.ob-box{max-width:840px}}
.ob-close{position:absolute;top:12px;right:12px}
.ob-lang-switch{position:absolute;top:12px;right:52px}
.ob-eyebrow{font-family:var(--fm);font-size:.6rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--c-root);margin-bottom:8px}
.ob-title{font-family:var(--fd);font-size:1.25rem;color:var(--tx);margin:0 0 8px}
.ob-sub{font-family:var(--fb);font-size:.78rem;color:var(--tx2);margin:0 0 20px;line-height:1.5}
.ob-presets{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:10px;margin-bottom:18px}
.ob-preset{text-align:left;background:var(--s2);border:1px solid var(--bd);border-radius:10px;padding:12px 14px;cursor:pointer;transition:all var(--tr);font-family:inherit}
.ob-preset:hover{border-color:var(--c-root);background:var(--s3)}
/* .ob-preset-active : marque le choix DÉJÀ actif dans une grille de presets (étape instrument de
   l'onboarding, cf. _onboardingRenderInstruments) — sans ce repère, la grille s'affichait sans aucun état
   alors qu'un instrument par défaut (guitare 6c standard) est bel et bien déjà sélectionné. Bordure
   accentuée + fond légèrement relevé, même vocabulaire visuel que :hover ci-dessus mais persistant. */
.ob-preset-active{border-color:var(--c-root);background:var(--s3)}
.ob-preset b{display:block;font-family:var(--fb);font-size:.76rem;color:var(--tx);margin-bottom:3px}
.ob-preset span{display:block;font-family:var(--fm);font-size:.6rem;color:var(--tx3);line-height:1.4}
.ob-skip{display:block;margin:0 auto;background:transparent;border:none;color:var(--tx3);font-family:var(--fm);font-size:.62rem;text-decoration:underline;cursor:pointer;padding:6px;text-align:center}
.ob-tips{display:flex;flex-direction:column;gap:16px;margin-bottom:22px}
.ob-tip{display:flex;gap:14px;align-items:flex-start}
.ob-tip-num{flex-shrink:0;width:26px;height:26px;border-radius:50%;background:var(--c-root);color:#111;font-family:var(--fm);font-weight:700;font-size:.72rem;display:flex;align-items:center;justify-content:center}
.ob-tip b{font-family:var(--fb);font-size:.78rem;color:var(--tx);display:block;margin-bottom:3px}
.ob-tip p{font-family:var(--fb);font-size:.72rem;color:var(--tx2);margin:0;line-height:1.5}
.ob-start{display:block;width:100%;background:var(--c-root);color:#111;border:none;border-radius:8px;padding:12px;font-family:var(--fb);font-weight:700;font-size:.8rem;cursor:pointer}
/* .modvis-item ajoutée (évolution, demande explicite : "Page modules : fusionner modules visibles/zoom et
   ordre des modules... réorganisables") — _tabOrderAttachDrag (app.js) est désormais aussi appelée depuis
   renderModuleVisibilityList sur .modvis-item, réutilisant CETTE MÊME classe .tab-order-drop-before comme
   indicateur de dépôt (déduite dynamiquement du sélecteur de la ligne réelle, cf. son commentaire), d'où
   le besoin d'une règle visuelle pour les 2 classes hôtes possibles. */
.tab-order-row.tab-order-drop-before,
.modvis-item.tab-order-drop-before{box-shadow:0 -2px 0 0 var(--c-root)}
/* Poignée de glisser dédiée (évolution ci-dessus) — seule zone de .modvis-item qui engage le
   réordonnancement (cf. _tabOrderAttachDrag, paramètre `handle`), le reste de la ligne portant maintenant
   trop de contrôles cliquables (toggle, info, slider, champ texte) pour qu'un drag depuis n'importe où soit
   sûr. touch-action:none nécessaire ICI (pas sur toute la ligne, contrairement à l'ancienne .tab-order-row
   qui ne couvrait que 50% de sa largeur pour la même raison) : cette poignée est étroite, un doigt posé
   dessus n'a jamais besoin de scroller la page en dessous, donc aucun compromis de largeur à faire. */
.modvis-drag-handle{flex-shrink:0;width:16px;text-align:center;font-family:var(--fm);color:var(--tx3);cursor:grab;touch-action:none;user-select:none;font-size:.7rem}
.modvis-drag-handle:active{cursor:grabbing}
/* Drag direct des barres d'onglets (cf. _liveTabDragAttach). opacity:.05 (valeur utilisée par le drag des
   blocs d'accords, .insp-dragging) rendait l'onglet quasi invisible plutôt que "transparent" perceptible —
   un bloc d'accord est une grande carte colorée sur fond neutre (le fantôme à .05 reste repérable), un
   onglet est un simple libellé texte fin (à .05 il disparaît purement et simplement, constaté par
   l'utilisateur). Valeur plus haute pour rester clairement translucide sans se fondre dans le fond. */
.tab.tab-dragging{opacity:.65}
/* Indicateur de dépôt pendant le drag direct des barres d'onglets (#mainTabs horizontal, #sidebarTabs
   vertical, cf. _liveTabDragAttach) — barre latérale plutôt que top pour #mainTabs, cohérent avec l'axe
   de déplacement de chaque barre. */
#mainTabs .tab.tab-order-drop-before{box-shadow:-2px 0 0 0 var(--c-root)}
#sidebarTabs .tab.tab-order-drop-before{box-shadow:0 -2px 0 0 var(--c-root)}
/* Clone de l'onglet qui suit le curseur pendant le drag (cf. _liveTabDragAttach) — même rôle que
   .insp-drag-ghost pour les blocs d'accords : positionné en fixed, mis à jour à chaque pointermove,
   ignore les clics (pointer-events:none) pour ne jamais intercepter le pointeur pendant le geste.
   transition:none obligatoire : le clone hérite de .tab{transition:all var(--tr)} (classe conservée sur
   le cloneNode), qui animait chaque changement de left/top sur .16s — donnant une sensation de traînée/
   inertie derrière le curseur au lieu d'un suivi instantané (constaté par l'utilisateur). */
.tab-drag-ghost{position:fixed;pointer-events:none;z-index:9999;display:flex;align-items:center;justify-content:center;background:var(--s2);border:1px solid var(--c-root);border-radius:var(--r);box-shadow:0 4px 14px rgba(0,0,0,.35);opacity:.92;transition:none}
.settings-control{display:flex;align-items:center;gap:8px}
.settings-slider{width:110px;accent-color:var(--c-root);cursor:pointer}
.settings-val{font-family:var(--fm);font-size:.62rem;color:var(--tx2);min-width:34px;text-align:right}
.settings-section[data-settings-tab="appearance"] .settings-row.has-slider>div:first-child{flex:1;min-width:0}
/* grid à 3 colonnes (pas 2) depuis l'ajout du bouton ↺ de réinitialisation AVANT chaque slider (demande
   explicite : "mettre devant le slider, pas à la ligne") — sans ça, ce 3e enfant se retrouvait poussé dans
   la 2e colonne (36px, prévue pour .settings-val) à la place du slider lui-même, l'écrasant à une largeur
   quasi nulle (rapporté avec capture : slider devenu très fin). auto pour le bouton (20px fixe, cf. .tbtn)
   au lieu d'une 3e colonne fixe supplémentaire : ne consomme que sa largeur réelle. */
.settings-section[data-settings-tab="appearance"] .settings-row.has-slider>.settings-control{display:grid;grid-template-columns:auto minmax(0,1fr) 36px;align-items:center;gap:8px;width:clamp(180px,32%,320px);flex-shrink:0}
.settings-section[data-settings-tab="appearance"] .settings-row.has-slider>.settings-control:not(.has-tbtn){grid-template-columns:minmax(0,1fr) 36px}
.settings-section[data-settings-tab="appearance"] .settings-row.has-slider .settings-slider{width:100%;min-width:0}
.settings-section[data-settings-tab="appearance"] .settings-val{font-size:max(10px,.52rem);min-width:28px}
.settings-text-input{width:min(240px,100%);box-sizing:border-box;padding:4px 7px;border:1px solid var(--bd);border-radius:var(--r);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.6rem}
.background-file-input{max-width:240px;color:var(--tx2);font-family:var(--fm);font-size:max(10px,.56rem)}
@media(max-width:700px){.settings-section[data-settings-tab="appearance"] .settings-row.has-slider>.settings-control{width:100%}}
/* Champ texte ÉDITABLE (demande explicite : "utiliser un slider avec valeur texte input plutôt que des
   boutons", zoom modules) — .settings-val existant (ci-dessus) est un <span> lecture seule ailleurs dans ce
   panneau ; ce module-ci a besoin d'un vrai <input> pour taper une valeur directement, pas juste la lire.
   Largeur fixe (pas juste min-width) : un input number/text n'a pas de largeur "naturelle" liée à son
   contenu comme un <span>, sans quoi il retomberait sur une largeur par défaut du navigateur (souvent 20
   caractères, bien trop large ici). */
.settings-val-input{font-family:var(--fm);font-size:.62rem;color:var(--tx);background:var(--s2);border:1px solid var(--bd);border-radius:4px;width:40px;padding:3px 4px;text-align:right}
.settings-val-input:focus{outline:none;border-color:var(--c-root)}
/* flex-wrap ajouté (cf. le correctif mobile de .settings-row juste au-dessus) — sans lui, une rangée de
   boutons (jusqu'à 6, ex. Taille du texte) qui passe sur SA PROPRE ligne pleine largeur pouvait quand même
   déborder si elle dépasse cette largeur, puisqu'un flex sans wrap ne réduit jamais le nombre de boutons
   par ligne, seulement leur taille individuelle (jamais souhaité ici, cf. min-width fixe des boutons via
   leur padding) — justify-content:center recentre le dernier rang incomplet plutôt que de le laisser collé
   à gauche. */
.settings-seg{display:flex;flex-wrap:wrap;gap:3px;justify-content:center}
/* BUG CORRIGÉ (demande explicite, avec screenshot : "toujours 2x2 boutons pour régler l'export... comme
   dans résolution - gammes sur le screenshot fourni") — .settings-seg (ci-dessus) est en flex-wrap : le
   nombre de boutons par rangée dépend alors de la largeur RÉELLE disponible (4 en 1 seule ligne sur un
   panneau Paramètres assez large, 2+2 seulement en dessous d'un certain seuil) — même classe de bug déjà
   rencontrée et corrigée sur le mixeur sidebar (desktop vs tiroir mobile) : grid à 2 colonnes FIXES ici
   garantit le même découpage (1×/2× puis 4×/6×) quelle que soit la largeur du panneau. Scopé aux 3
   sélecteurs de résolution PNG (Gammes/Accords/Progressions, toujours exactement 4 boutons) via cette classe
   dédiée plutôt que d'imposer 2 colonnes à TOUS les .settings-seg de l'app (certains ont 2, 6 ou 8 boutons,
   où un grid 2 colonnes fixe serait soit inutile soit inadapté). */
.settings-seg-2x2{display:grid;grid-template-columns:repeat(2,1fr)}
.settings-seg button{padding:3px 8px;border:1px solid var(--bd);border-radius:4px;background:transparent;color:var(--tx2);font-family:var(--fm);font-size:.6rem;cursor:pointer;transition:all var(--tr);-webkit-tap-highlight-color:transparent}
.settings-seg button.active{background:var(--s2);border-color:var(--c-root);color:var(--c-root);font-weight:600}
/* Onglet Paramètres > Raccourcis : une ligne par action rebindable, pastille du binding actuel + bouton
   Modifier. Dérivé de .settings-row/.settings-seg, pas de nouveau système visuel. */
.kbd-row{padding:4px 0;border-bottom:1px solid var(--bd)}
.kbd-row:last-child{border-bottom:none}
/* Colonnes icône/libellé (cf. _kbdActionLabelHtml dans app.js) — même principe que .tab-icon/.tab-label de
   la sidebar : largeur fixe pour la colonne icône, pour que la 1ère lettre de chaque nom de module
   s'aligne verticalement dans toute la liste des raccourcis, quel que soit l'encombrement du glyphe. */
.kbd-row-icon{display:inline-block;width:16px;flex-shrink:0;text-align:center}
.kbd-row-label{margin-left:6px}
.kbd-pill{padding:3px 8px;border-radius:999px;border:1px solid var(--bd);background:var(--s2);color:var(--tx2);font-family:var(--fm);font-size:.6rem;white-space:nowrap;min-width:20px;text-align:center;display:inline-flex;align-items:center;gap:4px}
.kbd-pill-x{border:none;background:transparent;color:var(--tx3);cursor:pointer;font-size:max(10px,.55rem);padding:0;line-height:1;-webkit-tap-highlight-color:transparent}
.kbd-pill-x:hover{color:var(--c-root)}
.kbd-modify{padding:3px 8px;border:1px solid var(--bd);border-radius:4px;background:transparent;color:var(--tx2);font-family:var(--fm);font-size:.6rem;cursor:pointer;transition:all var(--tr);-webkit-tap-highlight-color:transparent}
.kbd-modify:hover{background:var(--s2);border-color:var(--c-root);color:var(--c-root)}
/* Paramètres > Général > Modules visibles : une ligne cliquable par module (toggle), point plein/vide à
   droite plutôt qu'un <input type="checkbox"> — cohérent avec le vocabulaire de pastilles/pilules déjà
   utilisé ailleurs dans les Paramètres (.settings-seg, .kbd-pill) plutôt qu'un widget de formulaire natif. */
/* BUG CORRIGÉ / évolution (demande explicite : "aligner la textinput box avec le bloc module (à droite) et
   agrandir le slider sur l'espace disponible vers la gauche") — 1er jet : bouton module + info + slider+
   champ tous sur UNE seule ligne flex-wrap, le slider figé à 70px de large et le champ texte collé
   immédiatement après le bouton d'info (jamais aligné avec le bord droit du bloc module). Restructuré en 2
   lignes empilées (.modvis-item passe en flex-direction:column) : .modvis-item-row porte le bouton module +
   info (inchangé), .modvis-zoom-row en dessous porte le slider (flex:1, occupe tout l'espace disponible en
   grandissant vers la GAUCHE) + champ texte/% (flex:0 0 auto, ancré à droite) — ce bord droit s'aligne
   naturellement avec le bord droit de .modvis-item-row juste au-dessus puisque les 2 lignes partagent la
   même largeur totale (100% de .modvis-item). */
.modvis-item{display:flex;flex-direction:column;gap:4px;width:100%}
.modvis-item-row{display:flex;align-items:center;gap:6px;width:100%}
.modvis-zoom-row{display:flex;align-items:center;gap:6px;width:100%}
/* BUG CORRIGÉ (rapporté avec screenshot : "aligner la box valeur de zoom avec les blocs de module (bord
   droit)") — "%" intégré DANS la boîte texte via ::after sur ce wrapper (au lieu d'un <span> séparé après le
   champ, cf. l'ancien markup) : le bord droit du wrapper (donc de la boîte visible elle-même, padding-right
   du input élargi pour laisser la place au "%" superposé) touche maintenant le bord droit de la ligne,
   identique au bouton d'info juste au-dessus plutôt que de s'arrêter avant l'espace occupé par un "%" à
   côté. position:relative sur le wrapper + ::after en position:absolute superposé PAR-DESSUS le padding
   droit du input (jamais par-dessus le texte du nombre, cf. le padding-right du input ci-dessous qui laisse
   la place). */
/* CORRECTION (précision explicite : "ça doit être aligné avec la box qui contient le nom du module, pas
   avec le bouton i") — mesuré via DevTools : .modvis-row (la carte nom+icône+pastille du module) s'arrête
   AVANT le bouton .info-btn-catalog (qui vit dans le même .modvis-item-row, séparé par le gap:6px de la
   ligne, lui-même 18px de large + 2px de margin-left). Le bord droit de la ligne ENTIÈRE (bouton info
   inclus) n'est donc PAS le même bord que .modvis-row — c'est ce dernier qu'il faut viser. margin-right
   ci-dessous = largeur réservée par la colonne bouton info (18px + 2px margin + 6px gap = 26px), pour que
   le bord droit de CETTE boîte (val-wrap) retombe exactement sur le bord droit de .modvis-row juste
   au-dessus, plutôt que sur celui du bouton info. */
.modvis-zoom-val-wrap{position:relative;display:inline-flex;flex-shrink:0;margin-right:26px}
.modvis-zoom-val-wrap::after{content:"%";position:absolute;right:6px;top:50%;transform:translateY(-50%);font-family:var(--fm);font-size:.62rem;color:var(--tx3);pointer-events:none}
.modvis-zoom-val-wrap .settings-val-input{padding-right:16px;width:52px}
.modvis-row{display:flex;align-items:center;gap:8px;flex:1;min-width:0;padding:5px 8px;border:1px solid var(--bd);border-radius:4px;background:var(--pbg);color:var(--tx2);font-family:var(--fm);font-size:.62rem;cursor:pointer;transition:all var(--tr);-webkit-tap-highlight-color:transparent;text-align:left}
.modvis-row.active{color:var(--tx)}
.modvis-row:not(.active){opacity:.55}
.modvis-icon{width:16px;text-align:center;flex-shrink:0}
.modvis-label{flex:1}
.modvis-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;border:1px solid var(--bd2);background:transparent;transition:all var(--tr)}
.modvis-row.active .modvis-dot{background:var(--c-root);border-color:var(--c-root)}
.settings-footer{padding:10px 16px;border-top:1px solid var(--bd);display:flex;align-items:center;gap:8px;flex-shrink:0;flex-wrap:wrap}
.settings-footer button{flex:1 1 0;min-width:80px;padding:7px;border-radius:var(--r);border:1px solid var(--bd);background:transparent;color:var(--tx2);font-family:var(--fm);font-size:.62rem;cursor:pointer;white-space:nowrap}
.settings-footer button.primary{background:var(--c-root);border-color:var(--c-root);color:#111;font-weight:600}
/* Badge de version (cf. #ftVersionBadge/_ftRenderVersionBadge dans app.js) — flex:0 (ne concurrence pas la
   largeur des boutons, qui restent égaux entre eux comme avant) et discret (petit, couleur atténuée) : une
   info qu'on va chercher, pas un élément d'interface à mettre en avant.
   flex-basis:100% sur mobile (cf. media query plus bas) : avec 4 boutons + ce badge sur une seule ligne
   (ajout de "Vider le cache", cf. #ftForceClearBtn), un écran étroit forçait soit un retour à la ligne
   au milieu des boutons (mesuré : "Exporter"/"Importer" se repliaient sur 2 lignes chacun, illisibles),
   soit un espacement trop serré entre eux. Le badge prend sa propre ligne, les boutons restent groupés. */
.settings-version-badge{flex:0 0 auto;font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx3);white-space:nowrap;padding-right:4px}
@media(max-width:520px){
  .settings-version-badge{flex-basis:100%;text-align:right;padding-right:0}
  /* white-space:normal (pas nowrap, cf. règle de base) — BUG CORRIGÉ (rapporté : "le texte déborde du
     bouton sur Chrome, alors que sur Brave il prend 2 lignes ce qui est préférable") : "Vider le cache"
     (le plus long des 4 libellés) dépassait la largeur réelle du bouton en nowrap forcé — mesuré : 93px de
     texte pour 82,5px de bouton. Le repli sur 2 lignes que Brave affichait déjà (differences de moteur de
     rendu de texte entre navigateurs, mais le CSS demandait bien nowrap partout) devient le comportement
     standard voulu, plutôt qu'un accident de rendu propre à un seul navigateur. */
  .settings-footer button{min-width:0;font-size:max(10px,.58rem);padding:7px 4px;white-space:normal;line-height:1.25}
}

/* Bannière de mise à jour (cf. _ftShowUpdateBanner dans app.js) — injectée dans le DOM seulement quand une
   nouvelle version est réellement détectée en attente (au plus 1 fois par déploiement pour un onglet resté
   ouvert à travers) : position fixe en bas, au-dessus de tout le reste (z-index élevé, même famille que les
   modales), mais jamais bloquante — pas d'overlay derrière, l'app reste utilisable pendant qu'elle est
   affichée, contrairement à settings-modal qui capture le clic. */
#ftUpdateBanner{
  position:fixed;left:50%;bottom:16px;transform:translateX(-50%);z-index:9999;
  display:flex;align-items:center;gap:10px;
  background:var(--s2);border:1px solid var(--bd);border-radius:var(--r);
  padding:8px 10px 8px 14px;box-shadow:0 4px 18px rgba(0,0,0,.35);
  font-family:var(--fm);font-size:.68rem;color:var(--tx);
  max-width:calc(100vw - 24px);
  animation:ftUpdateBannerIn .25s ease both;
}
@keyframes ftUpdateBannerIn{from{opacity:0;transform:translateX(-50%) translateY(12px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
#ftUpdateBanner button{
  font-family:var(--fm);font-size:.62rem;border-radius:calc(var(--r) - 2px);cursor:pointer;
  padding:6px 12px;border:1px solid var(--c-root);background:var(--c-root);color:#111;font-weight:600;white-space:nowrap;
}
#ftUpdateBanner button.ft-update-dismiss{
  background:transparent;border-color:var(--bd);color:var(--tx3);padding:6px 9px;font-weight:400;
}
@media(max-width:480px){
  #ftUpdateBanner{left:12px;right:12px;transform:none;bottom:12px;max-width:none}
  @keyframes ftUpdateBannerIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
}
/* CHORDS */
.chords-top{display:flex;flex-direction:column;flex-shrink:0;background:var(--sf)}
.chords-top .rrow{padding:3px 4px;border-bottom:1px solid var(--bd)}
.chords-main{flex:1;overflow:auto;padding:6px;display:flex;flex-direction:column;gap:6px;min-width:0}
.chord-3d-panel{position:relative;flex:0 0 auto;overflow:hidden;border:1px solid var(--bd2);border-radius:var(--rl);background:linear-gradient(145deg,var(--s2),var(--sf));box-shadow:0 8px 28px var(--sh)}
.chord-3d-panel[hidden]{display:none}
.chord-3d-head{min-height:38px;padding:5px 8px;display:flex;align-items:center;justify-content:space-between;gap:10px;border-bottom:1px solid var(--bd);background:color-mix(in srgb,var(--sf) 88%,transparent)}
.chord-3d-heading{min-width:0;display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
.chord-3d-heading strong{font-family:var(--fb);font-size:.72rem;color:var(--tx)}
.chord-3d-title{min-width:0;color:var(--tx3);font-family:var(--fm);font-size:max(10px,.52rem);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.chord-3d-actions{display:flex;align-items:center;gap:5px;flex-shrink:0}
.chord-3d-reset,.chord-3d-close{height:26px;border:1px solid var(--bd2);border-radius:999px;background:var(--s3);color:var(--tx2);font:700 max(10px,.5rem) var(--fm);cursor:pointer;transition:color var(--tr),border-color var(--tr),background var(--tr)}
.chord-3d-reset{padding:0 9px}
.chord-3d-close{width:26px;padding:0;font-size:.62rem}
.chord-3d-reset:hover,.chord-3d-close:hover{color:var(--c-root);border-color:var(--c-root);background:var(--s2)}
.chord-3d-stage{position:relative;width:100%;height:clamp(280px,42vh,440px);overflow:hidden;cursor:grab;touch-action:none;outline:none;background:radial-gradient(circle at 48% 30%,color-mix(in srgb,var(--bd2) 38%,transparent),transparent 68%)}
.chord-3d-stage.is-dragging{cursor:grabbing}
.chord-3d-stage canvas{display:block;width:100%;height:100%}
.chord-3d-error{position:absolute;inset:0;display:grid;place-items:center;margin:0;padding:20px;color:var(--tx3);font:600 .65rem var(--fm);text-align:center}
.chord-3d-help{min-height:30px;padding:5px 9px;display:flex;align-items:center;justify-content:center;gap:13px;flex-wrap:wrap;border-top:1px solid var(--bd);color:var(--tx3);font:400 max(10px,.48rem) var(--fm)}
.chord-3d-help b{color:var(--tx2)}
.chord-3d-help-dot{display:inline-block;width:9px;height:9px;margin-right:3px;border-radius:50%;background:var(--c-root);vertical-align:-1px}
/* Compense la hauteur de la poignée repliée de #chStackBuilder (position:fixed en mobile, cf. plus haut)
   pour que le dernier diagramme ne soit jamais masqué derrière — quand l'utilisateur déplie, la carte
   grandit par-dessus le contenu (overlay), acceptable ponctuellement (ombre portée + fond opaque signalent
   l'état flottant). Placé après la règle de base .chords-main{padding:6px} pour gagner la cascade (même
   spécificité, mais padding-bottom seul doit survivre au raccourci padding déclaré juste au-dessus). */
@media(max-width:1023px){
  .chords-main{padding-bottom:60px}
}
.crg{display:flex;flex-wrap:wrap;gap:2px}
.crb{min-width:24px;height:24px;padding:0 3px;border-radius:4px;border:1px solid var(--bd);background:var(--pbg);color:var(--tx2);font-family:var(--fm);font-size:.6rem;cursor:pointer;transition:all var(--tr);display:flex;align-items:center;justify-content:center}
.crb:hover{border-color:var(--c-root);color:var(--tx)}
.crb.active{background:var(--c-root);border-color:var(--c-root);color:#111;font-weight:700}
.cti{padding:2px 7px;border-radius:4px;border:1px solid transparent;font-family:var(--fb);font-size:.6rem;color:var(--tx2);cursor:pointer;transition:all var(--tr);white-space:nowrap;flex-shrink:0;display:inline-block}
.cti:hover{background:var(--s2);color:var(--tx)}
.cti.active{background:var(--s2);border-color:var(--c-root);color:var(--c-root);font-weight:600}
.cti-sym{display:none}
/* min-height:1 ligne (basé sur 1.5rem, la plus grande police présente ici via .cr-root). Avant : 2 lignes
   réservées pour éviter que la grille "saute" quand un accord riche wrappe ; sur écran large la rangée tient
   sur 1 ligne et la 2e était du vide. Sur écran étroit elle peut wrapper (et donc décaler la grille) : seule
   la place réservée à vide a été retirée. */
.cr-header{display:flex;align-items:center;gap:6px;flex-wrap:wrap;line-height:1.3;padding:2px 0;min-height:calc(1.5rem*1.3)}
.cr-root{font-family:var(--fd);font-size:1.5rem;font-weight:800;line-height:1;flex-shrink:0}
.cr-qual{font-family:var(--fd);font-size:.95rem;font-weight:700;color:var(--tx2);flex-shrink:0}
.cr-send-prog{margin-left:auto;padding:3px 9px;border-radius:999px;border:1px solid var(--bd);background:var(--s2);color:var(--tx2);font-family:var(--fm);font-size:.6rem;font-weight:600;cursor:pointer;transition:all var(--tr);white-space:nowrap;flex-shrink:0}
.cr-send-prog:hover{border-color:var(--c-root);color:var(--c-root)}
.vgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,max-content));gap:10px;justify-content:center}
.vcard{background:var(--sf);border:1px solid var(--bd);border-radius:var(--r);padding:4px 6px 32px;display:flex;flex-direction:column;align-items:center;gap:4px;transition:border-color var(--tr),box-shadow var(--tr);min-width:0;width:100%;overflow:visible;box-sizing:border-box;-webkit-tap-highlight-color:transparent;position:relative}
.vcard:hover{border-color:var(--bd2);box-shadow:0 3px 10px var(--sh)}
/* z-index:5 — sans lui, une carte "expanded" (grid-column:1/-1, donc bien plus large) restait au même
   niveau d'empilement que ses voisines : selon l'ordre de rendu DOM, une carte suivante pouvait se
   retrouver visuellement par-dessus une partie de la carte agrandie (chevauchement constaté en usage —
   cf. capture montrant "Forme A (barré)" partiellement recouverte par la légende Extensions à droite). */
.vcard.expanded{grid-column:1/-1;border-color:var(--c-root);box-shadow:0 4px 16px var(--sh);z-index:5}
canvas.vc{display:block;flex-shrink:0}
/* padding latéral : réserve la place des boutons ⤢ (gauche) et ♪/⤓/⇱ (droite), positionnés en absolu
   par-dessus la carte (.vcard-zoom/.vcard-play) — sans lui, le titre centré (ex. "Forme A (ouverte)")
   passe sous ces boutons sur les cartes étroites mobiles (bug rapporté : bouton zoom peu lisible/mal
   aligné, en fait le texte du titre le chevauchait). 30px de chaque côté = diamètre bouton (22px) +
   inset (2-10px selon largeur d'écran) + marge de respiration. */
/* min-height:2 lignes (2*font-size*line-height) — sans ça, un label tenant sur 1 ligne ("Forme E (barré)")
   rend sa carte plus basse qu'un label plus long qui wrap sur 2 lignes ("Forme C (ouverte)"), cassant
   l'alignement horizontal des cartes dans la grille (bug rapporté : hauteurs de cartes inégales entre
   formes ouvertes/barrées d'un même accord). Toutes les cartes réservent désormais la même hauteur de
   label, qu'il occupe 1 ou 2 lignes. */
.vlabel{font-family:var(--fm);font-size:.68rem;font-weight:600;color:var(--tx2);text-align:center;line-height:1.3;min-height:calc(.68rem*1.3*2);display:flex;align-items:center;justify-content:center;padding-bottom:2px;padding-left:30px;padding-right:30px;width:100%;box-sizing:border-box}
/* Contour + picto renforcés (var(--tx2) au lieu de var(--bd2)/var(--tx3)) pour une meilleure lisibilité au
   repos, pas seulement au survol — demande explicite, ce bouton (♪ lecture d'accord/gamme, réutilisé aussi
   par .vcard-midi/.vcard-png/.vcard-info/.diat-play) était jugé trop discret par défaut. */
/* top:26px (pas 2px) — sous le titre (.vlabel), pas à sa hauteur : au même niveau, le bouton rond
   chevauchait visuellement le texte du titre (bug rapporté). .vcard a aussi vu son padding-top réduit
   (8px→4px) pour que le titre remonte vers le bord haut de la carte, l'espace ainsi libéré au-dessus
   servant désormais aux boutons plutôt qu'à un simple espacement. */
/* font-family explicite (var(--fm), même famille que .vcard-midi/.diat-play) — sans elle, .vcard-play
   hérite de la police du conteneur dans lequel il est posé, qui varie selon le contexte (ex. .srow-name-h
   impose var(--fd), une police "display" plus large avec descendante). Le glyphe ♪ (U+266A) est alors
   rendu via un fallback de police différent selon l'ancêtre, avec une largeur/hauteur visuellement très
   différente (mesuré : ~54% plus large + descendante visible via --fd contre --fb) — bug rapporté : le
   bouton lecture des gammes compatibles de l'Analyseur avait un picto nettement plus gros que celui des
   accords détectés, alors que les deux partagent exactement la même classe/taille de boîte. */
.vcard-play{position:absolute;top:26px;right:5px;width:22px;height:22px;border-radius:50%;background:var(--s3);border:1.5px solid var(--tx2);color:var(--tx2);font-size:.6rem;font-family:var(--fm);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all var(--tr);z-index:10;-webkit-tap-highlight-color:transparent;line-height:1}
.vcard-3d{position:absolute;bottom:5px;left:50%;transform:translateX(-50%);height:20px;min-width:34px;padding:0 7px;border-radius:10px;background:var(--s3);border:1.5px solid var(--tx2);color:var(--tx2);font:700 max(10px,.5rem) var(--fm);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all var(--tr);z-index:10;-webkit-tap-highlight-color:transparent;line-height:1}
.vcard-3d:hover{background:var(--s2);border-color:var(--c-root);color:var(--c-root)}
.vcard-3d.active{background:var(--c-root);border-color:var(--c-root);color:#111}
/* @media(hover:hover) : scope la règle aux VRAIS dispositifs pointés à la souris — sans ce garde-fou, un tap
   sur mobile pouvait laisser un état :hover fantôme "collé" indéfiniment sur le bouton jusqu'au prochain
   tap ailleurs sur la page (bug rapporté : bouton lecture d'un accord diatonique resté coloré en accent
   après un simple tap, uniquement sur mobile). Même précaution déjà appliquée ailleurs dans l'app pour
   .az-scale-row/preview au survol, cf. commentaire de azSuggestScales. */
@media(hover:hover){.vcard-play:hover{background:var(--s2);border-color:var(--c-root);color:var(--c-root)}}
/* Bouton zoom — même style circulaire que .vcard-play, coin opposé (haut-gauche). Remplace l'ancien
   comportement "clic n'importe où sur la carte = zoom" (cf. app.js makeZoomBtn). Base (mobile, <1300px) :
   mêmes top/inset que .vcard-play (top:2px;right:5px) en miroir — l'override desktop juste au-dessus
   (@media min-width:1300px) applique ensuite le même top:8px;left:10px que .vcard-play y reçoit
   (top:8px;right:10px), pour rester symétrique aux deux tailles d'écran plutôt qu'à une seule. */
/* transform:translateY — le glyphe ⤢ (police DM Sans par défaut, pas Space Mono comme ♪/.vcard-play) a son
   encre visuelle décentrée d'env. 2px vers le haut dans sa boîte de caractère (mesuré par centroïde de
   pixels sur canvas, cf. session de centrage des glyphes du Cercle des quintes) — sans cette compensation
   il paraît nettement plus haut que le bouton ♪ symétrique (haut-droite), pourtant aligné avec les mêmes
   top/right en CSS. */
.vcard-zoom{position:absolute;top:26px;left:5px;width:22px;height:22px;border-radius:50%;background:var(--s3);border:1.5px solid var(--tx2);color:var(--tx2);font-size:.68rem;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all var(--tr);z-index:10;-webkit-tap-highlight-color:transparent;line-height:1}
.vcard-zoom span{display:block;transform:translateY(0px)}
.vcard-zoom:hover{background:var(--s2);border-color:var(--c-root);color:var(--c-root)}
.vcard.expanded .vcard-zoom{background:var(--c-root);border-color:var(--c-root);color:#111}
.vcard-play.playing{background:#fff!important;border-color:#fff!important;color:#111!important}
.vcard-midi{position:absolute;top:auto;bottom:5px;right:5px;width:auto;height:auto;border-radius:10px;padding:0 6px;font-size:max(10px,.5rem);font-family:var(--fm);font-weight:700}
/* Barre d'actions Progressions (Texte/Pool) sur mobile : boutons tactiles plus grands et espacés
   (les styles inline height:22px/font-size:max(10px,.5rem) sont dimensionnés pour desktop) */
@media(max-width:1023px){
  #genActions{gap:8px 10px!important}
  #genActions .vcard-play{height:34px!important;font-size:.68rem!important;padding:0 12px!important;border-radius:17px!important}
  #genActions #genPlayBtn{width:34px!important;padding:0!important}
}
.vcard-png{position:absolute;top:auto;bottom:5px;left:5px;right:auto;width:auto;height:auto;border-radius:10px;padding:0 6px;font-size:max(10px,.5rem);font-family:var(--fm);font-weight:700}
/* Bouton "i" par carte, vue "Tous" d'Accords uniquement — centré en bas, entre PNG (gauche) et MIDI (droite). */
.vcard-info{position:absolute;bottom:5px;left:50%;transform:translateX(-50%);width:18px;height:18px;border-radius:50%;background:var(--s3);border:1.5px solid var(--tx2);color:var(--tx2);font-size:max(10px,.56rem);font-weight:700;font-family:var(--fm);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all var(--tr);z-index:10;-webkit-tap-highlight-color:transparent}
.vcard-info:hover{background:var(--s2);border-color:var(--c-root);color:var(--c-root)}
@keyframes vcardPulse{0%{transform:scale(1)}25%{transform:scale(1.04)}70%{transform:scale(.98)}100%{transform:scale(1)}}
.vcard.playing{animation:vcardPulse 200ms ease both}
@keyframes az-play-pulse{0%{box-shadow:0 0 0 0 rgba(255,107,0,.6)}70%{box-shadow:0 0 0 10px rgba(255,107,0,0)}100%{box-shadow:0 0 0 0 rgba(255,107,0,0)}}
/* Bouton "〜 Drone" du module Gammes pendant la lecture : pulsation continue (contrairement à az-play-pulse qui
   ne joue qu'une fois au clic) pour signaler que le drone tourne en fond sans que l'utilisateur ait quitté Gammes. */
/* droneBtnPulse : même correctif que sealPulse plus haut (cf. audit performance) — anneau expansif en
   transform+opacity au lieu de box-shadow animé (repaint continu à 60fps sinon). color-mix recalculé à
   chaque frame par le navigateur (valeur fixe, jamais interpolée par l'animation) est aussi évité en
   posant la couleur une fois sur border-color plutôt que dans le keyframe lui-même. */
@keyframes droneBtnPulse{0%{transform:scale(1);opacity:.55}100%{transform:scale(1.6);opacity:0}}
.rbtn-underline.drone-active{color:var(--c-root);border-bottom-color:var(--c-root);font-weight:700;position:relative}
.rbtn-underline.drone-active::after{
  content:'';position:absolute;inset:2px 6px;border-radius:4px;border:1.5px solid var(--c-root);
  pointer-events:none;animation:droneBtnPulse 1.6s ease-out infinite;
}

/* Panneau repliable "Tableau de référence" (ex-module Table, fusionné dans Gammes/Accords) — même aspect
   "plein" que l'empileur d'intervalles d'Accords (#chStackBuilder/.ch-stack-handle, cf. sa propre section
   plus bas dans ce fichier) : fond+bordure sur le conteneur entier (pas juste un texte discret), poignée
   cliquable en pleine largeur avec padding généreux, chevron qui pivote en CSS plutôt qu'un glyphe changé
   en JS (demande explicite avec capture d'écran : "donner au menu replié un aspect plus 'plein', à la
   manière de l'empileur d'intervalles... quand il est replié"). */
.ref-table-panel{background:var(--sf);border:1px solid var(--bd);border-radius:var(--r);box-sizing:border-box;margin:8px 2px 4px;min-width:0}
.ref-table-handle{display:flex;align-items:center;gap:8px;width:100%;padding:8px 12px;background:transparent;border:none;color:var(--tx2);font-family:var(--fm);font-size:.62rem;cursor:pointer;text-align:left;-webkit-tap-highlight-color:transparent}
.ref-table-handle:hover{color:var(--tx)}
.ref-table-chevron{font-family:var(--fm);font-size:.7rem;color:var(--tx3);transition:transform var(--tr);flex-shrink:0}
.ref-table-panel.expanded .ref-table-chevron{transform:rotate(180deg)}
.ref-table-body{display:none;border-top:1px solid var(--bd)}
.ref-table-panel.expanded .ref-table-body{display:block}
.ref-table-panel.ref-deferred .ref-table-body{display:none} /* 1re image de Gammes/Accords, cf. switchTab */
/* Audit perf 2026-09-24 : tableaux dépliés par défaut, en bas de Gammes/Accords (hors écran à l'arrivée sur
   l'onglet) — leur mise en page/peinture (400-600 éléments) est différée tant qu'ils ne sont pas visibles.
   contain-intrinsic-size garde une hauteur réaliste pour la barre de défilement. */
#page-scales .ref-table-panel.expanded .ref-table-body,#page-chords .ref-table-panel.expanded .ref-table-body{content-visibility:auto;contain-intrinsic-size:auto 700px}
/* SCALE TABLE */
/* padding-top volontairement à 0 : le th sticky se cale sur le bord haut de .stable-wrap (sa "top edge"),
   un padding-top créerait un espace vide entre ce bord et l'en-tête flottant pendant le scroll — le contenu
   du tableau resterait visible par-dessus le padding, juste au-dessus du header, nuisant à la lisibilité.
   min-width:0 (demande explicite avec capture : le tableau débordait de la largeur du corps du module) —
   .stable-wrap est un flex item dans un ancêtre column (.scales-main/.chords-main/.ref-table-panel), qui
   par défaut ne rétrécit JAMAIS sous la largeur intrinsèque de son contenu (min-width:auto implicite de
   tout flex item) ; .theory-table{width:100%} ci-dessous n'a alors plus aucun effet concret puisque le
   PARENT s'est déjà élargi pour accueillir le tableau à sa taille naturelle avant que ce width:100% ne
   s'applique — min-width:0 rétablit le rétrécissement normal, laissant width:100%+overflow:auto faire
   leur travail (le tableau se limite à la largeur réelle disponible, avec un scroll horizontal interne
   si son contenu (nombreuses colonnes d'intervalles) ne peut décidément pas tenir dans cette largeur). */
/* padding horizontal RETIRÉ (demande explicite : "j'aimerais supprimer cette marge" — le tableau laissait
   une petite marge interne visible dans le panneau replié de Gammes/Accords) : le tableau occupe désormais
   toute la largeur réelle de .ref-table-body, jusqu'à ses bords. padding-bottom gardé (espace sous la
   dernière ligne avant le bord du panneau, purement vertical, aucun rapport avec la marge signalée). */
.stable-wrap{flex:1;overflow:auto;padding:0 0 .5rem;min-width:0}
.theory-table{border-collapse:collapse;width:100%;font-size:.72rem}
/* outline (pas affecté par border-collapse, contrairement à border) en plus de la bordure normale : comble
   les interstices d'1px entre th adjacents par lesquels le contenu défilant devenait visible en transparence
   pendant le scroll — border-collapse fusionne les bordures des cellules et ne les fait pas suivre le th sticky.
   ::after opaque sous le th (au lieu d'un simple box-shadow, qui ne bloque pas le contenu et laissait passer
   la ligne suivante en transparence à certains taux de défilement) : couvre tout interstice sous-pixel entre
   le bas du th sticky et la première ligne du tbody qui défile dessous. */
.theory-table th{background:var(--s2);border:1px solid var(--bd);outline:1px solid var(--s2);padding:.45rem .3rem;text-align:center;font-family:var(--fm);font-weight:700;color:var(--tx);position:sticky;top:0;z-index:10}
.theory-table th::after{content:'';position:absolute;left:0;right:0;top:100%;height:3px;background:var(--s2);box-shadow:0 2px 0 0 var(--bd2)}
.theory-table td{border:1px solid var(--bd);padding:.35rem .25rem;font-family:var(--fm);text-align:center}
.theory-table tbody tr:nth-child(odd) td{background:var(--s2)}
.theory-table tbody tr:hover td{background:var(--s3)}
.row-label{font-family:var(--fb)!important;font-size:.72rem;text-align:left!important;padding-left:.6rem!important;white-space:normal;font-weight:600;color:var(--tx);width:7.5rem;max-width:7.5rem}
.cell-root{font-weight:700}
.cell-empty{color:var(--tx3)}

@media(max-width:1023px){
  /* Table : resserrer colonnes/police pour éviter le débordement horizontal sur mobile */
  .stable-wrap{padding:0 0 .2rem}
  .theory-table{font-size:max(10px,.56rem)}
  /* En-tête d'intervalles flottant sur mobile : sur desktop, .pages-container borne la hauteur et .stable-wrap
     devient son propre conteneur défilant (cf. règle desktop plus haut), ce qui active position:sticky sur les
     th. Sur mobile, rien ne borne .page/.stable-wrap (body ne fait que min-height:100vh) — c'est TOUTE la page
     qui défile au niveau du document, donc .stable-wrap ne déborde jamais de lui-même et sticky ne s'active
     jamais. On force ici .stable-wrap à devenir son propre conteneur défilant borné à la hauteur visible
     restante (viewport moins topbar), uniquement pour ce module — pas touché ailleurs pour ne pas casser le
     défilement naturel des autres pages mobiles.
     var(--rh-top) est déjà pilotée en JS pour le module Rythme (mesure réelle de .topbar) — réutilisée telle
     quelle ici plutôt que dupliquée sous un autre nom, voir _rhSyncTopOffset()/stSyncTopOffset(). */
  #page-scaletable.page.active{
    position:fixed;left:0;right:0;bottom:0;top:var(--rh-top,96px);
    overflow:hidden;z-index:5;
  }
  #page-scaletable .stable-wrap{height:100%;-webkit-overflow-scrolling:touch}
  .theory-table th{padding:.3rem .1rem}
  .theory-table td{padding:.22rem .08rem}
  .row-label{width:4.6rem;max-width:4.6rem;font-size:max(10px,.56rem);padding-left:.3rem!important}
}

/* INTERVALLES */
.rbtn-underline{min-width:26px;height:26px;padding:0 6px;border-radius:0;border:none;border-bottom:2px solid transparent;background:transparent;color:var(--tx2);font-family:var(--fm);font-size:.6rem;cursor:pointer;transition:all var(--tr)}
.rbtn-underline.active{color:var(--c-root);border-bottom-color:var(--c-root);font-weight:700}
/* rh-generate-btn (Strum "✦ Générer") : bouton à CONTOUR indépendant (demande explicite, 2e itération —
   1er essai en .rbtn-underline avec soulignement retiré ensuite : "retirer l'effet de soulignage et plutôt
   mettre un contour"), plus .rbtn-underline du tout (sa sémantique de toggle actif/inactif ne convient pas
   à une action momentanée sans état à refléter). Gabarit repris de #page-rhythm .rh-select (mêmes voisins
   de la subbar Strum, cf. index.html) mais couleur d'accent thème plutôt que neutre, pour le distinguer
   comme l'action "spéciale" de la rangée plutôt qu'un réglage ordinaire. Effet de clic scale(.88), même
   principe que .tbtn/.pill ailleurs dans l'app. */
.rh-generate-btn{height:26px;padding:0 8px;border-radius:var(--r);border:1px solid var(--c-root);background:transparent;color:var(--c-root);font-family:var(--fm);font-size:.62rem;font-weight:600;cursor:pointer;outline:none;transition:all var(--tr)}
.rh-generate-btn:hover{background:color-mix(in srgb, var(--c-root) 12%, transparent)}
.rh-generate-btn:active{transform:scale(.88);transition:transform 60ms}
/* Switch vitesse d'arpège (Accords/Progressions/Structures, #chArpSpeedBtn & co) : contrairement au texte
   nu .rbtn-underline (illisible comme bouton quand un seul état de texte est visible à la fois, cf. retour
   utilisateur), une vraie pilule avec fond/bordure + glyphe qui change selon l'état — pas besoin d'étiquette
   "Arpège" fixe à côté pour comprendre que c'est cliquable. Pas d'accent couleur thème ici (contrairement à
   .rbtn-underline.active) : rapide/lent est un choix, pas un réglage actif/inactif — les deux états portent
   le même style neutre, seuls le glyphe et le texte changent. */
/* flex-shrink:0 + white-space:nowrap sur le label : dans une subbar mobile (flex-wrap:nowrap;overflow-x:auto,
   cf. Cercle des quintes), ce bouton se retrouvait pressé en dessous de sa largeur naturelle — son label
   ("Arpège rapide"/"Arpège lent") retombait alors à la ligne (2 lignes), gonflant la hauteur du bouton donc
   de toute la subbar ("trop épaisse pour rien", constaté). Le rôle de overflow-x:auto sur la rangée est
   justement de faire défiler horizontalement plutôt que de comprimer le contenu — sans flex-shrink:0 ce
   comportement ne s'appliquait pas à ce bouton précis. */
.arp-toggle{display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 8px;border-radius:999px;border:1px solid var(--bd);background:var(--s2);color:var(--tx2);font-family:var(--fm);font-size:max(10px,.58rem);cursor:pointer;transition:all var(--tr);flex-shrink:0;white-space:nowrap}
.arp-toggle:hover{border-color:var(--c-root);color:var(--tx)}
.arp-toggle .arp-glyph{font-size:.72rem;line-height:1}

/* INSPIRATION */
.insp-card{background:var(--s2);border-radius:var(--rl);border:1px solid var(--bd);padding:10px;display:flex;flex-direction:column;gap:8px}
.insp-card-title{font-family:var(--fd);font-size:.9rem;font-weight:700;color:var(--tx);padding-bottom:6px;border-bottom:1px solid var(--bd);display:flex;align-items:center;gap:8px}
.insp-pool-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(88px,1fr));gap:6px}
/* Mode libre (12 fondamentales × 7 qualités, ordre généré fondamentale par fondamentale — cf. inspGetPoolChords)
   sur desktop : 14 colonnes fixes = exactement 2 fondamentales par ligne, plutôt que le auto-fill générique qui
   caserait un nombre de cartes par ligne dépendant de la largeur d'écran sans rapport avec ce groupement. */
@media(min-width:1024px){
  .insp-pool-grid.insp-pool-grid-free{grid-template-columns:repeat(14,1fr)}
}
.insp-chord-card{position:relative;touch-action:pan-y;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent;cursor:grab;background:var(--sf);border:1px solid var(--bd);border-radius:var(--r);padding:8px 6px;display:flex;flex-direction:column;align-items:center;gap:2px;transition:border-color var(--tr),opacity var(--tr)}
.insp-chord-card:hover{border-color:var(--card-col,var(--bd2))}
.insp-chord-card.insp-dragging{opacity:.05}
.insp-chord-card.playing{border-color:var(--card-col,var(--c-root));box-shadow:0 0 0 2px var(--card-col,var(--c-root)) inset}
.insp-chord-name{font-family:var(--fb);font-weight:700;font-size:.78rem;color:var(--card-col,var(--tx))}
.insp-chord-degree{font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3)}
/* Accords d'emprunt (hors gamme) : toujours affichés dans la banque désormais (plus de case à cocher),
   distingués par une bordure basse épaissie plutôt qu'un badge texte (plus discret, lisible d'un coup d'œil
   sans ajouter de texte dans chaque carte). */
.insp-chord-card.insp-chord-borrowed{border-bottom-width:3px;border-bottom-color:var(--card-col,var(--c-root))}
.insp-drag-ghost{position:fixed;pointer-events:none;z-index:9999;border-radius:var(--r);display:flex;align-items:center;justify-content:center;font-family:var(--fb);font-weight:700;color:#111;opacity:.5;box-shadow:0 4px 14px rgba(0,0,0,.35)}
/* background explicite (--bg, pas hérité) : #inspDropZone (Banque) est posé directement sur le fond de page
   (--bg, plus sombre) alors que #genTitle (Texte) est niché dans .prog-preview-card (--s2, plus clair) — sans
   ce fond propre, la même classe rendait visuellement différente selon son conteneur ambiant. */
.insp-drop-zone{position:relative;background:var(--bg);border:2px dashed var(--bd);border-radius:var(--rl);min-height:64px;padding:8px;transition:background var(--tr),border-color var(--tr)}
.insp-drop-zone.insp-drop-hover{border-color:var(--c-root);background:rgba(255,107,0,.08)}
.insp-drop-zone.insp-drop-empty .insp-drop-list::before{content:attr(data-empty-text);color:var(--tx3);font-family:var(--fm);font-size:.62rem}
.insp-drop-list{position:relative;display:flex;flex-wrap:wrap;align-items:flex-start;gap:8px;min-height:24px}
.insp-midi-badge{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx3);padding:3px 7px;border:1px solid var(--bd);border-radius:999px;white-space:nowrap}
.insp-midi-badge.insp-midi-off{color:var(--tx3);opacity:.6}
.prog-preview-card{border:1px solid var(--bd);border-radius:var(--r);background:var(--s2);padding:8px}
.prog-block{position:relative;display:inline-flex;align-items:center;gap:1px;touch-action:pan-y;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent;cursor:grab;transition:opacity var(--tr)}
.prog-block.insp-dragging{opacity:.05}
.prog-block-label{margin:0 2px;color:var(--card-col,var(--tx))}
.prog-block-card{background:var(--sf);border:1px solid var(--bd);border-radius:var(--r);padding:8px 20px 8px 10px;transition:border-color var(--tr),opacity var(--tr)}
.prog-chord-card{touch-action:pan-y;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent}
.prog-chord-card.insp-dragging{opacity:.05}
/* Mini-cartes de "Choix de la forme" (#genFormsGrid) : jamais réordonnables (contrairement à .prog-chord-card),
   donc aucun JS ne dispute le geste ici — pan-x pan-y laisse le navigateur choisir l'axe natif selon la direction
   réelle du doigt : scroll horizontal de la rangée de voicings, ou scroll vertical de la page à travers la carte.
   pan-x seul (version précédente) ne réservait que l'axe horizontal et bloquait tout scroll vertical démarré
   sur une carte, y compris pour atteindre le voicing suivant plus bas dans la page. */
.prog-form-card{touch-action:pan-x pan-y;-webkit-tap-highlight-color:transparent}
.prog-block-card:hover{border-color:var(--card-col,var(--bd2))}
.prog-block-card .prog-block-inner{display:flex;flex-direction:column;align-items:center;gap:2px}
.prog-block-card .prog-block-label{margin:0;font-family:var(--fb);font-weight:700;font-size:.78rem}
.prog-block-card .insp-chord-degree{display:block;min-height:1em}
.prog-block-card .gen-chord-del{position:absolute;top:3px;right:3px;margin-left:0}
.prog-drop-insert-line{position:absolute;top:4px;bottom:4px;width:3px;margin-left:-1px;background:var(--c-root);border-radius:2px;pointer-events:none;z-index:30;box-shadow:0 0 6px var(--c-root)}
.str-chord-pill{cursor:grab;touch-action:pan-y;user-select:none;-webkit-user-select:none;-webkit-tap-highlight-color:transparent;transition:opacity var(--tr)}
.str-chord-pill.insp-dragging{opacity:.2}
.str-chord-pill.str-chord-selected{outline:2px solid var(--c-root);outline-offset:1px;border-radius:4px}
.str-selection-rect{position:absolute;border:1px dashed var(--c-root);background:color-mix(in srgb,var(--c-root) 12%,transparent);pointer-events:none;z-index:6}
/* Lecteur de tab animé (Accompagnement, cf. app.js strTabReaderRenderStrip) — refonte visuelle "type
   Songsterr" (demande explicite) : vraies lignes de corde CONTINUES (une ligne = un ::after sur chaque
   .str-tabreader-row) avec les numéros de case posés dessus en pastilles opaques qui masquent localement
   le trait, plutôt que des cellules encadrées façon tableur. Grid CSS (pas flex) sur chaque ligne pour que
   TOUTES les lignes (strum, 6 cordes, effets) partagent exactement les mêmes colonnes — même leçon que la
   grille de tempo du Métronome cette session (flex indépendant par ligne ne garantit pas l'alignement).
   .str-tabreader-outer est un flex row à 2 enfants (labels sticky + zone de contenu) ; le SEUL élément qui
   scrolle est #strTabReaderBox lui-même (overflow-x:auto posé en JS) — la zone de contenu n'est qu'un
   conteneur de largeur fixe (nCols*30px) qui déborde de box, pas un scroll imbriqué. */
#page-structures .str-tabreader-outer{display:flex;align-items:stretch}
#page-structures .str-tabreader-labels{flex-shrink:0;position:sticky;left:0;z-index:2;background:var(--s2);display:flex;flex-direction:column;padding-right:6px}
#page-structures .str-tabreader-labelspacer{height:20px}
#page-structures .str-tabreader-stringlbl2{height:26px;display:flex;align-items:center;justify-content:flex-end;color:var(--tx3);font-family:var(--fm);font-size:.6rem;font-weight:700}
#page-structures .str-tabreader-scrollarea{position:relative}
#page-structures .str-tabreader-strumrow,#page-structures .str-tabreader-fxrow{display:grid;height:20px}
#page-structures .str-tabreader-strumsym{display:flex;align-items:center;justify-content:center;font-size:.6rem;color:var(--tx3)}
#page-structures .str-tabreader-strumsym[data-stroke="down"]{color:var(--c-root)}
#page-structures .str-tabreader-strumsym[data-stroke="up"]{color:#00acec}
#page-structures .str-tabreader-fxsym2{display:flex;align-items:center;justify-content:center;font-size:max(10px,.42rem);font-weight:700;color:var(--c-root);white-space:nowrap;overflow:hidden}
/* La ligne de corde elle-même : un trait horizontal centré verticalement dans la rangée (::after plutôt
   qu'une bordure par cellule — reste continu visuellement même si aucune note n'est jouée sur un pas). */
#page-structures .str-tabreader-row{display:grid;height:26px;position:relative}
#page-structures .str-tabreader-row::after{content:'';position:absolute;left:0;right:0;top:50%;height:1px;background:var(--bd2);z-index:0}
#page-structures .str-tabreader-col{position:relative;display:flex;align-items:center;justify-content:center;z-index:1}
/* Arcs hammer-on/pull-off/bend (sec.strokeLinks) : un seul <svg> par corde qui a au moins une liaison,
   superposé en absolu par-dessus la ligne de corde — au-dessus du trait (::after, z-index 0) mais SOUS les
   pastilles de note (z-index 1) pour que le chiffre reste lisible même si un arc passe dessous. */
#page-structures .str-tabreader-linksvg{position:absolute;left:0;top:0;width:100%;height:26px;pointer-events:none;z-index:0}
/* Note survolée pendant un glisser hammer/pull/bend (cf. strTabReaderAttachStringDrag) — cible valide
   (même corde que le point de départ) mise en évidence pour confirmer où le lien va se poser au relâchement. */
#page-structures .str-tabreader-note.str-link-target{outline:2px solid var(--c-root);outline-offset:1px}
/* Pastille de numéro de case : fond opaque = coupe visuellement la ligne de corde qui passe derrière,
   exactement le rendu attendu d'une tab lisible (le chiffre ne "flotte" pas sur un trait qui le traverse). */
#page-structures .str-tabreader-note{display:flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 3px;border-radius:4px;background:var(--s2);border:1px solid var(--bd2);color:var(--tx);font-family:var(--fd);font-size:.68rem;font-weight:700;cursor:pointer}
#page-structures .str-tabreader-note:hover{border-color:var(--c-root)}
/* Palm mute posé sur CE COUP précis (pas hérité d'un effet posé au niveau de l'accord entier, cf.
   strokeMuted séparé de fx dans strBuildTabDataForSection) — bordure pointillée distincte du symbole "PM"
   de la ligne d'effets sous les cordes, pour repérer d'un coup d'œil quels coups sont mutés individuellement. */
#page-structures .str-tabreader-note.str-stroke-muted{border-style:dashed;border-color:var(--c-root);border-width:2px}
/* Cellule vide (corde non jouée sur cet accord) : cible discrète de double-clic pour ajouter une note
   (dernier axe d'édition d'un "vrai éditeur de tablature"). Invisible au repos (pas de bordure) pour ne
   pas alourdir une tab par ailleurs déjà lisible — un point apparaît seulement au survol comme indice. */
#page-structures .str-tabreader-empty{display:block;width:14px;height:14px;border-radius:50%;cursor:pointer;z-index:1}
#page-structures .str-tabreader-empty:hover{background:var(--bd2)}
/* Petit champ inline pour taper la case au double-clic (strTabReaderShowNoteEditor) — remplace
   temporairement la pastille/cellule vide ciblée, largeur volontairement étroite (2 chiffres max, cases de
   manche réalistes). */
#page-structures .str-tabreader-fretinput{position:absolute;z-index:10;width:34px;height:20px;text-align:center;font-family:var(--fd);font-size:.68rem;font-weight:700;background:var(--s2);border:1.5px solid var(--c-root);border-radius:4px;color:var(--tx);padding:0}
/* Curseur de lecture : UNE SEULE barre verticale continue (position absolue, left recalculé en JS à chaque
   frame) qui traverse toute la hauteur de la portée — pas une classe par cellule×corde comme avant, c'est
   la différence visuelle la plus caractéristique d'un lecteur "pro" (Songsterr, etc.) par rapport à une
   simple grille de cellules surlignées indépendamment les unes des autres. */
#page-structures .str-tabreader-playhead{position:absolute;top:0;bottom:0;width:30px;background:var(--c-root);opacity:.16;pointer-events:none;z-index:0;transition:left .05s linear}
/* Note(s) actuellement sous le curseur : pastille pleine (classe posée/retirée en JS par strTabReaderRenderCursor
   sur les .str-tabreader-note de la colonne courante) — reste lisible même quand plusieurs cordes sonnent
   ensemble sous le même curseur, en plus de la barre de fond ci-dessus. */
#page-structures .str-tabreader-note.playing{background:var(--c-root);border-color:var(--c-root);color:#111}
.insp-midi-badge.insp-midi-on{color:#3d9e78;border-color:#3d9e7855}
.insp-kbd-row{display:flex;flex-wrap:wrap;gap:8px;font-family:var(--fm);font-size:.6rem;color:var(--tx2)}
.insp-kbd-row kbd{display:inline-block;min-width:16px;text-align:center;padding:1px 5px;border-radius:4px;border:1px solid var(--bd2);background:var(--s3);color:var(--tx);font-family:var(--fm);font-size:max(10px,.58rem);margin-right:4px}
.insp-clear-btn{height:22px;padding:0 8px;border-radius:999px;border:1px solid var(--bd);background:transparent;color:var(--tx3);font-family:var(--fm);font-size:max(10px,.55rem);cursor:pointer;transition:all var(--tr)}
.insp-clear-btn:hover{color:var(--tx);border-color:var(--bd2)}

/* HARMONY */
.tl-item{padding:.24rem .55rem;border-radius:6px;border:1px solid var(--bd);background:var(--s2);font-family:var(--fd);font-size:.78rem;font-weight:700;color:var(--tx2);cursor:pointer;transition:all var(--tr)}
.tl-item:hover{border-color:var(--bd2);color:var(--tx)}
.tl-item.on{border-color:var(--c-root);background:rgba(255,107,0,.1);color:var(--c-root)}
.tl-sep{color:var(--tx3);font-size:.6rem;flex-shrink:0}
.tl-all{padding:.18rem .5rem;border-radius:5px;border:1px solid var(--bd);background:var(--s2);font-family:var(--fm);font-size:max(10px,.52rem);color:var(--tx3);cursor:pointer;transition:all var(--tr);margin-left:auto;white-space:nowrap}
.tl-all.on{border-color:var(--c-root);color:var(--c-root)}
.hn-results{flex:1;overflow-y:auto;padding:.85rem;display:flex;flex-direction:column;gap:.75rem}
.hn-sec{padding:.4rem .65rem .26rem;font-family:var(--fm);font-size:max(10px,.44rem);text-transform:uppercase;letter-spacing:.11em;color:var(--tx3);border-bottom:1px solid var(--bd);background:var(--sf);flex-shrink:0}
.hn-mode-row{padding:.4rem .65rem;display:flex;gap:.35rem;align-items:center;border-bottom:1px solid var(--bd);flex-shrink:0}
.hn-mpill{flex:1;height:24px;border-radius:5px;border:1px solid transparent;background:transparent;color:var(--tx2);font-family:var(--fm);font-size:.66rem;cursor:pointer;transition:all var(--tr);border-bottom:2px solid transparent;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.hn-mpill.on{background:var(--s2);color:var(--c-root);border-bottom-color:var(--c-root);font-weight:700}
.hn-mpill:not(.on):hover{background:var(--s2);color:var(--tx)}
/* Mélodie */
.hn-mel-beat{display:flex;flex-direction:column;align-items:center;gap:2px;border-radius:5px;border:1px solid var(--bd);padding:3px 2px;cursor:pointer;min-height:42px;transition:all var(--tr);-webkit-tap-highlight-color:transparent}
.hn-mel-beat.strong{border-color:var(--bd2);background:var(--s2)}
.hn-mel-beat.has-note{border-color:var(--c-root)}
.hn-mel-beat.selected{border-color:var(--c-root);background:var(--s3)}
.hn-mel-beat-num{font-family:var(--fm);font-size:max(10px,.4rem);color:var(--tx3);line-height:1}
.hn-mel-beat-note{font-family:var(--fd);font-size:.65rem;font-weight:700;line-height:1;min-height:14px}
.hn-mel-beat-type{font-family:var(--fm);font-size:max(10px,.38rem);color:var(--tx3);line-height:1}
.hn-note-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:3px;padding:.5rem .65rem .35rem}
.hng{height:27px;border-radius:5px;border:2px solid transparent;background:var(--pbg);font-family:var(--fm);font-size:max(10px,.58rem);cursor:pointer;transition:all var(--tr);display:flex;align-items:center;justify-content:center;font-weight:700}
.hng:active{transform:scale(.93)}
.hn-qual-grid{display:flex;flex-wrap:wrap;gap:3px;padding:.3rem .65rem .4rem}
.hqb{padding:2px 6px;border-radius:4px;border:1px solid var(--bd);background:var(--pbg);color:var(--tx2);font-family:var(--fm);font-size:max(10px,.54rem);cursor:pointer;transition:all var(--tr)}
.hqb:hover{border-color:var(--bd2);color:var(--tx)}
.hqb.on{background:var(--s3);border-color:var(--c-root);color:var(--c-root);font-weight:700}
.hn-add-row{padding:.3rem .65rem .5rem}
.hn-add-btn{width:100%;height:27px;border-radius:var(--r);border:1px dashed var(--bd2);background:transparent;color:var(--tx2);font-family:var(--fm);font-size:.6rem;cursor:pointer;transition:all var(--tr);display:flex;align-items:center;justify-content:center;gap:.3rem}
.hn-add-btn:hover{border-color:var(--c-root);color:var(--c-root);background:rgba(255,107,0,.05)}
.hn-pool-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:3px;padding:.5rem .65rem}
/* opacity:.4 au repos retiré (demande explicite, comparaison avec la grille Fondamentale du mode Accords
   voisine) — hnRenderPool (app.js) donne désormais un style non-sélectionné déjà lisible par lui-même
   (fond neutre + bordure/texte teintés), l'opacity réduite en plus lavait le texte au lieu de simplement
   estomper visuellement les notes non retenues. */
.hn-np{height:27px;border-radius:5px;border:2px solid transparent;background:var(--pbg);font-family:var(--fm);font-size:max(10px,.58rem);cursor:pointer;transition:all var(--tr);display:flex;align-items:center;justify-content:center;font-weight:700}
.hn-prog-strip{border-top:1px solid var(--bd);background:var(--s2);flex-shrink:0;padding:.4rem .65rem}
.pchip{display:flex;align-items:center;gap:.22rem;padding:.15rem .4rem .15rem .55rem;border-radius:5px;border:1px solid var(--bd);background:var(--sf);font-family:var(--fm);font-size:.62rem;color:var(--tx);cursor:pointer;transition:all var(--tr);user-select:none}
.pchip:hover{border-color:var(--bd2)}
.pchip.focus{border-color:var(--c-root);background:rgba(255,107,0,.09);color:var(--c-root)}
.pchip .px{background:none;border:none;color:var(--tx3);cursor:pointer;font-size:.68rem;padding:0 1px;line-height:1;transition:color var(--tr)}
.pchip .px:hover{color:#e07070}
/* harmony result cards */
.rcard{background:var(--sf);border:1px solid var(--bd);border-radius:var(--rl);padding:.6rem .65rem}
.rcard-title{font-family:var(--fd);font-size:.88rem;font-weight:800;color:var(--tx);margin-bottom:.2rem;display:flex;align-items:baseline;gap:.35rem;flex-wrap:wrap;line-height:1.5;overflow:visible;padding-bottom:0;min-height:1.15em}
.rcard-title em{color:var(--c-root);font-style:normal}
/* Alignée sur la ligne de base du titre (align-items:baseline du parent, cf. juste au-dessus) — retour
   explicite : le badge (ex. "PROGRESSION") flottait en exposant au-dessus du titre avec align-items:
   flex-start, au lieu de suivre visuellement la même ligne de texte. */
.rcard-sub{font-family:var(--fm);font-size:max(10px,.46rem);text-transform:uppercase;letter-spacing:.1em;color:var(--tx3);line-height:1.25}
.krow{display:flex;align-items:center;gap:.6rem;padding:.4rem .65rem;border-radius:var(--r);border:1px solid var(--bd);background:var(--s2);margin-bottom:.3rem}
.krow.top{border-color:var(--c-root);background:var(--sf)}
.krow-name{font-family:var(--fd);font-size:.95rem;font-weight:800;min-width:76px;line-height:1}
.krow-name em{color:var(--c-root);font-style:normal}
.krow-mode{font-family:var(--fb);font-size:.68rem;color:var(--tx2);font-weight:400}
.krow-bar{flex:1;height:5px;border-radius:999px;background:var(--s3);overflow:hidden;min-width:30px}
.krow-fill{height:100%;border-radius:999px;background:var(--c-root)}
.krow-pct{font-family:var(--fm);font-size:max(10px,.58rem);color:var(--tx2);min-width:28px;text-align:right}
.krow-dots{display:flex;gap:2px;flex-wrap:wrap;justify-content:flex-end;max-width:130px}
.kdot{padding:.08rem .28rem;border-radius:2px;font-family:var(--fm);font-size:max(10px,.48rem);color:#111;font-weight:700}
@media(max-width:480px){.krow-dots{display:none}}
.srow-h{display:flex;align-items:flex-start;gap:.6rem;padding:.45rem .65rem;border-radius:var(--r);border:1px solid var(--bd);background:var(--s2);margin-bottom:.3rem;cursor:pointer;transition:all var(--tr)}
.srow-h:hover{border-color:var(--bd2);background:var(--s3)}
.srow-h.top{border-color:var(--c-root);background:var(--sf)}
.srow-badge{min-width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:var(--fm);font-size:max(10px,.55rem);font-weight:700;flex-shrink:0}
.b-full{background:rgba(80,184,100,.18);color:#50b864;border:1.5px solid #50b864}
.b-hi{background:rgba(140,196,80,.16);color:#8cc450;border:1.5px solid #8cc450}
.b-med{background:rgba(210,180,60,.16);color:#c8a030;border:1.5px solid #c8a030}
.b-lo{background:rgba(210,100,80,.14);color:#d06050;border:1.5px solid #c05040}
.srow-body-h{flex:1;min-width:0}
.srow-name-h{font-family:var(--fd);font-size:.85rem;font-weight:700;margin-bottom:.2rem;line-height:1.2}
.srow-name-h em{color:var(--c-root);font-style:normal}
.srow-cat{font-family:var(--fm);font-size:max(10px,.46rem);color:var(--tx3);margin-left:.28rem}
.notes-pills{display:flex;flex-wrap:wrap;gap:2px;margin-bottom:.18rem}
.npill{padding:.1rem .35rem;border-radius:3px;font-family:var(--fm);font-size:max(10px,.55rem);color:#111;font-weight:700;display:flex;align-items:center;gap:.1rem;border:2px solid transparent;line-height:1.3}
.npill .iv{font-size:max(10px,.44rem);font-weight:400;opacity:.8}
.npill.ghost{opacity:.28;filter:saturate(.3)}
.srow-info-h{font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3);line-height:1.6}
.deg-grid-h{display:grid;grid-template-columns:repeat(7,1fr);gap:.25rem;margin-top:.3rem}
.dcell{display:flex;flex-direction:column;align-items:center;gap:.1rem;padding:.28rem .1rem;border-radius:var(--r);border:1px solid var(--bd);background:var(--s2)}
.dcell.hit{border-color:var(--c-root);background:rgba(255,107,0,.08)}
.dcell.partial{border-color:#7c6fe0;background:rgba(124,111,224,.07)}
.dcell-roman{font-family:var(--fm);font-size:max(10px,.46rem);color:var(--tx3)}
.dcell-chord{font-family:var(--fm);font-size:max(10px,.58rem);font-weight:700;color:var(--tx);text-align:center;line-height:1.2}
.dcell-fn{font-family:var(--fm);font-size:max(10px,.41rem);color:var(--tx3);text-align:center;line-height:1.3}
.fn-row{display:flex;gap:.35rem;flex-wrap:wrap;margin-top:.4rem}
.fn-chip{padding:.18rem .5rem;border-radius:999px;font-family:var(--fm);font-size:max(10px,.54rem);border:1px solid var(--bd);background:var(--s2);color:var(--tx2);display:flex;align-items:center;gap:.28rem}
.fn-chip em{font-style:normal;font-weight:700}
.fn-T{border-color:#70b870;color:#70b870;background:rgba(112,184,112,.08)}
.fn-SD{border-color:#7c6fe0;color:#7c6fe0;background:rgba(124,111,224,.08)}
.fn-D{border-color:#e07070;color:#e07070;background:rgba(224,112,112,.08)}
.fn-modal{border-color:#e0b840;color:#c09030;background:rgba(224,184,64,.08)}
.fn-secondary{border-color:#60b8d0;color:#40a0c0;background:rgba(96,184,208,.08)}
.fn-out{border-color:var(--tx3);color:var(--tx3)}
.tension-wrap{display:flex;align-items:flex-end;gap:.35rem;margin-top:.55rem;padding-top:.4rem;border-top:1px solid var(--bd)}
.tb-lbl{font-family:var(--fm);font-size:max(10px,.48rem);color:var(--tx3);white-space:nowrap;align-self:center}
.tbar-col{display:flex;flex-direction:column;align-items:center;gap:3px;flex:1}
.tbar-block{border-radius:3px;width:100%;min-width:4px;max-width:30px}
.tbar-name{font-family:var(--fm);font-size:max(10px,.4rem);color:var(--tx3);text-align:center;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.modes-grid-h{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:.3rem}
.mchip{padding:.35rem .5rem;border-radius:var(--r);border:1px solid var(--bd);background:var(--s2)}
.mchip-name{font-family:var(--fm);font-size:max(10px,.45rem);color:var(--tx3);margin-bottom:.1rem}
.mchip-root{font-family:var(--fm);font-size:.62rem;font-weight:700}
.mchip-dots{display:flex;gap:2px;flex-wrap:wrap;margin-top:.18rem}
.mdot{width:11px;height:11px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:max(10px,.35rem);font-weight:700;color:#111;flex-shrink:0}
.cs-chip{padding:.2rem .5rem;border-radius:var(--r);border:1px solid var(--bd);background:var(--s2);font-family:var(--fm);font-size:max(10px,.54rem);color:var(--tx2);cursor:pointer;transition:all var(--tr);display:flex;flex-direction:column;gap:.08rem}
.cs-chip:hover{border-color:var(--bd2);color:var(--tx)}



/* MODAL */
.mov{display:none;position:fixed;inset:0;background:var(--ov);z-index:500;align-items:center;justify-content:center}
.mov.open{display:flex}
.mbox{background:var(--sf);border:1px solid var(--bd2);border-radius:var(--rl);padding:16px;width:min(310px,92vw);box-shadow:0 16px 48px var(--sh);display:flex;flex-direction:column;gap:10px}
.mtit{font-family:var(--fd);font-size:.88rem;font-weight:700;color:var(--tx)}
.mng{display:flex;flex-wrap:wrap;gap:4px}
.mnb{min-width:36px;height:30px;padding:0 5px;border-radius:5px;border:1px solid var(--bd);background:var(--s2);color:var(--tx2);font-family:var(--fm);font-size:.7rem;cursor:pointer;transition:all var(--tr)}
.mnb:hover{border-color:var(--bd2);color:var(--tx)}
.mnb.active{background:var(--tx);color:var(--bg);border-color:var(--tx);font-weight:700}
.mor{display:flex;align-items:center;gap:7px}
.mol{font-size:.62rem;color:var(--tx3);font-family:var(--fm)}
.mob{display:flex;gap:3px}
.mobt{width:24px;height:24px;border-radius:5px;border:1px solid var(--bd);background:var(--s2);color:var(--tx2);font-family:var(--fm);font-size:.65rem;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all var(--tr)}
.mobt:hover{border-color:var(--bd2);color:var(--tx)}
.mobt.active{background:var(--tx);color:var(--bg);border-color:var(--tx)}
.mac{display:flex;justify-content:flex-end;gap:6px}
.bsec{padding:5px 11px;border-radius:6px;border:1px solid var(--bd);background:transparent;color:var(--tx2);font-family:var(--fm);font-size:.63rem;cursor:pointer;transition:all var(--tr)}
.bsec:hover{border-color:var(--bd2);color:var(--tx)}
.bpri{padding:5px 11px;border-radius:6px;border:none;background:var(--tx);color:var(--bg);font-family:var(--fm);font-size:.63rem;font-weight:700;cursor:pointer}
.bpri:hover{opacity:.85}

/* TOAST & MISC */
.hint{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);background:var(--tx);color:var(--bg);font-family:var(--fm);font-size:.65rem;padding:6px 14px;border-radius:999px;opacity:0;pointer-events:none;transition:opacity .25s,visibility 0s linear .25s;visibility:hidden;z-index:9999;white-space:nowrap}
.hint.show{opacity:1;visibility:visible;transition:opacity .25s,visibility 0s} /* visibility : cf. .nav-overlay */
.empty-state{font-family:var(--fm);font-size:.65rem;color:var(--tx3);text-align:center;padding:2.5rem 1rem;border:1px dashed var(--bd2);border-radius:var(--r);line-height:2;flex-shrink:0}
::-webkit-scrollbar{width:3.2px;height:3.2px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--bd2);border-radius:3px}
/* Équivalent Firefox du bloc ::-webkit-scrollbar ci-dessus (Chrome/Edge) : sans scrollbar-width/-color,
   Firefox affiche sa scrollbar native complète (piste large, boutons fléchés haut/bas) au lieu du filet fin
   sans flèches des autres navigateurs — incohérence remarquée sur la sidebar desktop mais qui s'applique en
   réalité partout où ça déborde (mêmes éléments que le bloc webkit ci-dessus). */
*{scrollbar-width:thin;scrollbar-color:var(--bd2) transparent}

.str-mini-card{background:var(--sf);border:1px solid var(--bd);border-radius:6px;padding:6px;cursor:pointer;transition:all var(--tr);display:flex;flex-direction:column;gap:4px}
.str-mini-card:hover{border-color:var(--c-root);background:var(--s2)}
.str-mini-card.active{border-color:var(--c-root);box-shadow:0 0 0 1px var(--c-root)}
/* touch-action:none (pas pan-x) : le réordonnancement (appui long + drag) et le scroll horizontal natif
   du conteneur parent (.str-timeline-row, overflow-x:auto) se disputaient le MÊME axe — avec pan-x, tout
   mouvement horizontal était happé par le scroll natif du navigateur avant même d'atteindre le handler JS
   pointermove, donc un drag horizontal pur après l'appui long ne démarrait jamais (bug rapporté : "appui
   long + drag horizontal ne fonctionne pas, appui long + drag vertical puis horizontal fonctionne" — un
   premier mouvement vertical n'était lui pas intercepté, laissant le temps au JS de poser
   setPointerCapture avant le mouvement horizontal qui suit). none laisse le JS gérer tout le geste tactile
   sur le bloc ; strAttachSectionDrag fait l'auto-scroll du conteneur pendant le drag actif en compensation
   (cf. son code, recherche "auto-scroll"). */
.str-block{flex-shrink:0;border-radius:var(--r);border:2px solid transparent;padding:7px 9px;display:inline-flex;flex-direction:column;gap:4px;min-width:112px;min-height:145px;cursor:pointer;transition:all var(--tr),opacity var(--tr);position:relative;-webkit-tap-highlight-color:transparent;align-self:flex-start;touch-action:none;user-select:none}
.str-block-pills{min-height:65px;overflow-x:hidden}
.str-block-pills::-webkit-scrollbar{width:5px}
.str-block-pills::-webkit-scrollbar-thumb{background:var(--bd2);border-radius:3px}
.str-block.insp-dragging{opacity:.2}
.str-block:hover{filter:brightness(1.1)}
.str-block.active{border-bottom:3px solid var(--_bcolor,rgba(255,255,255,.7))!important}
/* Section survolée par la tête de lecture, SANS être sélectionnée pour édition (.active ci-dessus) — deux
   états distincts qui peuvent coexister ou non selon que la section en cours de lecture est aussi celle
   sélectionnée. Bug corrigé (signalé explicitement) : strHighlightChord réassignait strActiveIdx à CHAQUE
   changement de section pendant la lecture, faisant passer .active (donc éditable) sur une section jamais
   réellement cliquée/sélectionnée — la tête de lecture "leurrait" une sélection qui n'existait pas. Rendu
   volontairement différent de .active (bordure du HAUT pleine, pas la bordure du BAS) pour que les deux
   états restent visuellement distincts même superposés (section sélectionnée ET en cours de lecture en même
   temps) — pointillés essayés en premier puis explicitement écartés (demande : "pas en pointillés"), bordure
   pleine gardée pour rester net/lisible au même titre que .active.*/
.str-block.playing{border-top:3px solid var(--_bcolor,rgba(255,255,255,.7))!important}
.str-block-type{font-family:var(--fm);font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--tx);opacity:.9}
.str-block-bars{font-family:var(--fm);font-size:max(10px,.58rem);color:var(--tx2);opacity:.8}
.str-block-prog{font-family:var(--fd);font-size:.68rem;font-weight:700;line-height:1.3;word-break:break-word}
.str-block-btns{display:none}
.str-block-btn{flex:1;border:1px solid rgba(255,255,255,.2);border-radius:3px;background:rgba(255,255,255,.1);color:var(--tx);font-size:max(10px,.58rem);cursor:pointer;padding:2px 4px;-webkit-tap-highlight-color:transparent}
.str-add-btn{flex-shrink:0;min-width:64px;min-height:145px;border:2px dashed var(--bd);border-radius:var(--r);background:transparent;color:var(--tx3);font-size:1.8rem;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all var(--tr);align-self:flex-start}
.str-add-btn:hover{border-color:var(--c-root);color:var(--c-root)}
.hn-app{flex:1;display:flex;overflow:hidden;min-height:0;max-width:1600px;width:100%;margin:0 auto;box-sizing:border-box}
/* BUG CORRIGÉ (rapporté : "j'avais demandé qu'il occupe une place de 1600px au centre de l'écran, il a repris
   tout l'espace disponible") — .hn-app est le PREMIER enfant direct de #page-harmony (pas de toolbar séparée
   au-dessus comme dans d'autres modules), donc la règle générique desktop ".page > div:first-child{max-
   width:none}" (pensée pour une barre d'outils sticky en haut de page, cf. PAGE SYSTEM plus haut) le cible
   AUSSI et l'écrase — spécificité ".page > div:first-child" (0,1,1, une pseudo-classe + un type) > ".hn-app"
   seule (0,1,0) : le plafond de 1600px déclaré ci-dessus n'avait donc jamais d'effet réel. Exception ciblée,
   même pattern que #page-melodie/#page-drone plus haut dans ce fichier pour des cas similaires. */
#page-harmony>div:first-child{max-width:1600px!important;margin:0 auto!important;width:100%}
.hn-left{width:300px;flex-shrink:0;border-right:1px solid var(--bd);background:var(--sf);display:flex;flex-direction:column;overflow:hidden}
.hn-scroll{flex:1;overflow-y:auto;overflow-x:hidden;display:flex;flex-direction:column}
.hn-right{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
.hn-timeline{display:flex;gap:.28rem;align-items:center;padding:.45rem .85rem;background:var(--sf);border-bottom:1px solid var(--bd);flex-wrap:wrap;flex-shrink:0;min-height:42px}
/* flex:0 1 auto (et non flex:1) : flex:1 étirait la zone de saisie sur toute la hauteur de la colonne et
   épinglait la bande Progression tout en bas, laissant ~500px de vide entre les deux sur desktop — la bande
   suit maintenant directement les contrôles, tout en restant compressible (scroll) si l'écran est bas. */
.hn-input-a{flex:0 1 auto;overflow-y:auto;overflow-x:hidden;display:flex;flex-direction:column}
.hn-input-b{flex-shrink:0;border-top:1px solid var(--bd)}
@media(max-width:700px){
  .hn-app{flex-direction:column}
  .hn-left{width:100%;flex-shrink:0;border-right:none;border-bottom:1px solid var(--bd);max-height:none;height:auto;flex-direction:column}
  .hn-input-a{max-height:none;overflow-y:visible}
  .hn-right{flex:1;min-height:0}
}

/* ═══ METRO-SPECIFIC CSS (scoped inside page-metro) ═══ */
#page-metro .wrap{display:grid;grid-template-columns:auto 1fr;gap:.6rem;padding:.6rem;overflow-y:auto;flex:1;min-height:0;align-content:start}
#page-metro .wrap>.card:first-child{grid-column:1;grid-row:1;min-width:220px}
#page-metro .wrap>.card:nth-child(2){grid-column:2;grid-row:1;min-width:100px;overflow:auto}
#page-metro .wrap>.card:nth-child(n+3){grid-column:1/-1;grid-row:auto;width:100%}
/* Colonne métronome élargie à 135% de sa largeur naturelle (~477px → ~644px) sur grand écran, l'espace
   étant rogné sur la colonne groovebox — largeur totale du .wrap toujours plafonnée à 1600px (cf. règle
   .wrap plus haut). Scopé à partir de 1300px (plutôt qu'appliqué partout en desktop) : une colonne fixe de
   644px sur un écran plus étroit écraserait trop la groovebox (déjà repliée à sa largeur plancher de
   100px dès 900px de large) — ce fut vérifié visuellement avant de restreindre la portée de cette règle. */
@media(min-width:1024px){#page-metro .wrap{grid-template-columns:644px 1fr}}
/* Palier petit écran desktop (audit 2026-10-02) : entre 1024 et 1280px, la colonne de réglages à côté du cercle de
   644px tombait à ~150px (libellés coupés lettre par lettre) — les réglages passent sous le cercle. */
@media(min-width:1024px) and (max-width:1279px){
  #page-metro .wrap{grid-template-columns:minmax(0,1fr)}
  #page-metro .wrap>.card:first-child,#page-metro .wrap>.card:nth-child(2){grid-column:1;grid-row:auto}
  #page-metro .wrap>.card:first-child{max-width:680px;width:100%;justify-self:center}
}
/* Champ BPM topbar (#globalBpmInput) : 62px suffit pour un entier (ex. 120) mais tronque une valeur
   décimale (ex. 128.5 → n'affiche que "28.5", le 1 disparaît hors du champ — bug rapporté). Élargi
   seulement sur desktop, où la topbar a la place ; le mobile garde 62px, plus contraint en largeur. */
@media(min-width:1024px){#globalBpmInput{width:92px!important}}
/* Bouton tempo central du module Métronome (#tempoDisplayBtn, span#tempoDisplay) : 80px fixe suffit à un
   entier (ex. "120") mais déborde avec une décimale (ex. "125.4"/"399.9") — surtout sur desktop où la
   racine passe à 26px (cf. DESKTOP_BREAKPOINT/applySettings, 1.4rem ≈ 36px de police réelle, bien plus
   gourmand en largeur qu'en mobile à 18px de racine — bug rapporté, débordement visible seulement au-delà
   de 1150px). ~96px de texte pur mesuré pour "399.9" (pire cas, BPM max=400) ; 110px laisse une marge. */
@media(min-width:1024px){#tempoDisplayBtn{width:110px!important}}
/* #tempoDisplayBtn/boutons ±10..±.5 : réduits ~20% sur desktop (demande explicite, cf. commentaire sur
   .metro-ring-outer plus haut) — la police de base (1.4rem, cf. index.html) donnait 36.4px de haut sur la
   racine desktop 26px, nettement plus gros que les contrôles équivalents ailleurs dans l'app. */
@media(min-width:1024px){
  #tempoDisplayBtn{font-size:1.15rem!important}
}
/* Barre de contrôle groovebox (Sons/Pattern/CLR/8-16/kit/M/ON-OFF/Seul) : sur desktop, assez de largeur
   pour tout tenir sur une seule ligne — flex-wrap désactivé (au lieu du wrap mobile, nécessaire là-bas
   faute de place) avec un scroll horizontal en tout dernier recours si une fenêtre desktop est malgré
   tout redimensionnée très étroite, plutôt que de casser sur 2 lignes comme sur mobile. Scopé #page-groove
   (pas #page-metro) depuis la scission en module dédié. */
@media(min-width:1024px){#grooveCtrlBar{flex-wrap:nowrap!important;overflow-x:auto}#grooveCtrlBar>*{flex-shrink:0}}
.groove-mode-seg{display:flex;border:1px solid var(--bd);border-radius:5px;overflow:hidden;flex-shrink:0}
.groove-mode-seg button{height:24px;padding:0 .45rem;border:0;border-left:1px solid var(--bd);background:var(--s3);color:var(--tx3);font:700 max(10px,.5rem)/1 var(--fm);cursor:pointer;white-space:nowrap}
.groove-mode-seg button:first-child{border-left:0}.groove-mode-seg button.active{background:var(--c-root);color:#111}
.groove-len-seg button{min-width:26px;padding:0 .35rem;color:var(--tx2)}
#grooveLearnBtn.active{background:var(--c-root)!important;border-color:var(--c-root)!important;color:#111!important}
.groove-step-ruler{display:grid;gap:2px;margin:.15rem .5rem 0;color:var(--tx3);font:600 max(10px,.48rem)/1 var(--fm);text-align:center;overflow:hidden}
.groove-step-ruler span:nth-child(4n+1){color:var(--tx);font-weight:800}
.groove-step-ruler .measure-start{color:var(--c-root)}
@media(max-width:600px){#page-metro .wrap{grid-template-columns:1fr}#page-metro .wrap>.card:first-child,#page-metro .wrap>.card:nth-child(2){grid-column:1;grid-row:auto}}
/* Groovebox mobile (demande explicite : "sur mobile, seules doivent rester les cellules, les boutons A/S/CLR
   et le menu déroulant plus d'options (le volume doit être dans les options)") — la ligne principale de piste
   (badge+grille+volume+A+S+CLR+P, cf. grooveRenderTrackRows) est déjà dense en desktop ; sur mobile, le
   volume par piste (.groove-instr-vol, dans gridLine) est masqué, et son équivalent dans le panneau P
   (.groove-instr-vol-mobile, ajouté à ctrls) prend le relais — les 2 sont synchronisés au même état par
   setGrooveInstrVolume (cf. app.js), jamais affichés simultanément. */
@media(max-width:600px){:is(#page-groove,.gsz-gb) .groove-instr-vol{display:none!important}:is(#page-groove,.gsz-gb) .groove-instr-vol-mobile{display:flex!important}}
/* Barre d'outils Groovebox sur 2 RANGÉES en mobile (demande explicite : "sur mobile, transforme la subbar en
   2 subbars (2 unités en hauteur pour moins de scroll)") — en une seule ligne, atteindre CLR/MIDI/volume
   imposait de scroller horizontalement toute la barre. #grooveCtrlBar passe en colonne, ses 2 enfants
   .groove-ctrl-row deviennent les 2 lignes (ligne 1 : transport + choix sonores, ligne 2 : résolution +
   actions + volume, cf. leur découpage dans grooveInitPage). Chaque ligne garde son PROPRE scroll horizontal
   pour les cas extrêmes (très petit écran), mais l'essentiel tient désormais sans scroll.
   align-items:stretch sur le conteneur : sans ça, les 2 lignes se centreraient et perdraient la pleine
   largeur disponible. Le seuil 600px est celui déjà utilisé juste au-dessus pour le passage mobile de ce
   même module — un seuil unique pour toute la Groovebox, pas un 2e à maintenir en parallèle. */
@media(max-width:600px){
  :is(#page-groove,.gsz-gb) .groove-subbar{overflow-x:visible!important}
  :is(#page-groove,.gsz-gb) #grooveCtrlBar{flex-direction:column;align-items:stretch;width:100%;gap:4px}
  :is(#page-groove,.gsz-gb) .groove-ctrl-row{display:flex;align-items:center;gap:.2rem;flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;width:100%}
  /* Le master volume (260px fixe en desktop) rétrécit pour tenir dans la largeur mobile plutôt que de forcer
     un scroll à lui seul sur toute la 2e ligne. */
  :is(#page-groove,.gsz-gb) .groove-ctrl-row>div[style*="width:260px"]{width:auto!important;flex:1 1 auto;min-width:90px}
}
/* Au-dessus du seuil mobile, .groove-ctrl-row ne doit RIEN changer au rendu d'origine (barre sur une seule
   ligne) : les 2 groupes se comportent en simples conteneurs flex inline enchaînés, exactement comme si
   leurs enfants étaient restés à plat dans #grooveCtrlBar. */
@media(min-width:601px){
  :is(#page-groove,.gsz-gb) .groove-ctrl-row{display:flex;align-items:center;gap:.2rem;flex-wrap:nowrap}
  /* L'espaceur flex:1 (qui pousse le volume à droite) vit dans la 2e rangée : celle-ci doit donc pouvoir
     s'étendre pour que la poussée reste effective en desktop, comme avant la scission. */
  :is(#page-groove,.gsz-gb) .groove-ctrl-row:last-child{flex:1}
  /* Audit 2026-10-02 : depuis l'ajout de GEN/LEARN/MIDI la barre ne tenait plus sur une ligne sur AUCUN
     portable (405px cachés à 1152px, 191px à 1366px, encore 21px à 1536px — volume général invisible).
     D'abord le volume rétrécit (260px → 110px min) ; si ça ne suffit toujours pas, la 2e rangée passe à la
     ligne au lieu de déborder. flex-basis 0 + min-width:min-content : le passage à la ligne n'est décidé que
     sur la largeur MINIMALE de la rangée (volume à 110px), pas sur sa largeur confortable. */
  :is(#page-groove,.gsz-gb) #grooveCtrlBar{flex-wrap:wrap!important;row-gap:4px;max-width:100%;min-width:0;overflow:visible!important}
  :is(#page-groove,.gsz-gb) #grooveCtrlBar>.groove-ctrl-row:last-child{flex:1 1 0!important;min-width:min-content}
  :is(#page-groove,.gsz-gb) .groove-ctrl-row>div[style*="width:260px"]{width:auto!important;flex:0 1 260px;min-width:110px}
}
/* Groovebox desktop (demande explicite, précisée après une 1re version incorrecte où le badge s'étirait sur
   les 2 lignes : "le badge ne doit PAS être étiré sur 2 lignes... 1ère ligne A/S/CLR/OPT + slider volume,
   2e ligne cellules steps SEULEMENT") — .groove-track-grid (posée sur l'ancien conteneur flex "gridLine",
   cf. grooveRenderTrackRows) devient une grille CSS 1 colonne × 2 lignes UNIQUEMENT au-delà de 600px
   (symétrique à la règle mobile ci-dessus, qui garde ses propres réarrangements en-deçà) : ligne 1 = badge +
   .groove-track-ctrlrow (contrôles) CÔTE À CÔTE en flex, comme sur mobile ; ligne 2 = .groove-grid (cellules
   de steps) SEULE, pleine largeur — aucune colonne réservée au badge sur cette ligne, contrairement à la
   1re version.*/
@media(min-width:601px){
  /* row-gap:.35rem !important (pas 2px comme le gap horizontal du header) — demande explicite : "ajoute un
     petit espace entre la ligne du slider de volume et la ligne des steps" : la grille de steps collait
     presque directement sous la ligne de contrôles (2px, la même valeur utilisée pour l'espacement
     HORIZONTAL des boutons entre eux), les 2 lignes se lisaient comme une seule masse continue plutôt que 2
     zones distinctes (contrôles vs. édition du pattern). !important nécessaire : gridLine (cf.
     grooveRenderTrackRows, app.js) porte un style INLINE gap:2px (comportement flex par défaut, utilisé tel
     quel sur mobile où .groove-track-grid n'a aucun effet de mise en page) — un inline style l'emporte
     normalement sur toute règle de feuille de style externe, même plus spécifique, d'où le besoin de forcer
     ici plutôt que de juste déclarer row-gap normalement (qui aurait été silencieusement ignoré). */
  :is(#page-groove,.gsz-gb) .groove-track-grid{display:grid;grid-template-columns:1fr;grid-template-rows:auto auto;row-gap:.35rem!important}
  :is(#page-groove,.gsz-gb) .groove-track-header{display:flex;align-items:center;gap:2px;grid-column:1;grid-row:1}
  :is(#page-groove,.gsz-gb) .groove-track-ctrlrow{display:flex;align-items:center;gap:2px;flex:1}
  :is(#page-groove,.gsz-gb) .groove-grid{grid-column:1;grid-row:2}
}
/* #page-groove : simple colonne verticale (cf. grooveInitPage, app.js) — pas la grille 2 colonnes de
   #page-metro (héritée du couplage historique circle+groovebox), la Groovebox n'a plus besoin de partager
   sa largeur avec un cercle de métronome maintenant qu'elle est un module à part entière. */
/* max-width relevé 1600→2000px (demande explicite : "utiliser un peu plus de largeur quand disponible,
   pour ne pas tronquer en mode 32 steps") — mesuré : sur un écran large (1920-2200px de large), le
   plafond de 1600px laissait 300-600px inutilisés alors que les cellules d'un pattern à 32 pas restaient
   sous leur taille maximale (_GROOVE_STEP_MAX_PX=48px, cf. renderGrooveGrid/app.js) faute d'espace. 2000px
   (pas illimité) : reste raisonnablement borné pour ne pas étirer excessivement le reste de l'interface
   (toolbar/cartes) sur un très grand écran, tout en donnant nettement plus de marge aux 32 pas avant que
   le plafond de cellule n'entre en jeu. */
:is(#page-groove,.gsz-gb) .wrap{max-width:2000px;width:100%;margin-left:auto;margin-right:auto;padding:.6rem;overflow-y:auto;flex:1;min-height:0;display:flex;flex-direction:column;gap:.6rem;}
/* background retiré (cf. #page-metro .card, PAGE SYSTEM) — même bug de spécificité ID > classes qui écrasait
   toujours l'effet "verre dépoli" (.page.module-bg-enabled .card) même fond de module actif. */
:is(#page-groove,.gsz-gb) .card{border:1px solid var(--bd);border-radius:var(--rl);padding:0.8rem;min-width:0}
#page-groove:not(.module-bg-enabled) .card,.gsz-gb .card{background:var(--sf)}
:is(#page-groove,.gsz-gb) .card-title{font-family:var(--fd);font-size:.9rem;font-weight:700;color:var(--tx);margin-bottom:.6rem}
/* STEP (#page-step) : mêmes règles .wrap/.card/.card-title que la Groovebox ci-dessus — même famille visuelle
   (cartes empilées scrollables), demande explicite "TB 303 : séparer du module groovebox" traitée comme un
   changement d'organisation de page, pas de refonte visuelle. */
#page-step .wrap{max-width:1400px;width:100%;margin-left:auto;margin-right:auto;padding:.6rem;overflow-y:auto;flex:1;min-height:0;display:flex;flex-direction:column;gap:.6rem;}
#page-step .card{border:1px solid var(--bd);border-radius:var(--rl);padding:0.8rem;min-width:0}
#page-step:not(.module-bg-enabled) .card{background:var(--sf)}
#page-step .card-title{font-family:var(--fd);font-size:.9rem;font-weight:700;color:var(--tx);margin-bottom:.6rem}
/* Mobile (brief de refonte groovebox, §"Optimisation mobile" : "Supprimer les contours de cartes"/"Supprimer
   les espaces inutiles") — sur petit écran, la Groovebox empile 6+ cartes (toolbar, grille, TB-303, FX rack,
   Pages, Banque de patterns) chacune avec sa propre bordure+padding+gap ; visuellement lourd sur une largeur
   déjà contrainte, contrairement au desktop où l'espace respire naturellement (donc scopé #page-groove, pas
   une règle .card générale — ne touche aucun autre module). Bordure et fond retirés (les cartes deviennent
   de simples blocs empilés, séparés par le gap du .wrap plutôt que par un encadré visible), padding
   horizontal réduit (.8rem→.4rem, gagne de la largeur réelle pour la grille de pas — objectif explicite du
   brief "Grid 8 steps responsive : Les pads doivent remplir la largeur dispo") ; padding vertical conservé
   (la respiration entre le titre et son contenu reste utile même sans bordure). */
@media(max-width:600px){
  :is(#page-groove,.gsz-gb) .card{border:none;background:transparent;padding:.5rem .4rem;border-radius:0}
  :is(#page-groove,.gsz-gb) .wrap{padding:.4rem;gap:.3rem}
  /* Sous-cartes internes (#grooveTrackRows/#tb303Card/#grooveFxRack, cf. app.js) — bordure+fond posés en
     style INLINE (pas une classe .card), donc !important nécessaire pour les neutraliser depuis une media
     query mobile sans dupliquer tout le JS de rendu derrière un flag responsive. Même logique que
     #page-groove .card ci-dessus, appliquée à ce niveau plus profond de la hiérarchie. */
  :is(#page-groove,.gsz-gb) #grooveTrackRows,:is(#page-groove,.gsz-gb) #tb303Card,:is(#page-groove,.gsz-gb) #grooveFxRack{border:none!important;background:transparent!important;padding-left:0!important;padding-right:0!important}
}
/* background retiré d'ici (posé ailleurs via var(--sf) globalement pour .card) — un ID (#page-metro) a une
   spécificité plus forte que n'importe quel nombre de classes et écrasait donc TOUJOURS l'effet "verre
   dépoli" (.page.module-bg-enabled .card, cf. PAGE SYSTEM plus haut) avec un fond opaque, même quand le fond
   de module du Métronome était actif (rapporté : "metronome est opaque avec une image de fond" — bug
   silencieux introduit en généralisant l'effet depuis le Métronome vers tous les modules, cette règle-ci
   n'avait alors plus jamais été retirée). */
#page-metro .card{border:1px solid var(--bd);border-radius:var(--rl);padding:0.8rem;min-width:0;box-shadow:0 10px 30px rgba(0,0,0,.22)}
#page-metro:not(.module-bg-enabled) .card{background:var(--sf)}
#page-metro .card-title{font-family:var(--fd);font-size:.9rem;font-weight:700;color:var(--tx);margin-bottom:.6rem}
#page-metro .metro-grid{display:flex;flex-direction:column;gap:0.8rem;align-items:center;width:100%}
#page-metro .metro-circle-wrapper{display:flex;flex-direction:column;align-items:center;gap:.8rem;position:relative;width:100%}
/* Plein écran métronome (toggleMetroFullscreen() dans app.js) — même pattern que .fbcard.scale-fs (Gammes,
   cf. plus haut) : position:fixed;inset:0 plutôt que la Fullscreen API, pour s'adapter immédiatement à la
   taille réelle du viewport (OS/navigateur/zoom) sans les limitations de permission/geste de l'API native.
   Scope volontairement réduit à .metro-circle-wrapper (cercle, signature, tempo, tap, nudges, N2/Flash/etc.)
   et PAS à toute la carte : les sliders volume metro/groove (.metro-controls) restent hors du mode plein
   écran, non demandés dans ce mode dédié à la lecture/tempo. */
/* Pas de préfixe #page-metro : toggleMetroFullscreen() (app.js) reparente #metroCircleWrapper directement
   sous <body> pendant le plein écran, pour l'extraire du contexte d'empilement piégeant de .wrap (ancêtre
   avec position:sticky;z-index:100 via la règle générique ".page>div:first-child", qui plafonnait le
   z-index de tout descendant — même position:fixed — bien en dessous de .topbar). Un sélecteur scopé à
   #page-metro ne s'appliquerait plus une fois l'élément sorti de ce sous-arbre. */
/* #page-metro .metro-circle-wrapper{position:relative;...} (règle de base, cf. plus haut) a une spécificité
   ID+classe (0,1,1) — strictement supérieure à .metro-circle-wrapper.metro-fs seul (0,2,0), un ID pesant
   toujours plus que n'importe quel nombre de classes. Comme toggleMetroFullscreen() (app.js) reparente
   maintenant l'élément SOUS #page-metro (et non plus sous <body>, pour garder actives les nombreuses règles
   #page-metro .xxx du cercle/disque central), cette règle de base redevenait gagnante et écrasait le
   position:fixed voulu ici (constaté : position calculée "relative" au lieu de "fixed", le fullscreen ne
   couvrait plus tout l'écran mais restait contraint dans le flux scrollable de .pages-container). D'où le
   préfixe #page-metro ajouté ici aussi, pour égaler la spécificité et gagner par ordre d'apparition (cette
   règle est déclarée après celle de la ligne ~1391). */
#page-metro .metro-circle-wrapper.metro-fs{
  container-type:normal;
  position:fixed;inset:0;z-index:9000;background:var(--bg);
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  /* gap réduit (1rem→.6rem) par rapport à la carte normale — demande explicite : le plein écran laissait un
     vide vertical visible au-dessus/en dessous du cercle malgré le centrage. Le contenu total (signature,
     cercle, barre de tempo, tap, boutons du bas) mesuré à ~890px sur un viewport de 900px de haut avec
     l'ancien gap de 1rem×4 espacements (104px) — resserrer l'espacement entre les blocs libère de la place
     pour un cercle plus grand (cf. --metro-fs-w ci-dessous, relevé en conséquence) plutôt que de laisser
     cet espace inutilisé de chaque côté du centrage vertical. */
  padding:12px;overflow:auto;gap:.6rem;
}
/* La barre de tempo (#tempoNeedle, largeur 100% en HTML) et les autres enfants directs de
   .metro-circle-wrapper n'ont pas de max-width propre — seul .metro-ring-outer (le cercle) en avait un
   (560px en HTML). Sans plafond appliqué au wrapper LUI-MÊME, ces enfants s'étiraient sur toute la largeur
   du viewport en plein écran, très disproportionnés par rapport au cercle recentré — constaté : barre de
   tempo immense, cercle petit, contrôles écrasés en bas avec un vide énorme de chaque côté.
   --metro-fs-w est une taille DÉRIVÉE du viewport réel (pas une valeur fixe comme le 640px essayé en
   premier lieu) : sur un écran large (ex. 1812×1000), une largeur fixe de 640px laisse un bloc minuscule et
   collé en haut avec un vide immense en dessous (bug rapporté sur capture) — le contenu doit au contraire
   GRANDIR pour remplir l'espace disponible, borné par la plus petite des deux dimensions du viewport pour
   ne jamais dépasser la hauteur en portrait/fenêtre étroite. */
/* 80vh laissait le cercle déborder du haut de l'écran (le reste du contenu — sig, barre de tempo, tap,
   boutons Sons/N2/Flash — a aussi besoin d'espace vertical propre, jusqu'à ~260px cumulés avec le gap
   resserré ci-dessus, contre ~280px avant). 62vh (relevé depuis 55vh, cf. gap réduit qui libère la marge
   nécessaire) calibré pour qu'un cercle plus grand tienne quand même verticalement même sur un écran large
   mais bas (ex. 1812×1000), sans déborder en haut ni couper les boutons du bas — vérifié par mesure réelle
   du contenu total (Playwright) avant d'ajuster cette valeur, pas au jugé. */
/* Plafond fixe 900px retiré (demande explicite : "doit occuper plus d'espace en largeur" sur desktop) — sur
   un grand écran (ex. 1812×1000), ce plafond était systématiquement plus restrictif que 85vw/62vh, empêchant
   le cercle de vraiment grandir avec l'écran. min(85vw,62vh) seul continue de protéger le tenue verticale
   (62vh reste la valeur mesurée par Playwright, cf. commentaire ci-dessus) sans imposer de limite absolue de
   largeur — le cercle grandit désormais vraiment avec la largeur disponible sur les grands écrans. */
#page-metro .metro-circle-wrapper.metro-fs{max-width:none;--metro-fs-w:min(85vw,62vh)}
#page-metro .metro-circle-wrapper.metro-fs>*:not(#metroFsExitBtn){width:100%;max-width:var(--metro-fs-w)}
/* .metro-ring-outer a width:min(100%,560px,56vh) en style INLINE (index.html, pensé pour la carte normale)
   — un style inline gagne sur toute règle CSS externe sans !important, quelle que soit sa spécificité de
   sélecteur. Overrider max-width seul (comme avant) ne suffisait PAS à faire grandir le cercle au-delà de
   560px en plein écran : max-width ne peut que RÉTRÉCIR une boîte dont la largeur de base (width) est déjà
   fixée plus bas par le style inline, jamais l'agrandir au-delà — confirmé par mesure (Playwright) : sur un
   écran 1920×1080, --metro-fs-w résolvait bien à ~670px mais le cercle restait bloqué à 560px tant que
   `width` lui-même n'était pas aussi overridé ici. D'où le width:...!important ajouté en plus de max-width. */
#page-metro .metro-circle-wrapper.metro-fs .metro-ring-outer{width:var(--metro-fs-w)!important;max-width:var(--metro-fs-w)!important}
/* #page-metro .metro-circle{max-width:200px} (règle desktop ≥1150px, cf. plus haut) plafonne le disque
   central ("PLAY"/le tempo affiché pendant la lecture) à 200px — pensé pour la carte normale, où l'anneau
   fait au plus ~560px. En plein écran l'anneau peut faire jusqu'à 900px (--metro-fs-w) : sans ce override,
   le disque central restait minuscule au milieu d'un grand cercle, disproportionné. container-type:normal
   (cf. plus haut) a aussi neutralisé le calcul cqw normalement utilisé ici — recalculé en pourcentage de
   .metro-ring-outer via une taille absolue dérivée de --metro-fs-w plutôt que de réactiver cqw pour ce cas.
   IMPORTANT : la règle de base #page-metro .metro-circle{position:absolute;top:50%;left:50%;
   transform:translate(-50%,-50%)} (cf. plus bas) est elle aussi scopée à #page-metro — puisque
   toggleMetroFullscreen() (app.js) reparente #metroCircleWrapper sous <body> pendant le plein écran (hors
   de #page-metro, cf. plus haut), TOUTE règle #page-metro .metro-circle cesse de s'appliquer, y compris ce
   positionnement de base. Sans le redéclarer ici, le disque perdait son centrage absolu (position:static,
   constaté visuellement collé en haut du cercle plutôt qu'en son centre). */
/* Disque central + texte agrandis pour suivre la taille réelle de l'anneau en plein écran — demande
   explicite : "il faut tout aggrandir pour suivre le cercle". max-width/height (315px, jusqu'ici fixe)
   ignorait totalement --metro-fs-w (qui peut monter à ~900px sur grand écran, cf. commentaire plus haut sur
   .metro-ring-outer) : le disque central restait donc minuscule au milieu d'un grand anneau. Passé à un ratio
   de --metro-fs-w (40%, jusqu'à 3x plus grand que l'ancien plafond fixe sur un écran large) plutôt qu'un
   plafond absolu — suit maintenant proportionnellement la taille réelle du cercle plein écran. */
#page-metro .metro-circle-wrapper.metro-fs .metro-circle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:35%;height:35%;max-width:calc(var(--metro-fs-w)*.4);max-height:calc(var(--metro-fs-w)*.4);min-width:120px;min-height:120px}
#page-metro .metro-circle-wrapper.metro-fs .metro-beatcount{font-size:calc(var(--metro-fs-w)*.13)}
/* Chiffres de temps agrandis en plein écran aussi (même demande que la taille normale, cf. règle .beatN
   plus haut) — ratio plus généreux (.24 vs .13 pour "PLAY") car le disque plein écran a maintenant nettement
   plus de place réelle disponible (cf. le max-width relevé ci-dessus). */
#page-metro .metro-circle-wrapper.metro-fs .metro-beatcount.beat1,#page-metro .metro-circle-wrapper.metro-fs .metro-beatcount.beat2,#page-metro .metro-circle-wrapper.metro-fs .metro-beatcount.beat3,#page-metro .metro-circle-wrapper.metro-fs .metro-beatcount.beat4,#page-metro .metro-circle-wrapper.metro-fs .metro-beatcount.beat5,#page-metro .metro-circle-wrapper.metro-fs .metro-beatcount.beat6,#page-metro .metro-circle-wrapper.metro-fs .metro-beatcount.beat7,#page-metro .metro-circle-wrapper.metro-fs .metro-beatcount.beat8,#page-metro .metro-circle-wrapper.metro-fs .metro-beatcount.beat9,#page-metro .metro-circle-wrapper.metro-fs .metro-beatcount.beat10,#page-metro .metro-circle-wrapper.metro-fs .metro-beatcount.beat11,#page-metro .metro-circle-wrapper.metro-fs .metro-beatcount.beat12{font-size:calc(var(--metro-fs-w)*.24)}
/* top:8px;right:8px se superposait pixel pour pixel avec #btnFSTopbar (bouton plein écran générique de la
   topbar, toujours au-dessus en position:sticky) — le clic était intercepté par ce dernier avant même
   d'atteindre metroFsExitBtn. Repositionné en bas, loin de toute la zone topbar. */
/* Fond d'écran UI/module : .page (isolation:isolate) et .subbar créent des contextes d'empilement qui
   enfermaient le plein écran sous topbar/subbar. Neutralisés uniquement pendant le plein écran. */
body.metro-fs-open #page-metro,body.metro-fs-open #page-metro.module-bg-enabled{isolation:auto!important;z-index:auto!important;transform:none!important}
.metro-circle-wrapper.metro-fs #metroFsExitBtn{display:flex!important;position:fixed!important;top:16px!important;right:16px!important;bottom:auto;left:auto;z-index:9001}
#page-metro .metro-circle-svg{position:absolute;width:300px;height:220px}
/* will-change testé retiré puis réintroduit — l'hypothèse "promotion de calque différée" a été infirmée
   (retiré, le freeze rapporté persistait de façon comparable) ; remis car il ne coûte rien et reste une
   optimisation légitime pour l'animation continue de l'aiguille. */
/* 2026-10-10 : rotation posée sur l'élément <svg> (calque compositeur), plus sur le <g> interne (relayout SVG à chaque frame) */
#metroNeedleSvg,#metroNeedleSvg2{will-change:transform;overflow:visible}
/* will-change:transform (et non plus `left`) — la position est posée en translateX par
   animateNeedleContinuous, cf. son commentaire : `left` forçait un layout par frame. */
#tempoNeedle{will-change:transform}
#page-metro .metro-section{cursor:pointer;transition:opacity .12s,fill .12s;/* 2026-10-10 : stroke/stroke-width retirés de la transition (relayout SVG par frame, -6 pts CPU x4) */opacity:1;fill:transparent;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
#page-metro .metro-section.beat1{stroke:var(--tx3);opacity:.6}
#page-metro .metro-section.beat-other{stroke:var(--c-root);opacity:.4}
#page-metro .metro-section.active{opacity:1;fill-opacity:1;stroke-width:2.5}
#page-metro .metro-section.beat1.active{fill:var(--tx);stroke:var(--tx)}
#page-metro .metro-section.beat-other.active{fill:var(--c-root);stroke:var(--c-root)}
/* .dim : anticipation de la 2e moitié d'un temps pendant que la 1ère est active (demande explicite,
   "lumière qui suit l'aiguille") — 85% d'opacité, même couleur que l'état actif de son type de temps
   (beat1/beat-other), jamais combinée avec .active (cf. animateNeedleContinuous, un temps donné n'a jamais
   les 2 classes en même temps sur ses 2 moitiés). */
#page-metro .metro-section.dim{fill-opacity:.85}
#page-metro .metro-section.beat1.dim{fill:var(--tx);stroke:var(--tx)}
#page-metro .metro-section.beat-other.dim{fill:var(--c-root);stroke:var(--c-root)}
/* Disque central volontairement figé : aucun flash dessus (demande explicite utilisateur après un premier
   essai avec flash) — seul son texte change (PLAY / numéro de temps, cf. metro-beatcount). Le flash visuel
   au tempo vit uniquement sur l'anneau secondaire (#metoPulseRing, cf. applyRingFlash dans app.js). */
#page-metro .metro-circle{width:108px;height:108px;border-radius:50%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--s2);overflow:hidden;display:flex;align-items:center;justify-content:center;z-index:10;pointer-events:auto;box-shadow:inset 0 0 15px var(--sh);outline:none;-webkit-tap-highlight-color:transparent}
/* Petit témoin discret "horloge partagée active" (metroClockActive), distinct du bouton Play/Pause du clic
   (metroRunning) — évite la confusion "le bouton semble en pause mais Strum/Structures restent calés en
   phase dessus". Volontairement minimal : un simple point, pas de refonte visuelle.
   !important nécessaire : #metroClockIndicator porte un style INLINE (background/opacity, cf. index.html)
   posé comme état de repos — un style inline gagne toujours face à un sélecteur externe, quelle que soit sa
   spécificité, donc cette règle .on restait sans effet malgré la classe correctement appliquée (bug
   constaté : "je ne le vois jamais actif" — la classe ON était bien posée par _syncClockActiveIndicator,
   seul le rendu visuel ne suivait jamais). */
#metroClockIndicator.on{background:var(--c-root)!important;opacity:1!important;box-shadow:0 0 4px var(--c-root)}
#page-metro .metro-beatcount{font-family:var(--fm);font-size:4.2rem;font-weight:700;color:var(--tx);text-align:center;position:relative;z-index:40}
/* Chiffres de temps (1/2/3/4...) agrandis pour mieux remplir le cercle — demande explicite : "les chiffres
   1 2 3 4 sont trop petits, ils devraient remplir le cercle". "PLAY" (mot plus long, 4 lettres vs 1 chiffre)
   garde la taille de base ci-dessus, qui lui convient déjà ; seules les classes .beatN (posées UNIQUEMENT
   quand un vrai numéro de temps est affiché, cf. leur point d'assignation dans app.js — jamais sur "PLAY")
   reçoivent ce grossissement, donc aucun impact sur l'état arrêté. */
#page-metro .metro-beatcount.beat1,#page-metro .metro-beatcount.beat2,#page-metro .metro-beatcount.beat3,#page-metro .metro-beatcount.beat4,#page-metro .metro-beatcount.beat5,#page-metro .metro-beatcount.beat6,#page-metro .metro-beatcount.beat7,#page-metro .metro-beatcount.beat8,#page-metro .metro-beatcount.beat9,#page-metro .metro-beatcount.beat10,#page-metro .metro-beatcount.beat11,#page-metro .metro-beatcount.beat12{font-size:8rem}
#page-metro .metro-beatcount.beat1{color:#fff}
#page-metro .metro-beatcount.beat2,#page-metro .metro-beatcount.beat3,#page-metro .metro-beatcount.beat4,#page-metro .metro-beatcount.beat5,#page-metro .metro-beatcount.beat6,#page-metro .metro-beatcount.beat7,#page-metro .metro-beatcount.beat8,#page-metro .metro-beatcount.beat9,#page-metro .metro-beatcount.beat10,#page-metro .metro-beatcount.beat11,#page-metro .metro-beatcount.beat12{color:var(--c-root)}
#page-metro .metro-controls{display:flex;flex-direction:column;gap:.8rem;width:100%}
#page-metro .ctrl-row{display:flex;gap:.4rem;align-items:center;flex-wrap:wrap}
#page-metro .ctrl-label{font-family:var(--fm);font-size:.65rem;color:var(--tx3);text-transform:uppercase;letter-spacing:.05em;min-width:60px}
#page-metro .ctrl-btn{min-width:32px;height:32px;padding:0 8px;border-radius:5px;border:1px solid var(--bd);background:var(--s2);color:var(--tx2);font-family:var(--fm);font-size:.65rem;font-weight:700;cursor:pointer;transition:all var(--tr);display:flex;align-items:center;justify-content:center}
#page-metro .ctrl-btn:hover{border-color:var(--bd2);color:var(--tx);background:var(--s3)}
#page-metro .ctrl-btn.on{border-color:var(--c-root);color:var(--c-root);background:var(--s2)}
#page-metro .ctrl-btn.adjust{min-width:46px}
/* La grille garde ses dimensions fixes sur desktop ; sur mobile, les 7 colonnes se partagent réellement
   la largeur disponible pour que les boutons extrêmes restent dans la carte. */
.metro-tempo-adjust-row{grid-template-columns:46px 46px 46px 110px 46px 46px 46px;gap:14px}
/* En Simple, les réglages fins masqués ne doivent pas déplacer TAP hors de la colonne centrale. */
#page-metro[data-uilvl="1"] #tapBtn{grid-column:4}

/* Favoris — accès commun aux bibliothèques, sans déplacer leurs commandes existantes. */
#sidebarTabs .tab[data-tab="favorites"] .tab-icon::before,
#mainTabs .tab[data-tab="favorites"] .tab-icon::before,
.ft-ico[data-ico="favorites"]{--ico:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m12 2 3.1 6.3 6.9 1-5 4.9 1.2 6.9L12 17.8l-6.2 3.3 1.2-6.9-5-4.9 6.9-1z' fill='none' stroke='black' stroke-width='1.7' stroke-linejoin='round'/%3E%3C/svg%3E")}
.favorites-root{flex:1;min-height:0;overflow:auto;padding:20px;box-sizing:border-box;color:var(--tx)}
.fav-page-head{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:18px}
.fav-page-head h1{font-family:var(--fd);font-size:1.4rem;margin:0 0 5px}
.fav-head-actions,.fav-filters,.fav-library-controls{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.fav-hint{font-family:var(--fb);font-size:.78rem;color:var(--tx2);line-height:1.5;margin:4px 0}
.fav-button,.fav-icon,.fav-select,.fav-search,.fav-star,.fav-drag{font:inherit;font-family:var(--fb);font-size:.8rem;box-sizing:border-box;border:1px solid var(--bd);border-radius:var(--r);background:var(--s2);color:var(--tx);min-height:36px;padding:7px 12px}
.fav-button,.fav-icon,.fav-star,.fav-drag{cursor:pointer}
.fav-button:hover,.fav-icon:hover,.fav-star:hover{background:var(--s3);border-color:var(--bd2)}
.fav-button.primary{background:var(--c-root);border-color:var(--c-root);color:#111;font-weight:600}
.fav-button.danger{color:var(--bad,#e05656)}
.fav-button:focus-visible,.fav-icon:focus-visible,.fav-select:focus-visible,.fav-search:focus-visible,.fav-card-main:focus-visible,.fav-star:focus-visible,.fav-drag:focus-visible{outline:2px solid var(--c-root);outline-offset:2px}
.fav-icon{padding:5px 10px;flex-shrink:0;font-size:1.1rem;min-width:36px}
/* Étoiles de favoris : même gabarit que les boutons standards des barres de modules (.rotor-btn / subbar : 28 px,
   fond --s3, contour --bd, rayon 5 px, police mono grasse), partout dans l'application. */
.fav-star,.fav-star.fav-current,.fav-star.fav-recent-star{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;min-height:28px;height:28px;min-width:28px;padding:0 .5rem;margin:0;border:1px solid var(--bd);border-radius:5px;background:var(--s3);color:var(--tx2);font:700 max(11px,.62rem)/1 var(--fm);flex-shrink:0;white-space:nowrap;cursor:pointer;transition:background .12s,border-color .12s,color .12s}
.fav-star:hover{background:var(--s3);border-color:var(--c-root);color:var(--tx)}
.fav-star.on{color:#e8b93f;border-color:#e8b93f}
/* Surface en plein écran : pas d'étoiles de favoris (page / set) dans la barre d'octaves */
.tab[data-tab="favorites"]{position:relative;-webkit-touch-callout:none;user-select:none;-webkit-user-select:none}
/* Appui long possible (favori ajoutable ou déjà enregistré) : l'étoile de l'onglet est jaune. */
#mainTabs .tab[data-tab="favorites"].fav-can-add .tab-icon::before,#sidebarTabs .tab[data-tab="favorites"].fav-can-add .tab-icon::before,#desktopSidebar .tab[data-tab="favorites"].fav-can-add .tab-icon::before{background-color:#e8b93f}
.fav-select{max-width:100%;cursor:pointer}
.fav-search{min-width:160px;flex:1}
.fav-filters{margin-bottom:18px}
.fav-views{display:flex;gap:6px;margin:0 0 14px}
.fav-views .fav-button.selected{border-color:var(--c-root);color:var(--c-root);background:var(--sf)}
.fav-recents-hint{margin:-6px 0 14px}
.fav-recent-date{font-size:.68rem;color:var(--tx3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fav-card-footer:has(.fav-recent-date) .fav-badges{flex:1;min-width:0}
.fav-recent-star[aria-pressed="true"]{color:#e8b93f}
.fav-items{position:relative;display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,245px),1fr));gap:12px;align-items:stretch}
.fav-card{display:flex;flex-direction:column;position:relative;border:1px solid var(--bd);border-radius:var(--rl);overflow:hidden;background:var(--sf);min-width:0}
.fav-card:hover{border-color:var(--bd2)}
.fav-card-main{flex:1;display:flex;flex-direction:column;gap:6px;width:100%;padding:14px;border:none;background:transparent;color:var(--tx);text-align:left;cursor:pointer;font-family:var(--fb)}
.fav-card-main:disabled{opacity:.55;cursor:default}
.fav-card-title{font-size:.85rem;font-weight:600;overflow-wrap:anywhere}
.fav-card-type{font-size:.68rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--c-root)}
.fav-card-summary{font-size:.75rem;color:var(--tx2);line-height:1.5;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;overflow-wrap:anywhere}
.fav-card-footer{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 12px 10px}
.fav-badges{display:flex;flex-wrap:wrap;gap:4px;min-width:0}
.fav-badge{font-size:.66rem;padding:3px 6px;background:var(--s2);border-radius:4px;max-width:100%;overflow-wrap:anywhere}
.fav-preview{position:relative;width:100%;height:90px;background:var(--s2);border:1px solid var(--bd);border-radius:var(--r);overflow:hidden;display:flex;align-items:center;justify-content:center}
.fav-preview-surface{width:140px;max-width:100%;height:auto;aspect-ratio:16/9;background:var(--bg,var(--s2))}
.fav-pz{position:absolute;box-sizing:border-box;border:1px solid var(--bd2);border-radius:3px;background-color:var(--sf);overflow:hidden}
.fav-pz::after{content:"";position:absolute}
.fav-pz-grid{background-image:radial-gradient(circle,var(--c-root) 1.2px,transparent 1.7px);background-size:9px 9px;background-position:center}
.fav-pz-piano{background-image:linear-gradient(90deg,var(--bd2) 1px,transparent 1px),linear-gradient(90deg,transparent 5px,var(--tx3) 5px,var(--tx3) 11px,transparent 11px);background-size:16px 100%,16px 60%;background-repeat:repeat-x;background-position:0 0}
.fav-pz-strings{background-image:repeating-linear-gradient(0deg,transparent 0 5px,var(--tx3) 5px 6px)}
.fav-pz-fader{background-image:linear-gradient(90deg,transparent calc(50% - 1px),var(--bd2) calc(50% - 1px) calc(50% + 1px),transparent calc(50% + 1px))}
.fav-pz-fader::after{left:20%;right:20%;top:40%;height:5px;border-radius:2px;background:var(--c-root)}
.fav-pz-knob{border-radius:50%}
.fav-pz-knob::after{left:calc(50% - 1px);top:12%;width:2px;height:38%;border-radius:1px;background:var(--c-root)}
.fav-pz-xy{background-image:linear-gradient(90deg,transparent calc(50% - .5px),var(--bd2) calc(50% - .5px) calc(50% + .5px),transparent calc(50% + .5px)),linear-gradient(0deg,transparent calc(50% - .5px),var(--bd2) calc(50% - .5px) calc(50% + .5px),transparent calc(50% + .5px))}
.fav-pz-xy::after{left:60%;top:35%;width:5px;height:5px;border-radius:50%;background:var(--c-root)}
.fav-pz-pads{background-color:var(--s3);background-image:linear-gradient(90deg,var(--sf) 2px,transparent 2px),linear-gradient(0deg,var(--sf) 2px,transparent 2px);background-size:25% 50%}
.fav-pz-orbital{border-radius:50%;background-image:radial-gradient(circle,var(--c-root) 0 8%,transparent 9% 38%,var(--bd2) 39% 42%,transparent 43% 68%,var(--bd2) 69% 72%,transparent 73%)}
.fav-pz-mixer{background-image:repeating-linear-gradient(90deg,transparent 0 3px,var(--c-root) 3px 5px,transparent 5px 9px);background-size:100% 70%;background-repeat:no-repeat;background-position:0 100%}
.fav-pz-looper{border-radius:50%;background-image:radial-gradient(circle,var(--c-root) 0 16%,transparent 18%)}
.fav-pz-ctl{background-image:linear-gradient(var(--tx3),var(--tx3));background-size:60% 2px;background-position:center;background-repeat:no-repeat}
.fav-drag{position:absolute;top:16px;right:16px;z-index:1;padding:0 5px;min-height:24px;background:var(--sf);cursor:grab;opacity:.7}
.fav-card[data-favorite-id]{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}
.fav-card.dragging{opacity:.3;outline:2px dashed var(--bd2);outline-offset:-2px}
.fav-card-ghost{position:fixed!important;left:0;top:0;z-index:10000;margin:0;pointer-events:none;opacity:.6;box-shadow:0 14px 36px #000b;border-color:var(--c-root);will-change:transform}
.fav-drop-bar{position:absolute;width:4px;border-radius:2px;background:var(--c-root);box-shadow:0 0 8px var(--c-root);pointer-events:none;z-index:3;display:none}
/* Progressions : une séquence lisible, sans aperçu/titre/résumé redondants. */
.fav-progression-content{display:flex;flex-direction:column;gap:12px;width:100%;min-width:0}
.fav-progression-meta{display:flex;align-items:center;justify-content:space-between;gap:8px}
.fav-progression-count{font-size:.68rem;color:var(--tx3);white-space:nowrap}
.fav-progression-chords{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
.fav-progression-chord{display:flex;align-items:center;justify-content:center;min-width:44px;box-sizing:border-box;padding:8px 9px;border:1px solid var(--bd);border-radius:6px;background:var(--s2);color:var(--tx);box-shadow:inset 0 -2px 0 var(--fav-chord-color,var(--c-root));font-family:var(--fm);font-size:.9rem;font-weight:600;white-space:nowrap}
.fav-card-progression .fav-card-main{padding:12px}
.fav-card-progression .fav-card-footer{padding:0 12px 8px}
.fav-card-progression .fav-badges{flex:1}
.fav-card-progression .fav-icon{min-height:28px;height:28px;padding:0 8px;font-size:1rem}
.fav-empty{grid-column:1/-1;text-align:center;padding:36px 16px;border:1px dashed var(--bd2);border-radius:var(--rl);color:var(--tx2);font-family:var(--fb)}
.fav-modal{padding:16px;z-index:9999}
.fav-dialog{width:min(100%,680px);max-height:85dvh;display:flex;flex-direction:column;background:var(--sf);color:var(--tx);border:1px solid var(--bd);border-radius:var(--rl);box-shadow:0 12px 40px #0005}
.fav-dialog-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px;border-bottom:1px solid var(--bd)}
.fav-dialog-head h2{font-family:var(--fd);font-size:1rem;margin:0;overflow-wrap:anywhere}
.fav-dialog-body{overflow:auto;padding:16px;display:flex;flex-direction:column;gap:10px}
.fav-library-controls{position:sticky;top:-16px;background:var(--sf);z-index:1;padding:8px 0}
.fav-library-list{display:flex;flex-direction:column;gap:6px}
.fav-library-row{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 10px;border:1px solid var(--bd);border-radius:var(--r);min-width:0}
.fav-library-row>div{min-width:0;display:flex;flex-direction:column;overflow-wrap:anywhere}
.fav-library-row strong{font-family:var(--fb);font-size:.85rem}
.fav-collection-checks{display:flex;flex-direction:column;gap:8px}
.fav-check{display:flex;align-items:center;gap:10px;padding:8px;font-family:var(--fb);font-size:.85rem;cursor:pointer;overflow-wrap:anywhere}
.fav-check input{width:18px;height:18px;accent-color:var(--c-root)}
.fav-undo{position:fixed;bottom:14px;left:50%;transform:translateX(-50%);z-index:10002;display:flex;align-items:center;gap:10px;background:var(--sf);color:var(--tx);border:1px solid var(--bd2);border-radius:var(--r);padding:8px 12px;box-shadow:0 4px 18px #0005;max-width:calc(100% - 24px);box-sizing:border-box;font-family:var(--fb);font-size:.78rem}
@media(max-width:600px){
  .favorites-root{padding:12px}
  .fav-page-head{align-items:flex-start;flex-direction:column;gap:10px}
  .fav-head-actions{width:100%}
  .fav-head-actions .fav-button{flex:1}
  .fav-filters .fav-search{flex-basis:100%;width:100%}
  .fav-filters .fav-select{flex:1;min-width:0}
  .fav-modal{padding:10px}
  .fav-dialog{max-height:90dvh}
  .fav-undo{font-size:.7rem;gap:6px;flex-wrap:wrap;justify-content:center}
}
@media(max-width:600px){
  .metro-tempo-adjust-row{
    grid-template-columns:repeat(3,minmax(0,1fr)) minmax(0,2.4fr) repeat(3,minmax(0,1fr));
    gap:clamp(3px,1vw,6px);
  }
  #page-metro .metro-tempo-adjust-row .ctrl-btn.adjust{
    width:100%!important;min-width:0;padding:0;font-size:clamp(.65rem,3vw,.85rem)!important;
    justify-self:stretch;
  }
  #page-metro #tempoDisplayBtn{padding:.15rem .2rem!important;font-size:1.1rem!important}
}
#page-metro .tempo-display{min-width:60px;text-align:center;font-family:var(--fm);font-size:1.2rem;font-weight:700;color:var(--c-root)}
#page-metro .play-btn{height:44px;min-width:100px;border-radius:7px;border:1px solid var(--c-root);background:var(--c-root);color:#111;font-family:var(--fd);font-size:1.4rem;font-weight:700;cursor:pointer;transition:all var(--tr);user-select:none}
#page-metro .play-btn:hover{opacity:.85}
#page-metro .play-btn.playing{opacity:.8}
#page-metro #switch1,#page-metro #switch2,#page-metro #pulseToggle,#page-metro #pulse2Toggle,#page-metro #subdivToggle,#page-metro #barreToggle,#page-metro #halfCircleToggle,#page-metro #metroStepToggle{background:var(--s3)!important;color:var(--tx2)!important;border-color:var(--bd)!important}
#page-metro #switch1.on,#page-metro #switch2.on,#page-metro #pulseToggle.on,#page-metro #pulse2Toggle.on,#page-metro #subdivToggle.on,#page-metro #barreToggle.on,#page-metro #countInToggle.on,#page-metro #halfCircleToggle.on,#page-metro #metroStepToggle.on{background:var(--c-root)!important;color:var(--bg)!important;border-color:var(--c-root)!important;font-weight:700!important}
/* Bouton STEP : porte une glyphe SVG (pas du texte comme ses voisins), d'où un padding resserré et un
   centrage explicite — sinon l'icône se retrouve décalée par le line-height hérité des boutons textuels.
   La couleur de l'icône suit `currentColor`, donc l'inversion de l'état .on ci-dessus s'y applique seule. */
#page-metro .metro-step-btn{
  padding:.3rem .55rem;border:1px solid var(--bd);border-radius:6px;cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center;transition:all .12s;
}
#page-metro .sound-select{padding:4px 7px;border-radius:var(--r);border:1px solid var(--bd);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.65rem;cursor:pointer;outline:none}
/* Flèches natives de spinner masquées sur tous les <input type="number"> du Métronome — redondantes
   depuis l'ajout du scroll molette (_metroBindWheelInputs) et responsables d'un débordement/troncature du
   texte signalé sur Ramp Tempo/Mute Aléatoire (ex. "8" ou "15:00" coupés) : ces champs sont dimensionnés en
   px fixes pour leur contenu texte seul, la largeur de la flèche native (~16-20px selon navigateur) n'était
   pas comptée, si bien que le chiffre lui-même se retrouvait compressé/tronqué dans l'espace restant. */
#page-metro input[type="number"].sound-select{-moz-appearance:textfield;appearance:textfield}
#page-metro input[type="number"].sound-select::-webkit-inner-spin-button,
#page-metro input[type="number"].sound-select::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}
/* Largeur fixe pour les 4 menus déroulants de son de la groovebox (Kick/Snare/Hihat/Clap) — sans elle,
   chaque <select> se dimensionnait sur son option la plus longue ("A · Acoustique" vs "A · Grave" par
   exemple), donc les CLR (déjà à taille fixe) et les sliders de volume à leur droite tombaient à des
   positions horizontales différentes d'une rangée à l'autre (demande explicite : tout aligner). */
/* Déplacé de #page-metro vers #page-groove (scission en module dédié, demande explicite "revamp complet...
   module groovebox dédié"). touch-action:none sur .groove-step (ajouté avec le glisser-peindre, demande
   explicite "glisser sur la grille pour tracer les beats... à la manière du lien roland") — sans lui, un
   glisser tactile scrollerait la page au lieu de peindre les cases sur mobile/tablette. */
/* width:82px retiré (bug corrigé, signalé : "le menu déroulant n'est pas ajusté") — cette règle datait
   d'avant l'agrandissement du select (cf. son style inline min-width:140px, "agrandir le bouton de menu
   déroulant de préset"), jamais mise à jour en même temps : width (propriété fixe) l'emportait
   silencieusement sur le min-width inline, donc le select restait coincé à 82px malgré l'agrandissement
   demandé — le texte "A · TR-808" débordait/semblait tronqué. min-width (pas width) laisse le select
   respecter sa taille inline réelle (140px) tout en restant un plancher, pas un plafond fixe. */
:is(#page-groove,.gsz-gb) .groove-instr-sound{min-width:140px;flex-shrink:0}
/* Pan grisé sur kit HQ (bug corrigé — cf. le commentaire détaillé sur panWrap/_grooveSyncSoundSelectVisibility
   dans app.js : un pan par piste n'a structurellement aucun effet sur un kit HQ échantillonné). opacity
   seule (pas display:none) — le curseur reste visible, juste visuellement inerte, cohérent avec disabled
   qui bloque déjà l'interaction. */
:is(#page-groove,.gsz-gb) .groove-instr-pan input[type="range"]:disabled{opacity:.35;cursor:not-allowed}
/* aspect-ratio:1 + max-width (pas une height fixe en px) — demande explicite : "cellules plus carrées (y
   compris en 8 cellules)". L'ancienne approche (height:18px fixe, puis 11px codé en dur pour le mode 32 pas
   ci-dessous) ne pouvait rester carrée qu'à UNE largeur de conteneur précise : dès que la largeur réelle de
   la carte change (fenêtre plus large/étroite, ou l'ajout récent des boutons A/S/CLR sur la même ligne que
   la grille, cf. grooveRenderTrackRows), la hauteur fixe ne suivait plus la largeur réelle d'une cellule et
   le ratio carré se perdait — particulièrement visible en 8 pas, où chaque cellule 1fr est bien plus large
   qu'à 16/32 pas (jusqu'à ~92px de large mesuré). aspect-ratio:1 seul aurait alors produit des cellules
   carrées de 92×92px en 8 pas (bien trop hautes, cf. 1er correctif tenté avec un max-height insuffisant qui
   empêchait au contraire tout carré à 8/16 pas) — max-width plafonne la largeur elle-même à une taille de
   cellule raisonnable, laissant le <input>/<div>.groove-step ne PAS remplir tout son 1fr en 8 pas
   (justify-items:center sur .groove-grid ci-dessous recentre chaque cellule dans sa colonne 1fr, cf. plus
   bas) plutôt que de plafonner seulement la hauteur, qui ne peut de toute façon pas dépasser une largeur déjà
   contenue une fois aspect-ratio appliqué. */
/* min-width:15px (régression mobile signalée — "la mise en forme est moche", cellules écrasées à 14×14px sur
   un viewport 390px, boutons A/S/CLR/⋯ se chevauchant sur la même ligne) : depuis le passage à des colonnes
   max-content (cf. renderGrooveGrid dans app.js, pour serrer les cellules aux résolutions larges), .groove-
   grid n'a plus de largeur minimale garantie — sur un écran étroit, le flex parent (gridLine, badge+grille+
   volume+A+S+CLR+⋯ tous sur la même ligne) comprime la grille bien en dessous d'une taille cliquable
   confortable pour tenir tous ces éléments. min-width fixe un plancher (15px, jamais plus petit même si la
   ligne entière ne tient plus) — au-delà, .groove-grid bascule en scroll horizontal PROPRE À LUI (cf.
   overflow-x:auto ci-dessous) plutôt que de continuer à écraser les cellules OU de laisser les boutons A/S/
   CLR/⋯ (qui doivent, eux, rester pleinement visibles/cliquables en permanence) déborder de l'écran.*/
/* max-width relevé 28→48px (170% de 28, cf. _GROOVE_STEP_MAX_PX*grooveCellZoom dans renderGrooveGrid) —
   demande explicite : "les cellules peuvent être agrandies à 170% facilement" (menu de zoom dans la barre
   d'outils, cf. #grooveCellZoomSelect). Ce CSS max-width n'est PLUS la limite réelle de taille (déplacée
   côté JS, cf. le calcul de cellPx qui fixe déjà width via gridTemplateColumns) — juste un plafond de
   sécurité assez large pour ne jamais clipper une cellule à 170%. */
/* max-width relevé 48→120px (plage de zoom étendue à 250%, cf. grooveSetCellZoom dans app.js) — plafond de
   sécurité assez large pour ne jamais clipper une cellule au zoom maximum, la vraie limite reste côté JS. */
:is(#page-groove,.gsz-gb) .groove-step{/* 2026-10-10 : transition limitée à transform/opacity (all = -20 pts CPU x4 en lecture) */width:100%;min-width:15px;max-width:120px;aspect-ratio:1;border-radius:3px;background:var(--s3);border:1px solid var(--bd);display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:max(10px,.45rem);color:var(--tx2);transition:transform .1s,opacity .1s;user-select:none;touch-action:none}
:is(#page-groove,.gsz-gb) .groove-step.on{background:var(--c-root);border-color:var(--c-root);color:#111;font-weight:700}
:is(#page-groove,.gsz-gb) .groove-step.beatstart{box-shadow:inset 2px 0 0 color-mix(in srgb,var(--tx) 38%,transparent)}
:is(#page-groove,.gsz-gb) .groove-step.downbeat{box-shadow:inset 3px 0 0 color-mix(in srgb,var(--c-root) 72%,var(--tx) 28%)}
:is(#page-groove,.gsz-gb) .groove-step.on.accent,:is(#page-groove,.gsz-gb) .groove-step-half.on.accent{box-shadow:inset 0 0 0 2px color-mix(in srgb,#fff 72%,var(--c-root) 28%),0 0 7px color-mix(in srgb,var(--c-root) 68%,transparent);filter:saturate(1.2) brightness(1.12)}
/* Highlight des temps forts (demande explicite : "highlight des temps fort (fond de cellule plus clair") —
   2 niveaux : .downbeat (1er step de la mesure, le repère le plus fort) plus clair que .beatstart (1er step
   de chaque temps, y compris les temps 2/3/4...). Seul l'état INACTIF (pas .on) est éclairci : une case déjà
   activée reste sur son style .on habituel (couleur d'accent pleine), qui est déjà le signal visuel le plus
   important et ne doit jamais être dilué par l'accent rythmique. color-mix avec --tx (pas de nouvelle
   variable --s4/--s5, qui n'existent dans aucun des thèmes/palettes définis, cf. leurs déclarations groupées
   plus haut dans ce fichier) — reste automatiquement cohérent avec CHAQUE thème/palette (clair et sombre),
   la couleur de base (--s3) s'éclaircit ou s'assombrit relativement à --tx (le texte, donc la couleur la
   plus contrastée disponible dans chaque thème) plutôt qu'une valeur fixe pensée pour un seul thème.
   Contraste RELEVÉ depuis les valeurs d'origine (demande explicite : "augmenter le contraste temps forts/
   faibles/1er temps") — 18%→30% (beatstart), 35%→52% (downbeat) : les 2 anciens niveaux étaient trop proches
   l'un de l'autre ET du fond neutre --s3 pour rester lisibles d'un coup d'œil, en particulier au zoom réduit
   (petites cellules).
   2e passe (retour utilisateur : "aucun changement" avec le 1er relevé de %) — un simple dégradé de gris
   reste perceptivement trop subtil à cette taille de cellule (~28px), même à un écart de 30/52%, surtout
   noyé parmi des cellules .on pleine couleur d'accent (cf. le cas réel testé : peu de cellules inactives
   restent visibles sur un pattern rempli). Ajout d'une bordure teintée en plus du fond (un changement de
   couleur DE BORDURE se détecte perceptivement bien plus vite qu'un même écart de couleur DE FOND — la
   bordure encadre toute la cellule alors qu'un fond peut se confondre avec les cellules voisines) : bd (la
   bordure neutre par défaut, cf. .groove-step) → var(--tx) à 45%/70%, mêmes 2 niveaux downbeat>beatstart.
   Pourcentages de fond aussi relevés une 2e fois (30/52%→42/68%). */
:is(#page-groove,.gsz-gb) .groove-step.beatstart:not(.on){background:color-mix(in srgb,var(--s3) 58%,var(--tx) 42%);border-color:color-mix(in srgb,var(--bd) 55%,var(--tx) 45%)}
:is(#page-groove,.gsz-gb) .groove-step.downbeat:not(.on){background:color-mix(in srgb,var(--s3) 32%,var(--tx) 68%);border-color:color-mix(in srgb,var(--bd) 30%,var(--tx) 70%)}
/* Opacité réduite sur les temps FAIBLES (demande explicite : "réduire l'opacité des temps faibles") — les
   cellules qui ne sont ni .beatstart ni .downbeat (donc les positions hors 1er temps de mesure/temps) sont
   maintenant estompées, ce qui fait ressortir les temps forts PAR CONTRASTE plutôt qu'en poussant encore
   leur propre éclaircissement. N'affecte que l'état INACTIF (:not(.on)), même logique que ci-dessus — une
   cellule activée reste toujours à pleine opacité, c'est le signal le plus important. */
:is(#page-groove,.gsz-gb) .groove-step:not(.on):not(.beatstart):not(.downbeat){opacity:.55}
/* Tête de lecture (.current, demande explicite : "rendre la tête de lecture plus lisible") — outline + fond
   teintés en BLANC (var(--tx), pas var(--c-root)) — confirmé explicitement par l'utilisateur ("pas accent
   color, BLANC") après une bascule d'aller-retour sur ce point. var(--tx) garantit aussi un contraste avec
   TOUS les fonds possibles (.on couleur d'accent pleine incluse, où un contour à la même couleur d'accent
   deviendrait invisible — bug déjà rencontré une fois sur ce même composant). Un essai de grossissement
   (transform:scale) a aussi été tenté entre-temps mais REVERTÉ (retour utilisateur : "pas d'effet de zoom,
   c'est une régression") — revenu à ce design outline/fond.*/
:is(#page-groove,.gsz-gb) .groove-step.current{outline:3px solid var(--tx);outline-offset:1px;box-shadow:0 0 0 1px var(--bg)}
:is(#page-groove,.gsz-gb) .groove-step.current:not(.on){background:color-mix(in srgb,var(--s3) 60%,var(--tx) 40%)}
/* Mode Subdivide (demande explicite, revue après une 1ère implémentation qui doublait le nombre de
   cellules : "pas 2 fois plus de cellules, diviser les cellules existantes en 2 demi-cellules") — la
   cellule .groove-step-split GARDE la même taille/position de grille que .groove-step normal (même
   background/border neutre hérités, cf. règle .groove-step ci-dessus qui s'applique aussi à elle via son
   propre nom de classe additionnel), mais devient un conteneur flex pour 2 demi-cellules .groove-step-half
   accolées (gauche=sous-pas A, droite=sous-pas B — revu depuis un 1er essai haut/bas, demande explicite :
   "je voudrais un split gauche/droite au lieu de haut/bas") au lieu d'un seul bloc cliquable. Chaque moitié
   reprend indépendamment .on/.beatstart/.downbeat/.current (mêmes règles ci-dessus, sélecteur générique
   .groove-step.on etc. s'applique aussi à .groove-step-half.on car la spécificité ne dépend que du nom de
   classe, pas d'un sélecteur enfant) — pas de nouvelle règle de couleur nécessaire, seule la géométrie
   (taille/empilement) change ici. flex-direction:row (pas column) — seul changement vs. la version haut/
   bas, le reste (gap/padding/dimensionnement des demi-cellules) reste identique. */
/* opacity:1 explicite — le conteneur .groove-step-split ne porte plus jamais .on/.beatstart/.downbeat lui-
   même (l'accent est posé sur chaque DEMI-CELLULE individuellement depuis renderGrooveGrid, cf. plus bas),
   donc il matcherait TOUJOURS la règle générique ".groove-step:not(.on):not(.beatstart):not(.downbeat)
   {opacity:.55}" ci-dessus et estomperait l'ensemble du conteneur — annulé ici explicitement, l'opacité
   réelle de chaque moitié est gérée par ses propres règles .groove-step-half ci-dessous.
   BUG CORRIGÉ (signalé : "c'est la couleur des cellules qui change... plus sombres en mode %2") — un 1er
   essai de ce correctif (juste "#page-groove .groove-step-split{...opacity:1}", spécificité (0,1,0)) ne
   suffisait PAS : la règle générique ci-dessus a une spécificité PLUS ÉLEVÉE (0,4,0 — chaque :not(.classe)
   compte comme un sélecteur de classe dans le calcul CSS, donc ".groove-step:not(.on):not(.beatstart):
   not(.downbeat)" vaut 4 sélecteurs de classe, pas 1), donc elle continuait à s'appliquer MALGRÉ l'ordre
   du fichier (la spécificité prime toujours sur l'ordre d'apparition). opacity:.55 sur le PARENT se
   MULTIPLIE avec l'opacity:1 des enfants (l'opacité CSS compose le long de l'arbre DOM, un enfant à 1 dans
   un parent à .55 reste rendu à .55 effectif) — d'où les cellules actives visiblement plus sombres malgré
   des background-color/border-color identiques en valeur brute. Sélecteur ID (#page-groove reste, mais
   ajouté !important en dernier recours : la seule façon fiable de battre une règle plus spécifique sans
   dupliquer inutilement les 3 :not() ici). */
/* padding retiré (bug corrigé, signalé : "les contours extérieurs des cellules sont légèrement plus petits
   [en mode %2]") — mesuré : avec padding:1px, le rectangle coloré combiné des 2 demi-cellules s'arrêtait à
   1px du bord réel de la case de grille sur chaque côté (46px visibles sur 48px réels), alors qu'une case
   normale (.groove-step, sans padding) va bord à bord (48px pleins) — d'où l'impression de contour
   légèrement plus petit. Le gap:1px entre les 2 moitiés suffit déjà à les séparer visuellement, ce padding
   externe était redondant et est la seule cause de cet écart de taille apparente. */
:is(#page-groove,.gsz-gb) .groove-step-split{display:flex;flex-direction:row;gap:1px;background:transparent;border:none;cursor:default;opacity:1!important}
/* width:100%+min-width:0 (bug corrigé — sans eux, une demi-cellule à flex:1 1 0 ET display:flex avec du
   texte à l'intérieur (le glyphe ●) se dimensionnait sur son CONTENU intrinsèque au lieu de s'étirer sur
   toute la largeur/hauteur du conteneur (min-width/min-height:auto est la valeur PAR DÉFAUT d'un flex item,
   qui prime sur flex-basis quand le contenu est plus étroit) — les demi-cellules apparaissaient en fines
   bandes de quelques px au lieu de couvrir toute la cellule). min-height:0 même raison sur l'axe croisé. */
:is(#page-groove,.gsz-gb) .groove-step-half{flex:1 1 0;height:100%;min-width:0;min-height:0;border-radius:2px;background:var(--s3);border:1px solid var(--bd);display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:max(10px,.38rem);color:var(--tx2);transition:transform .1s,opacity .1s;user-select:none;touch-action:none}
:is(#page-groove,.gsz-gb) .groove-step-half.on{background:var(--c-root);border-color:var(--c-root);color:#111;font-weight:700}
:is(#page-groove,.gsz-gb) .groove-step-half.current{outline:2px solid var(--tx);outline-offset:0}
:is(#page-groove,.gsz-gb) .groove-step-half.current:not(.on){background:color-mix(in srgb,var(--s3) 60%,var(--tx) 40%)}
/* Accent temps fort/faible + opacité — mêmes règles EXACTES que .groove-step (cf. ses commentaires plus
   haut pour le détail), dupliquées ici pour .groove-step-half : l'accent est désormais posé sur CHAQUE
   demi-cellule individuellement (cf. renderGrooveGrid), pas sur le conteneur .groove-step-split, donc ces
   règles doivent exister pour les 2 sélecteurs séparément (bug corrigé : "en mode %2, réduire la
   transparence (trop de différence entre %1 et %2)" — sans elles, aucune demi-cellule n'aurait jamais
   reçu ni l'éclaircissement beatstart/downbeat ni la protection :not(.on) de la règle d'opacité). */
:is(#page-groove,.gsz-gb) .groove-step-half.beatstart:not(.on){background:color-mix(in srgb,var(--s3) 58%,var(--tx) 42%);border-color:color-mix(in srgb,var(--bd) 55%,var(--tx) 45%)}
:is(#page-groove,.gsz-gb) .groove-step-half.downbeat:not(.on){background:color-mix(in srgb,var(--s3) 32%,var(--tx) 68%);border-color:color-mix(in srgb,var(--bd) 30%,var(--tx) 70%)}
:is(#page-groove,.gsz-gb) .groove-step-half:not(.on):not(.beatstart):not(.downbeat){opacity:.55}
/* .locked (demande explicite : "afficher une indication sur un step qui a été modifié par appui long") —
   petit point dans le coin haut-droit, visible sur .groove-step ET .groove-step-half (même sélecteur
   ::after appliqué aux 2, position:relative déjà implicite via display:flex qui n'empêche pas un enfant
   absolu). Couleur --tx (blanc/texte) plutôt qu'une couleur d'accent dédiée — cohérent avec le reste des
   indicateurs déjà neutres de ce module (playhead .current, cf. son propre commentaire "pas accent color,
   BLANC"), et reste visible sur une case .on (fond plein couleur d'accent) comme sur une case inactive.
   pointer-events:none : ne doit jamais intercepter le clic/appui long destiné à la case elle-même. */
:is(#page-groove,.gsz-gb) .groove-step.locked::after,:is(#page-groove,.gsz-gb) .groove-step-half.locked::after{content:'';position:absolute;top:1px;right:1px;width:4px;height:4px;border-radius:50%;background:var(--tx);opacity:.85;pointer-events:none}
:is(#page-groove,.gsz-gb) .groove-step.locked,:is(#page-groove,.gsz-gb) .groove-step-half.locked{position:relative}
/* overflow-x:auto (régression mobile, cf. commentaire sur .groove-step min-width ci-dessus) — la grille peut
   défiler horizontalement SI ELLE SEULE n'a plus la place, plutôt que d'écraser ses cellules sous leur
   min-width ou de forcer badge/volume/A/S/CLR/⋯ (les vrais éléments à taille fixe de gridLine) à rétrécir
   ou déborder. touch-action:pan-x autorise explicitement le geste de scroll horizontal tactile sur cette
   grille — sans lui, touch-action:none posé sur chaque .groove-step (nécessaire au glisser-peindre, cf. son
   propre commentaire) bloquerait aussi le scroll de son conteneur parent par défaut sur certains navigateurs
   mobiles. */
:is(#page-groove,.gsz-gb) .groove-grid{display:grid;gap:2px;justify-content:start;overflow-x:auto;touch-action:pan-x}
/* ── TB-303 ── */
/* Chaque colonne = 1 step complet (piano-roll vertical + octave + accent/slide), pas juste une case on/off
   comme les pistes batterie — cf. l'en-tête du module TB-303 dans app.js sur pourquoi elle a besoin de son
   propre langage visuel (une vraie hauteur de note par step, pas un simple binaire). min-width évite que les
   colonnes deviennent illisibles à 32 steps sur une carte étroite — overflow-x:auto posé sur #tb303Card
   (cf. index.html/app.js) prend le relais si la largeur totale dépasse la carte. */
/* Ergonomie revue 2 fois (demande explicite la plus récente : "donne-lui le look de la groovebox... carrés
   clicables/peignables... inspire-toi du site roland"). 1ère revue (commentaire encore visible dans
   l'historique git) : cellules passées de 121×9px à 121×18px avec bordures franches entre lignes — un
   progrès sur la lisibilité, mais toujours des cases RECTANGULAIRES très allongées (121px large), pas
   carrées comme dans la grille batterie juste au-dessus (screenshot fourni : "espace inutile actuellement"
   sur les colonnes). CETTE revue : cellules carrées (aspect-ratio:1 + max-width, EXACTEMENT le même
   mécanisme que .groove-step ci-dessus, cf. son propre commentaire sur pourquoi une height fixe ne peut pas
   rester carrée) — même gabarit visuel que la grille batterie, cohérence complète entre les 2 grilles du
   module plutôt qu'un simple air de famille. min-width descendu à 22px (26px avant) : des cases carrées de
   22px sont déjà confortables au clic/glisser, pas besoin d'aller plus large pour la lisibilité qu'apportait
   avant la hauteur seule. */
/* grid-auto-columns:max-content (pas minmax(22px,1fr)) + justify-content:start — bug trouvé en vérifiant
   "espace inutile actuellement" (screenshot fourni) : 1fr consomme TOUJOURS 100% de la largeur disponible du
   conteneur par définition, donc à 8 pas (valeur par défaut) sur une carte large, chaque colonne s'étirait à
   ~125px alors que la cellule qu'elle contient plafonne à 24px (cf. .tb303-cell max-width) — un vide entre
   chaque case carrée au lieu de les rapprocher. justify-content:start seul n'aurait rien changé : il ne
   reprend la main que quand les pistes de la grille NE remplissent PAS déjà 100% du conteneur, ce que 1fr
   empêche par construction. max-content laisse chaque colonne se dimensionner sur son contenu réel (la
   cellule carrée, 22-24px), justify-content:start regroupe alors les colonnes à gauche plutôt que de les
   étaler sur la largeur totale — mêmes cellules serrées quel que soit le nombre de pas actif (8/16/32). */
#page-step .tb303-grid{grid-auto-flow:column;grid-auto-columns:max-content;justify-content:start}
#page-step .tb303-col{display:flex;flex-direction:column;gap:0;min-width:22px}
#page-step .tb303-pianoroll{display:flex;flex-direction:column;gap:0;border:1px solid var(--bd);overflow:hidden}
/* Coins arrondis seulement sur la 1ère/dernière colonne (pas chaque .tb303-pianoroll individuellement, qui
   donnerait une grille "en escalier" avec un arrondi répété sur les 8-32 colonnes) — cohérent avec
   #tb303NoteLabels dont le coin gauche est arrondi côté app.js (cf. tb303RenderGrid). */
#page-step .tb303-col:last-child .tb303-pianoroll{border-radius:0 4px 4px 0}
/* 12 cellules empilées = 1 octave chromatique, cf. TB303_NOTE_NAMES (12 demi-tons) — .root (do, note%12===0)
   marqué d'un fond distinct (pas seulement une bordure, trop discrète à l'ancienne taille 9px et toujours
   utile pour un repère de hauteur immédiat sans lire chaque étiquette). max-width:24px (pas de height fixe,
   cf. commentaire ci-dessus) — même valeur que .groove-step (28px) moins quelques px : le piano-roll a
   souvent plus de lignes visibles (7-12) que la grille batterie n'a de pistes (6), une case légèrement plus
   petite garde la hauteur totale de la grille raisonnable sans sacrifier le ratio carré.*/
/* max-width relevé 24→41px (170% de 24, même zoom partagé que .groove-step ci-dessus, cf. grooveCellZoom). */
/* max-width relevé 41→103px (même rebasage de plage de zoom, cf. .groove-step ci-dessus). */
#page-step .tb303-cell{width:100%;max-width:103px;aspect-ratio:1;background:var(--s3);border-bottom:1px solid var(--bd);cursor:pointer;transition:background .1s}
#page-step .tb303-cell:last-child{border-bottom:none}
#page-step .tb303-cell.root{background:color-mix(in srgb,var(--s3) 80%,var(--tx) 20%)}
#page-step .tb303-cell.on{background:var(--c-root);border-color:var(--c-root)}
#page-step .tb303-cell.root.on{background:var(--c-root)}
#page-step .tb303-cell:hover:not(.on){background:color-mix(in srgb,var(--s3) 60%,var(--tx) 40%)}
/* Highlight des temps forts (même convention que .groove-step.beatstart/.downbeat, cf. leur commentaire) —
   ajouté au piano-roll TB-303 qui n'avait jusqu'ici AUCUN repère de temps, contrairement à la grille
   batterie juste au-dessus dans la même page (incohérence visuelle relevée en revue ergonomie). */
#page-step .tb303-col.beatstart .tb303-cell:not(.on){background:color-mix(in srgb,var(--s3) 82%,var(--tx) 18%)}
#page-step .tb303-col.downbeat .tb303-cell:not(.on){background:color-mix(in srgb,var(--s3) 65%,var(--tx) 35%)}
/* Playhead (demande explicite : "playhead doit être visible" — cf. _startTb303Playhead dans app.js) — même
   convention visuelle que .groove-step.current (outline blanc) juste au-dessus dans ce fichier, appliquée à
   la COLONNE entière (pas une cellule précise, cf. commentaire tb303-accent-on ci-dessus sur la même
   distinction) puisque le "step courant" concerne tout le piano-roll de cette colonne, pas une seule note. */
#page-step .tb303-col.tb303-current .tb303-pianoroll{outline:2px solid var(--tx);outline-offset:1px}
/* Accent/slide déplacés du ctrl-row (mini-boutons −/+/A/S sous chaque colonne, supprimés — demande
   explicite : "accent avec clic droit... supprimer les minuscules boutons sous la piste") vers des gestes
   directs sur la grille (clic droit = accent, glisser horizontal sur la même note = slide, cf.
   col.oncontextmenu/cell.onpointerenter dans tb303RenderGrid) — plus aucun bouton à viser, juste un retour
   visuel sur la grille elle-même. Accent = bordure pleine largeur sur TOUTE la colonne (pas une cellule
   précise, l'accent est un attribut du step entier) ; slide = teinte distincte sur la cellule active
   (bleu-ish plutôt que --c-root/orange, les 2 flags pouvant être actifs simultanément sur le même step —
   une seule couleur pour les 2 rendrait la combinaison illisible). */
/* .tb303-accent-on posé directement sur CHAQUE .tb303-pianoroll (pas sur .tb303-col comme avant Subdivide) —
   nécessaire depuis le %2 (cf. _tb303BuildColumnHalf dans app.js) : une colonne subdivisée contient 2 piano-
   rolls indépendants (sous-pas A/B), chacun avec son propre accent éventuel — une seule marque au niveau de
   la colonne ne pourrait plus distinguer "A accentué" de "B accentué". Sélecteur simplifié en conséquence
   (l'ancien ciblait .tb303-col.tb303-accent-on .tb303-pianoroll, désormais juste .tb303-pianoroll.tb303-
   accent-on — fonctionne identiquement en mode non subdivisé, un seul piano-roll par colonne).*/
#page-step .tb303-pianoroll.tb303-accent-on{box-shadow:inset 0 0 0 2px var(--c-root)}
#page-step .tb303-cell.slide-on{background:#4a9eff;border-color:#4a9eff}
/* Subdivide TB-303 (%2, port du système déjà en place pour la batterie, cf. .groove-step-split/.groove-step-
   half ci-dessus) — décision explicite : chaque COLONNE se scinde en 2 demi-colonnes côte à côte (pas un 2e
   étage de piano-roll empilé), la largeur de colonne (grid-template-columns, cf. tb303CellPx dans app.js)
   reste inchangée pour ne jamais désaligner la grille avec celle de la batterie juste au-dessus — seul le
   CONTENU intérieur se scinde. gap:1px entre les 2 moitiés, même valeur que .groove-step-split. */
#page-step .tb303-col-split .tb303-col-inner{display:flex;flex-direction:row;gap:1px;height:100%}
#page-step .tb303-pianoroll-half{flex:1 1 0;min-width:0}
/* ── Potards circulaires TB-303 ── (demande explicite : "potards circulaires plutôt que sliders") — SVG
   construit/animé en JS (cf. tb303KnobEl dans app.js), pas un <input type="range"> stylé : un vrai slider
   HTML ne peut pas être rendu circulaire de façon fiable cross-navigateur (appearance:none perd le support
   clavier/accessibilité natif sans réel gain ici, puisque l'interaction est de toute façon un glisser
   personnalisé). */
#page-step .tb303-knob-wrap{display:flex;flex-direction:column;align-items:center;gap:2px;width:52px}
/* #page-play (SURFACE) légèrement plus large que STEP (52px) : ses libellés français plus longs
   ("Résonance", "Relâchement", "Désaccord"...) coupaient sinon en plein milieu d'un mot sur 2 lignes même
   avec le retour à la ligne activé (cf. .tb303-knob-label ci-dessous) — 64px leur laisse assez de place pour
   tenir sur 1-2 lignes à des coupures plus naturelles. */
#page-play .tb303-knob-wrap{display:flex;flex-direction:column;align-items:center;gap:2px;width:64px}
#page-step .tb303-knob-label{font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3);text-align:center;white-space:nowrap}
/* #page-play (SURFACE) a des libellés plus longs que STEP (« Env. filtre », « Relâchement », « Désaccord »,
   « Sous-octave »...) — bug rapporté avec capture d'écran : white-space:nowrap les faisait déborder du
   potard de 52px et se superposer au texte du potard voisin. Autorisé à passer sur 2 lignes (hauteur fixe
   réservée ci-dessous pour que les potards restent alignés entre eux même quand certains libellés tiennent
   sur 1 ligne et d'autres sur 2) plutôt que d'élargir chaque potard, ce qui aurait fait una grille bien plus
   large pour un gain de lisibilité minime. */
#page-play .tb303-knob-label{
  font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3);text-align:center;
  white-space:normal;line-height:1.15;min-height:2.3em;display:flex;align-items:flex-end;justify-content:center;
  width:100%;                 /* le span par défaut ne remplit PAS .tb303-knob-wrap (align-items:center dessus
                                  sizes ses enfants au contenu) — sans ceci le texte déborde toujours du potard
                                  de 52px au lieu de passer à la ligne dedans. */
  box-sizing:border-box;overflow-wrap:break-word;word-break:break-word;
  /* min-width:0 — PIÈGE FLEXBOX classique et 2e cause du même bug (screenshot persistant après un 1er
     correctif qui posait déjà width:100%+overflow-wrap) : un enfant flex garde par défaut min-width:auto,
     qui vaut la largeur du plus long MOT INSÉCABLE de son contenu ("Résonance", "Attaque", "Désaccord" n'ont
     aucun espace où couper) — cette largeur minimale l'emporte sur width:100%/overflow-wrap tant qu'elle
     n'est pas explicitement annulée, et le texte déborde du potard voisin malgré une boîte de 52px. */
  min-width:0;
}
#page-step .tb303-knob-val,
#page-play .tb303-knob-val{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--c-root);font-weight:700}
#page-step .tb303-knob-track,
#page-play .tb303-knob-track{fill:none;stroke:var(--s3);stroke-width:4;stroke-linecap:round}
#page-step .tb303-knob-fill,
#page-play .tb303-knob-fill{fill:none;stroke:var(--c-root);stroke-width:4;stroke-linecap:round}
#page-step .tb303-knob-dot,
#page-play .tb303-knob-dot{fill:var(--tx)}
#page-step .tb303-knob-svg,
#page-play .tb303-knob-svg{touch-action:none}
#page-metro .preset-row{display:flex;align-items:center;gap:.5rem;padding:.4rem .6rem;border-radius:6px;border:1px solid var(--bd);background:var(--s2);cursor:pointer;transition:all .12s;font-family:var(--fm);font-size:.85rem}
#page-metro .preset-row:hover{border-color:var(--bd2);background:var(--s3)}
/* Liste Sons (#soundSetList) : police de .preset-row (.85rem, pensée pour le contexte #presetsList qui
   l'écrase de toute façon avec son propre span/strong à .62rem inline) beaucoup trop grande ici, où le
   contenu texte est directement celui de .preset-row — bug rapporté avec capture d'écran ("les polices sont
   énormes"). Réduite une 2e fois (.65rem→.55rem, demande explicite : "encore réduire la police, affiner les
   cartes pour en afficher plus verticalement"), cohérente avec les libellés les plus compacts de l'app
   (.metro-feat-label et consorts, .55rem). Padding vertical des rows réduit en écho (.4rem→.28rem) pour
   qu'affiner la police se traduise vraiment par plus de lignes visibles sans scroller, pas seulement du
   texte plus petit dans des rows toujours aussi hautes. */
#page-metro .sound-row{font-size:max(10px,.55rem);padding:.28rem .5rem}
#page-metro .accent-btn{padding:.3rem .5rem;border-radius:5px;border:1px solid var(--bd);background:var(--s3);color:var(--tx2);font-family:var(--fm);font-size:max(10px,.58rem);cursor:pointer;transition:all .12s}
/* FEATURES CARD (Swing/Accents/Beat Skip/Human Feel/Ramp Tempo/Mute Aléatoire/Session Timer) — remplace
   l'ancien flex-wrap:wrap non structuré (7 blocs flex:1;min-width:160px sans séparation visuelle, qui
   cassaient de façon imprévisible selon la largeur réelle disponible). Grille régulière avec séparateurs de
   carte (fond légèrement distinct + bordure) pour que chaque réglage soit visuellement délimité, plus
   lisible qu'un simple espacement. Toutes les cards font 1 colonne, y compris "Accents par beat" (jusqu'à
   12 boutons selon la signature rythmique — cf. .accent-btn ci-dessous, réduit en taille pour tenir dans
   une largeur de 1/3 plutôt que de forcer la carte sur 2 colonnes). */
#page-metro .metro-feat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(160px,100%),1fr));gap:.6rem}
/* align-self:start RETIRÉ (demande explicite : "toutes les cartes de réglage doivent être alignées entre
   elles") — réintroduit le stretch par défaut de la grille (chaque .metro-feat-item remplit la hauteur de sa
   row, alignant leurs bords bas sur la carte la plus haute de la même ligne), pour un alignement visuel
   propre entre "Swing & Human Feel"/"Accents par beat"/"Beat Skip" (et la ligne du dessous). Le bug molette
   que align-self:start corrigeait (cf. son ancien commentaire ici, et _bindCardWheelToNearestSlider dans
   app.js) est désormais résolu autrement, À LA SOURCE du binding : le wheel cible un conteneur INTERNE
   (.metro-feat-content, cf. juste en dessous) qui garde sa hauteur naturelle même quand .metro-feat-item lui
   s'étire — l'espace vide sous ce conteneur interne, dans une carte étirée plus haute que son contenu, ne
   déclenche donc plus la molette. */
#page-metro .metro-feat-item{background:var(--s2);border:1px solid var(--bd);border-radius:var(--r);padding:.6rem;min-width:0}
/* .metro-feat-content : enveloppe interne du contenu réel d'une carte (titre+contrôles), à hauteur naturelle
   — seule cible du binding molette (cf. _bindCardWheelToNearestSlider dans app.js), pour rester scopée au
   contenu visible même quand .metro-feat-item (son parent direct) est étirée par la grille. */
#page-metro .metro-feat-content{min-width:0}
#page-metro .metro-feat-wide{grid-column:span 2}
#page-metro .metro-feat-wide input[type=range].sound-select{min-width:90px}
#page-metro .metro-feat-label{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx3);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.5rem}
/* Sliders du métronome : appearance:none (inline) retire le style natif ; Chrome garde le pouce accent-color,
   Firefox le rend en blanc. Pouce/piste explicites pour les deux moteurs. */
#page-metro input[type=range].sound-select::-webkit-slider-runnable-track{height:5px;background:var(--s2);border-radius:2px}
#page-metro input[type=range].sound-select::-moz-range-track{height:5px;background:var(--s2);border-radius:2px}
#page-metro input[type=range].sound-select::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;margin-top:-5px;border-radius:50%;background:var(--c-root);border:none;cursor:pointer}
#page-metro input[type=range].sound-select::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:var(--c-root);border:none;cursor:pointer}
@media(max-width:900px){
  #page-metro .metro-feat-grid{grid-template-columns:repeat(2,1fr)}
}
/* min-width déplacé du style inline (index.html) vers ici : devait varier entre desktop et mobile
   (cf. media query ci-dessous), impossible avec une valeur figée en inline sans !important. */
#page-metro #grooveKitSelect{min-width:70px}
#page-metro #grooveStyle{min-width:80px}
@media(max-width:520px){
  #page-metro .metro-feat-grid{grid-template-columns:1fr}
  #page-metro .metro-feat-wide{grid-column:auto}
  /* Le groupe [légende "Sons"+grooveKitSelect / légende "Pattern"+grooveStyle / CLR] (index.html, carte
     Groove) débordait légèrement sur petit écran, poussant CLR hors du viewport visible (scrollable mais
     pas visible sans interaction, constaté). Le vrai facteur limitant n'était PAS min-width (déjà réduit
     sans effet) mais la largeur INTRINSÈQUE du <select> non stylé, qui prend la largeur de son option la
     plus longue affichée ("Electronic"/"Syncopated") — d'où max-width + overflow:hidden ci-dessous plutôt
     qu'un simple ajustement de min-width. */
  #page-metro #grooveKitSelect{max-width:64px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  #page-metro #grooveStyle{max-width:72px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
}
#pulse2Overlay{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9999}

#page-rhythm .rh-wrap{display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden}
#page-rhythm .rh-sticky-player{flex-shrink:0;box-shadow:0 4px 10px -4px rgba(0,0,0,.4);margin:.6rem .6rem 0}
#page-rhythm .rh-scroll-body{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:.6rem;padding:.6rem}
/* background retiré d'ici (cf. #page-metro .card, PAGE SYSTEM) — même bug de spécificité ID > classes qui
   écrasait toujours l'effet "verre dépoli" ([class$="-card"]) même fond de module actif. */
#page-rhythm .rh-card{border:1px solid var(--bd);border-radius:var(--rl);padding:.7rem;min-width:0;max-width:100%;box-sizing:border-box}
#page-rhythm:not(.module-bg-enabled) .rh-card{background:var(--sf)}
#page-rhythm .rh-card-title{font-family:var(--fd);font-size:.85rem;font-weight:700;color:var(--tx);margin-bottom:.5rem;display:flex;align-items:center;gap:.4rem}
/* .rh-wave-zone (3e itération de la portée de la vague — historique complet dans son commentaire HTML,
   cf. index.html) : regroupe subbar+chordRow+grille (tout ce qui suit le titre) comme wrapper positionné,
   demande explicite "visible derrière les cellules, à partir du séparateur titre/rangée de boutons". Sans
   ce wrapper il aurait fallu calculer/mesurer la hauteur du titre pour positionner #rhWave manuellement en
   CSS pur (hauteur variable selon la langue/le zoom) — le wrapper la fait démarrer naturellement au bon
   endroit via inset:0 (cf. .rh-wave plus bas). Le border-top qui servait de séparateur (auparavant sur
   .rh-subbar directement, cf. son style inline dans index.html) est déplacé ICI et élargi aux bords réels
   de la carte via une marge négative qui compense le padding de .rh-card (.7rem) — "le séparateur peut être
   élargi pour toucher les bords de la card", demande explicite. */
/* calc(-.7rem - 1px) : compense aussi le border:1px de .rh-card en plus de son padding .7rem — un simple
   -.7rem laissait un écart de 1px avec le bord réel de la carte (constaté par mesure DOM), le border n'étant
   pas inclus dans le padding. margin-bottom/padding-bottom symétriques (2e correctif, demande explicite :
   "faire aller la vague jusqu'en bas de la card, pas jusqu'en bas des cellules") — la zone s'arrêtait avant
   au padding-bottom du .rh-card, laissant un espace sous la grille où le dégradé de la vague se faisait
   couper net par overflow:hidden (ligne horizontale visible signalée). padding-bottom compense la marge
   négative pour ne RIEN déplacer visuellement (la grille garde la même position), seule la zone de clip de
   la vague s'étend jusqu'au bord réel de la carte. */
#page-rhythm .rh-wave-zone{position:relative;overflow:hidden;margin:0 calc(-.7rem - 1px) calc(-.7rem - 1px);padding:0 calc(.7rem + 1px) calc(.7rem + 1px);border-top:1px solid var(--bd)}
/* Note : l'ancien border-top inline de #rhSubbar (index.html) qui faisait doublon avec celui-ci a été retiré
   directement dans le HTML plutôt que neutralisé ici — une règle CSS externe ne peut pas l'emporter sur un
   style inline (spécificité toujours supérieure), donc "border-top:none" ici n'aurait jamais fonctionné. */
/* Tous les enfants DIRECTS de la zone doivent rester au-dessus de la vague (z-index 0, cf. #rhWave) — seule
   .rh-wave elle-même (premier enfant du DOM, déjà z-index 0) reste en dessous. */
#page-rhythm .rh-wave-zone>:not(.rh-wave){position:relative;z-index:1}
#page-rhythm .rh-toolbar{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}
#page-rhythm .rh-select{padding:4px 7px;border-radius:var(--r);border:1px solid var(--bd);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.65rem;cursor:pointer;outline:none}
/* #rhPatternSearch réutilise .rh-select (même look que le filtre de genre voisin) mais cursor:pointer n'a
   pas de sens pour un champ texte — override ciblé plutôt que dupliquer toute la règle. */
#page-rhythm input.rh-select{cursor:text}
#page-rhythm input.rh-select::placeholder{color:var(--tx3)}
/* Version compacte du <select> de signature rythmique (demande explicite : "adapter les boutons 4/4...
   même style que les autres") — hauteur alignée sur .pg (26px)/.rbtn-underline voisins, sans dupliquer
   .rh-select (encore utilisée telle quelle ailleurs dans ce module — éditeur, menu de coup, etc.). */
#page-rhythm .rh-select-compact{height:26px;padding:0 6px;border-radius:5px;border:1px solid var(--bd);background:var(--s2);color:var(--tx2);font-family:var(--fm);font-size:.62rem;cursor:pointer;outline:none;box-sizing:border-box}
#page-rhythm .rh-select-compact:hover{color:var(--tx);border-color:var(--bd2)}
/* Éditeur replié : pas de marge sous le titre (rien en dessous à espacer, cf. #rhEditorBody display:none)
   — demande explicite : "réduire sa taille verticale lorsqu'il est replié, moins d'espace vertical vide
   inutile". La classe est retirée par rhToggleEditorCollapsed() (app.js) au dépli, rendant la marge normale. */
#page-rhythm .rh-editor-collapsed .rh-card-title{margin-bottom:0}
#page-rhythm .rh-card-title:hover .rh-select{border-color:var(--bd2)}
#page-rhythm .rh-play-btn{height:40px;min-width:90px;border-radius:7px;border:2px solid var(--c-root);background:transparent;color:var(--c-root);font-family:var(--fd);font-size:1.1rem;font-weight:700;cursor:pointer;transition:all var(--tr);user-select:none}
#page-rhythm .rh-play-btn.playing{background:transparent;color:var(--c-root);border-color:var(--c-root);opacity:1}
#page-rhythm .rh-tempo{font-family:var(--fm);font-size:1rem;font-weight:700;color:var(--c-root);min-width:50px;text-align:center}
/* Ligne d'accords alignée par mesure, au-dessus de la grille (une progression liée depuis Progressions) */
#page-rhythm .rh-chord-row{display:grid;gap:4px;margin-top:.5rem;width:100%;overflow-x:auto}
#page-rhythm .rh-chord-lbl{font-family:var(--fm);font-size:.6rem;font-weight:700;color:#111;text-align:center;padding:2px 0;border-radius:5px 5px 0 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* Grille visualiseur */
#page-rhythm .rh-grid-wrap{margin-top:.5rem;width:100%}
/* Boutons vider ligne/colonne (demande explicite) — même taille/forme que .rh-cell, largeur/hauteur posées
   en JS par rhRenderGrid (variables --rh-cell-w/--rh-cell-h, cf. son commentaire) plutôt qu'en dur ici : la
   taille réelle d'une cellule diffère entre desktop (fixe, 97px) et mobile (1fr, dépend de la largeur
   d'écran) — ce mécanisme reste correct dans les deux cas sans dupliquer la logique de breakpoint.
   Générés dynamiquement (nombre de colonnes/lignes dépend de la signature rythmique). Rangée de colonnes
   au-dessus de la grille, alignée sur ses colonnes ; le bouton "tout vider" occupe le coin supérieur droit
   (demande explicite), aligné avec la colonne de boutons de ligne juste en dessous (même largeur --rh-cell-w,
   donc naturellement au-dessus d'elle grâce à gap:4px partagé par .rh-col-clear-header et .rh-grid-body). */
#page-rhythm .rh-col-clear-header{display:flex;gap:4px;align-items:flex-start}
#page-rhythm .rh-col-clear-row{display:flex;gap:4px}
#page-rhythm .rh-grid-body{display:flex;gap:4px;align-items:flex-start;margin-top:4px}
#page-rhythm .rh-row-clear-col{display:flex;flex-direction:column;gap:4px}
/* box-sizing:border-box + min-width/min-height:0 : un <button> a un min-width/min-height AUTO par défaut en
   tant qu'item flex, basé sur son padding UA natif + le texte "✕" — sans ces 2 resets, ce minimum intrinsèque
   dépassait le width/height explicites posés ici (bug rapporté : boutons vider ligne/colonne visiblement
   plus grands que les cellules .rh-cell malgré des variables --rh-cell-w/h identiques). .rh-cell n'a jamais
   eu ce problème : c'est une div (pas de padding UA de bouton) qui posait déjà min-width:0 explicitement. */
#page-rhythm .rh-clear-btn{display:flex;align-items:center;justify-content:center;width:var(--rh-cell-w,97px);height:var(--rh-cell-h,48px);min-width:0;min-height:0;box-sizing:border-box;padding:0;margin:0;border-radius:6px;background:var(--bg);border:1px solid var(--bd);color:var(--tx3);cursor:pointer;font-family:var(--fm);font-size:.85rem;transition:color .08s,background-color .08s,border-color .08s;user-select:none;flex-shrink:0}
#page-rhythm .rh-clear-btn:hover{background:var(--s3);color:var(--tx);border-color:var(--bd2)}
/* Bouton "tout vider" (coin sup. droit) : légèrement plus marqué que les boutons ligne/colonne — action
   plus destructive (efface l'intégralité du pattern), doit se distinguer visuellement des boutons partiels. */
#page-rhythm .rh-clear-all-btn{background:var(--s2);border-color:var(--bd2);font-weight:700}
#page-rhythm .rh-clear-all-btn:hover{background:color-mix(in srgb, var(--c-root) 16%, var(--s2));border-color:var(--c-root);color:var(--tx)}
/* Vague colorée : représente le GESTE de la main droite qui strumme — chaque coup doit se lire comme un
   "claquement" net et séparé, même deux downstrokes consécutifs rapprochés (retour explicite : "je puisse
   lire un double downstroke par 2 mouvements de vague vers le bas, pas un seul mouvement qui représente les
   deux... réfléchis à une manière de la rendre plus naturelle et réactive, le but est la lisibilité"). Une
   simple `transition` (essai précédent) fondait un coup dans le suivant dès qu'ils se rapprochaient (la
   retombée du 1er chevauchait la montée du 2e). Remplacé par un @keyframes à profil ASYMÉTRIQUE (montée
   quasi instantanée à pleine intensité, puis retombée rapide mais nette avant le coup suivant — cf.
   rhWaveHit), rejoué en entier à CHAQUE coup via rhUpdateWave (retire/reflow/rajoute la classe), même quand
   la direction ne change pas d'un coup à l'autre — deux coups de même sens redémarrent donc chacun leur
   propre "claquement" au lieu de fusionner. Bande ANCRÉE (transform-origin haut pour down, bas pour up),
   pleine hauteur (~42%, restaurée après l'essai "balayage traversant" jugé moins lisible). Prend TOUTE la
   largeur de la carte. Masquée (classe .visible retirée) quand rhState.waveEnabled est coupé. */
/* scaleY seul (essai précédent) ne donne aucun sentiment de DÉPLACEMENT — juste un fondu d'intensité sur
   place (retour explicite : "semble ne pas avoir de mouvement réel haut/bas"). La bande a maintenant une
   hauteur fixe modeste (55% de la carte, pas toute sa hauteur) et se TRANSLATE réellement en plus de claquer
   : pour un downstroke elle part du haut et glisse vers le bas (translateY 0→45%) en même temps qu'elle
   apparaît puis s'efface, pour un upstroke l'inverse (bas→haut) — un vrai trajet court et net, cohérent avec
   le geste physique d'un coup de médiator, pas juste un grandissement statique. */
/* Trajet précédent (translateY relatif à la hauteur de la bande elle-même, ancrée top/bottom) ne couvrait
   qu'une portion centrale de la carte, jamais vraiment le haut ET le bas (retour explicite : "j'aimerais que
   la vague parcoure toute la zone de la card, pas juste une partie centrale, la durée est bonne comme ça").
   La bande est maintenant positionnée en % de la hauteur TOTALE de la carte (top animé directement, plus une
   position ancrée + translateY partiel) : pour un downstroke elle part du tout haut (top:-55%, hors champ)
   et parcourt jusqu'au tout bas (top:100%, hors champ également) — donc TOUTE la hauteur de la carte est
   traversée à un moment ou un autre du trajet, pas seulement son centre. Durée/profil de claquement (montée
   quasi instantanée à 15%, retombée jusqu'à 100%) inchangés, jugés bons par l'utilisateur. */
#page-rhythm .rh-wave{position:absolute;inset:0;pointer-events:none;z-index:0;overflow:hidden}
#page-rhythm .rh-grid-body{position:relative;isolation:isolate}
#page-rhythm .rh-grid-body>:not(.rh-wave){position:relative;z-index:1}
/* Les fonds translucides laissent apparaître la même vague verticale que sous la sine. */
#page-rhythm.rh-cells-wave .rh-cell{background:transparent}
#page-rhythm.rh-cells-wave .rh-cell[data-stroke="down"]{background:color-mix(in srgb,var(--c-root) 16%,transparent)}
#page-rhythm.rh-cells-wave .rh-cell[data-stroke="up"]{background:color-mix(in srgb,#00acec 16%,transparent)}
/* Cause de la "cassure" signalée : le dégradé précédent finissait en COULEUR PLEINE à 100% (bord de la
   bande), donc le bord opposé de la div (juste après) retombait BRUTALEMENT à transparent — une vraie
   discontinuité nette, pas un effet de flou. Le dégradé revient maintenant à transparent aux DEUX bords
   (0% et 100%), avec un pic de couleur pleine au centre (50%) — un vrai fondu sans arête dure nulle part
   dans la bande, quelle que soit la portion visible une fois rognée par overflow:hidden du parent. */
/* 2026-10-10 : top:0 + opacity:0 au repos ; le claquement (translateY+opacity) est joué en WAAPI par _rhFlashPlay (rythmique.js),
   fini top/filter:blur animés (rendu natif plein cadre à chaque frame). */
/* Même bande et mêmes couleurs sous la sinusoïde et sous les blocs. */
#page-rhythm :is(.rh-wave-bar,.rh-sine-flash){position:absolute;left:0;right:0;top:0;height:55%;opacity:0;z-index:0;pointer-events:none}
#page-rhythm :is(.rh-wave-bar,.rh-sine-flash)[data-dir="down"]{background:linear-gradient(to bottom, transparent 0%, color-mix(in srgb, var(--c-root) 85%, transparent) 50%, transparent 100%)}
#page-rhythm :is(.rh-wave-bar,.rh-sine-flash)[data-dir="up"]{background:linear-gradient(to top, transparent 0%, color-mix(in srgb, #00acec 85%, transparent) 50%, transparent 100%)}
/* 0-15% : montée quasi instantanée (claquement immédiat), déjà bien engagée dans le haut de la carte ; 15-
   100% : continue de glisser vers le bas jusqu'à sortir complètement par en dessous en s'effaçant — le
   mouvement ET le fondu se terminent ensemble. Symétrique pour up (bas→haut). */
/* Bande "Vague" sinusoïdale (mode 'sine', cf. _rhRenderSine dans rythmique.js) : SVG en coordonnées pixel
   dessiné dans ce conteneur de hauteur fixe (120px, 100px mobile — le JS relit clientHeight). Purement
   visuelle : pointer-events:none partout, la grille dessous reste l'outil d'édition. Couleurs = celles de
   la grille (down=--c-root, up=#00acec, mute/vide=--tx3) pour que le point se lise comme la cellule. */
#page-rhythm .rh-sine{position:relative;height:120px;margin-top:.5rem;box-sizing:border-box;border-radius:var(--r);background:color-mix(in srgb,var(--s2) 70%,transparent);border:1px solid var(--bd);overflow:hidden;pointer-events:none;user-select:none}
#page-rhythm .rh-sine svg{display:block;position:absolute;inset:0;z-index:1}
#page-rhythm .rh-sine .sn-mid{stroke:var(--tx3);stroke-width:1;stroke-dasharray:3 4;opacity:.55}
#page-rhythm .rh-sine .sn-beat{stroke:var(--tx3);stroke-width:1;stroke-dasharray:1 3;opacity:.4}
#page-rhythm .rh-sine .sn-beat.sn-bar{opacity:.8}
#page-rhythm .rh-sine .sn-lbl{fill:var(--tx2);font-family:var(--fm);font-size:11px;font-weight:700}
#page-rhythm .rh-sine .sn-lbl.sn-and{fill:var(--tx3);font-weight:400}
#page-rhythm .rh-sine .sn-curve{fill:none;stroke:var(--tx3);stroke-width:1.6;opacity:.75;stroke-linejoin:round}
#page-rhythm .rh-sine .sn-str{stroke:var(--tx3);opacity:.4}
#page-rhythm .rh-sine .sn-slbl{fill:var(--tx3);font-family:var(--fm);font-size:10px;opacity:.8}
#page-rhythm .rh-sine .sn-curve.sn-ghost{opacity:.3;stroke-dasharray:3 3}
#page-rhythm .rh-sine .sn-accpath{fill:none;stroke-width:3.4;stroke-linecap:round;stroke-linejoin:round;opacity:.85}
#page-rhythm .rh-sine .d-down .sn-accpath{stroke:var(--c-root)}
#page-rhythm .rh-sine .d-up .sn-accpath{stroke:#00acec}
#page-rhythm .rh-sine .d-mute .sn-accpath{stroke:var(--tx3)}
#page-rhythm .rh-sine .sn-main{transform-box:fill-box;transform-origin:center}
#page-rhythm .rh-sine .d-down .sn-main,#page-rhythm .rh-sine .d-down circle:only-child{fill:var(--c-root)}
#page-rhythm .rh-sine .d-up .sn-main,#page-rhythm .rh-sine .d-up circle:only-child{fill:#00acec}
#page-rhythm .rh-sine .d-mute .sn-main,#page-rhythm .rh-sine .d-mute circle:only-child{fill:var(--tx3)}
#page-rhythm .rh-sine .sn-x{stroke:var(--tx3);stroke-width:1.3;stroke-linecap:round;fill:none}
/* Coup basse seule dans la Vague : un seul point plein sur la corde jouée, cerné d'un anneau (même teinte que le point). */
#page-rhythm .rh-sine .sn-bassring{fill:none;stroke-width:1.3;pointer-events:none}
#page-rhythm .rh-sine .d-down .sn-bassring{stroke:var(--c-root)}
#page-rhythm .rh-sine .d-up .sn-bassring{stroke:#00acec}
#page-rhythm .rh-sine .rh-sine-dot.k-ghost .sn-bassring{opacity:.45}
#page-rhythm .rh-sine .sn-acc{fill:none;stroke:var(--c-root);stroke-width:1.6}
/* 2026-10-10 : curseur et halos de pulsation = calques HTML AU-DESSUS du svg (z 2/3), animés en transform/opacity seulement
   par WAAPI (_rhCursorSync/_rhSinePulse) — plus d'attributs SVG réécrits par frame ni de filter:brightness. */
#page-rhythm .rh-sine .sn-ov{position:absolute;inset:0;pointer-events:none}
#page-rhythm .rh-sine .sn-halos{z-index:2}
#page-rhythm .rh-sine .sn-cur{z-index:3;display:none}
#page-rhythm .rh-sine .sn-cur i{position:absolute;display:block}
#page-rhythm .rh-sine .sn-cur-l{left:-.5px;top:3px;width:1px;background:var(--tx);opacity:.28}
#page-rhythm .rh-sine .sn-cur-c{left:-7px;top:-7px;width:14px;height:14px;box-sizing:border-box;border-radius:50%;background:var(--bg);border:2px solid var(--tx)}
#page-rhythm .rh-sine .sn-halo{position:absolute;left:0;top:0;width:10px;height:10px;border-radius:50%;opacity:0}
#page-rhythm .rh-sine .sn-halo.d-down{background:color-mix(in srgb,var(--c-root) 75%,#fff)}
#page-rhythm .rh-sine .sn-halo.d-up{background:color-mix(in srgb,#00acec 75%,#fff)}
/* Coup fantôme dans la Vague : point plus petit (rayon réduit côté JS) et ~45 % d'opacité ; pas d'.sn-accpath. */
#page-rhythm .rh-sine .rh-sine-dot.k-ghost .sn-main,#page-rhythm .rh-sine .rh-sine-dot.k-ghost .sn-x{opacity:.45}
@media (max-width:640px){#page-rhythm .rh-sine{height:100px}}
/* Mobile : colonnes en 1fr (largeur = grille répartie sur la largeur dispo, PAS de taille fixe) — la
   taille fixe façon "noires" (cf. bloc desktop @media(min-width:1024px) plus bas, qui écrase gridCols en
   repeat(N,97px) via JS) ferait déborder l'écran sur un petit mobile en résolution fine (32 pas). Le
   scroll horizontal reste donc le comportement mobile, comme avant ce chantier. */
/* flex-grow posé dynamiquement par rhRenderGrid (0 sur desktop = colonnes à largeur fixe, la grille ne doit
   pas s'étirer au-delà de son propre contenu réel, sinon .rh-row-clear-col est poussé hors champ dans
   .rh-grid-body ; 1 sur mobile = colonnes en 1fr, la grille DOIT au contraire remplir la largeur dispo pour
   que ce 1fr ait un sens). */
/* margin-top retiré (était .5rem) : désalignait la grille par rapport à .rh-row-clear-col, qui vit dans le
   même flex .rh-grid-body sans cette marge — .rh-grid-body porte déjà son propre margin-top:4px pour
   l'espacement sous les boutons de colonnes, inutile de le dupliquer ici en plus. */
#page-rhythm .rh-grid{display:grid;gap:4px;min-width:0;overflow-x:auto}
/* Cellules RECTANGULAIRES horizontales (demande explicite, remplace l'ancien carré aspect-ratio:1) —
   aspect-ratio:2/1 : deux fois plus large que haute, cohérent avec une grille fixée à 8 colonnes/ligne
   (cf. gridCols dans rhRenderGrid) quel que soit le nombre total de pas (jusqu'à 4 lignes pour 32 pas). */
#page-rhythm .rh-cell{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;aspect-ratio:2/1;border-radius:6px;background:var(--s2);border:1px solid var(--bd);cursor:pointer;transition:border-color .08s;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;touch-action:manipulation;font-family:var(--fm);min-width:0;overflow:hidden}
#page-rhythm .rh-cell:hover{border-color:var(--bd2)}
#page-rhythm .rh-cell.beat-start{border-left:2px solid var(--tx3)}
#page-rhythm .rh-cell .rh-arrow{font-size:clamp(.6rem,4cqw,1.1rem);font-weight:700;line-height:1}
#page-rhythm .rh-cell .rh-lbl{font-size:max(10px,.45rem);color:var(--tx3);text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;overflow:hidden}
#page-rhythm .rh-cell{container-type:inline-size}
#page-rhythm .rh-cell[data-stroke="down"] .rh-arrow{color:var(--c-root)}
#page-rhythm .rh-cell[data-stroke="up"] .rh-arrow{color:#00acec}
/* Fond teinté par direction (pas juste la couleur du symbole) — indice visuel fort demandé explicitement
   ("pour avoir un indice visuel fort sur le type de mouvement à jouer"), même logique de couleur que le
   symbole ci-dessus (var(--c-root) pour down, #00acec pour up) mais en fond très atténué pour rester
   lisible sous le symbole et le libellé. color-mix garde la teinte cohérente avec le thème clair/sombre
   actif (var(--s2) comme base) plutôt qu'une couleur fixe qui jurerait sur un thème clair. */
#page-rhythm .rh-cell[data-stroke="down"]{background:color-mix(in srgb, var(--c-root) 16%, var(--s2))}
#page-rhythm .rh-cell[data-stroke="up"]{background:color-mix(in srgb, #00acec 16%, var(--s2))}
#page-rhythm .rh-cell[data-stroke="mute"]{background:var(--s3)}
#page-rhythm .rh-cell[data-stroke="mute"] .rh-arrow{color:var(--tx3)}
#page-rhythm .rh-cell.accented{box-shadow:inset 0 0 0 2px var(--c-root)}
#page-rhythm .rh-cell.accented .rh-arrow{font-weight:900;transform:scale(1.12)}
#page-rhythm .rh-cell[data-stroke="rest"] .rh-arrow{opacity:.15}
/* Note fantôme (3e niveau de dynamique, exclusive avec .accented) : « (↓) » plus petit et translucide, sans
   cadre. transform/opacity plutôt que font-size : ce dernier est écrasé par des !important de la media query
   mobile (.rh-arrow{font-size:16px!important}). */
#page-rhythm .rh-cell.ghost .rh-arrow{transform:scale(.72);opacity:.6;font-weight:500}
/* Coup « basse seule » (pattern.bass[i], demande explicite) : petit « B » discret collé à la flèche (en exposant,
   couleur neutre pour ne pas concurrencer la teinte down). Dimensionné en em : suit la taille de la flèche, y
   compris les font-size !important de la media query mobile. */
#page-rhythm .rh-cell .rh-bass-tag{font-size:.5em;font-weight:800;letter-spacing:0;margin-left:.15em;color:var(--tx2);opacity:.9;vertical-align:.55em;line-height:1;font-family:var(--fm)}
@media (max-width:1023px){#page-rhythm .rh-cell .rh-arrow{font-size:clamp(1rem,22cqw,1.6rem)}}
#page-rhythm .rh-cell.playhead{outline:2px solid var(--tx);outline-offset:1px;background:color-mix(in srgb, var(--tx) 22%, var(--s2))}
/* Flash coloré du fond quand la case est ACTIVE (playhead), teinté selon le sens du coup (down/up) plutôt
   qu'un gris neutre — repère visuel fort et immédiat, cohérent avec l'accent de couleur déjà présent sur
   les cases down/up au repos (cf. règles [data-stroke] ci-dessus). */
#page-rhythm .rh-cell.playhead[data-stroke="down"]{outline-color:var(--c-root);background:color-mix(in srgb, var(--c-root) 55%, var(--s2))}
#page-rhythm .rh-cell.playhead[data-stroke="up"]{outline-color:#00acec;background:color-mix(in srgb, #00acec 55%, var(--s2))}
/* Le playhead garde son contour ; l'animation de fond vient seulement de la vague verticale. */
/* Mini menu de choix direct du symbole (appui long sur une case) */
.rh-stroke-menu{position:fixed;z-index:800;display:flex;gap:3px;padding:5px;border-radius:8px;background:var(--sf);border:1px solid var(--bd2);box-shadow:0 6px 18px -4px rgba(0,0,0,.5)}
.rh-stroke-menu-btn{width:34px;height:34px;padding:0;margin:0;border-radius:6px;border:1px solid var(--bd);background:var(--s2);color:var(--tx);font-family:inherit;font-size:1rem;line-height:1;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .1s}
.rh-stroke-menu-btn:hover{border-color:var(--c-root);color:var(--c-root);background:var(--s3)}
.rh-stroke-menu-sep{width:1px;align-self:stretch;background:var(--bd)}
.rh-stroke-menu-accent{font-style:italic;font-size:.85rem}
.rh-stroke-menu-accent.on{border-color:var(--c-root);color:var(--c-root);background:var(--s3);box-shadow:inset 0 0 0 1px var(--c-root)}
.rh-stroke-menu-accent.disabled{opacity:.3;cursor:not-allowed}
/* Bouton « Fantôme » (3e niveau de dynamique) : même gabarit que l'accent, rendu atténué quand inactif. */
.rh-stroke-menu-ghost{font-size:.8rem;opacity:.85}
.rh-stroke-menu-ghost.on{border-color:var(--c-root);color:var(--c-root);background:var(--s3);box-shadow:inset 0 0 0 1px var(--c-root);opacity:1}
.rh-stroke-menu-ghost.disabled{opacity:.3;cursor:not-allowed}
/* Bouton « B » (basse seule) : même gabarit que l'accent/le fantôme ; grisé hors d'un coup down. */
.rh-stroke-menu-bass{font-size:.85rem;opacity:.85}
.rh-stroke-menu-bass.on{border-color:var(--c-root);color:var(--c-root);background:var(--s3);box-shadow:inset 0 0 0 1px var(--c-root);opacity:1}
.rh-stroke-menu-bass.disabled{opacity:.3;cursor:not-allowed}
/* Menu p-lock groovebox (appui long sur un pas) — vélocité/hauteur/decay/tone/probabilité/micro-timing/
   ratchet, cf. GROOVE_LOCK_FIELDS dans app.js. Popover plus large que .rh-stroke-menu (7 curseurs, pas
   quelques boutons), même famille visuelle (position:fixed, fond --sf, ombre) pour rester cohérent avec le
   reste des menus contextuels de l'app (accents/appui long Rythmique). */
/* Agrandi (demande explicite : "agrandir le popup, aligner tous les sliders") — 180px→280px : au 1er
   gabarit, le libellé le plus long ("Micro-timing", 13 caractères) débordait de son min-width:62px et
   passait à la ligne, ce qui décalait visuellement CE slider par rapport aux 6 autres (chaque ligne
   calculait sa propre hauteur/alignement indépendamment dès qu'un libellé wrappait). min-width du libellé
   relevé à 78px (mesuré : suffisant pour "Micro-timing" sur une seule ligne à cette police), donc les 7
   sliders démarrent maintenant tous exactement à la même position X — c'est CE réglage, pas une propriété
   de chaque <input> individuel, qui les "aligne". */
.groove-lock-menu{position:fixed;z-index:800;display:flex;flex-direction:column;gap:7px;padding:12px 14px;border-radius:8px;background:var(--sf);border:1px solid var(--bd2);box-shadow:0 6px 18px -4px rgba(0,0,0,.5);width:280px}
.groove-lock-menu-title{font-family:var(--fm);font-size:.68rem;font-weight:700;color:var(--tx);text-transform:uppercase;letter-spacing:.04em;margin-bottom:4px}
.groove-lock-row{display:flex;align-items:center;gap:.5rem}
.groove-lock-row span:first-child{font-family:var(--fm);font-size:.62rem;color:var(--tx3);min-width:78px;flex-shrink:0}
.groove-lock-row input[type="range"]{flex:1;min-width:0;height:7px;cursor:pointer;-webkit-appearance:none;appearance:none;background:var(--s3);border-radius:3px;accent-color:var(--c-root)}
.groove-lock-row span:last-child{font-family:var(--fm);font-size:.62rem;font-weight:700;color:var(--c-root);min-width:42px;flex-shrink:0;text-align:right}
.groove-lock-menu-reset{margin-top:6px;padding:6px 10px;border:1px solid var(--bd);background:var(--s3);color:var(--tx2);border-radius:5px;cursor:pointer;font-family:var(--fm);font-size:.62rem;font-weight:700}
.groove-lock-menu-reset:hover{border-color:var(--c-root);color:var(--c-root)}
/* Bibliothèque de patterns */
#page-rhythm .rh-pattern-row{display:flex;align-items:center;gap:.6rem;padding:.5rem .6rem;border-radius:6px;border:1px solid var(--bd);background:var(--s2);cursor:pointer;transition:all .12s}
/* Audit perf 2026-09-28 : 64 lignes / ~1600 nœuds sur ~9000 px, presque tous hors écran, mis en page et peints à
   chaque visite de Rythme. content-visibility:auto saute les lignes hors écran ; auto 120px = hauteur retenue
   après le 1er rendu (pas de saut de la barre de défilement ensuite). */
#page-rhythm .rh-pattern-row{content-visibility:auto;contain-intrinsic-size:auto 120px}
#page-rhythm .rh-pattern-row:hover{border-color:var(--bd2);background:var(--s3)}
#page-rhythm .rh-pattern-row.active{border-color:var(--c-root);background:var(--s3)}
/* Fond de module actif : ces états (hover/actif) ont une spécificité ID+classe qui l'emporterait sinon sur
   la règle générique de verre dépoli (.page.module-bg-enabled .rh-pattern-row, classe seule) — !important
   nécessaire ici pour garder le verre visible même au survol/sélection. */
.page.module-bg-enabled#page-rhythm .rh-pattern-row,
.page.module-bg-enabled#page-rhythm .rh-pattern-row:hover,
.page.module-bg-enabled#page-rhythm .rh-pattern-row.active{
  background:color-mix(in srgb,var(--sf) var(--glass-opacity,84%),transparent)!important;
  
}
#page-rhythm .rh-pattern-name{font-family:var(--fd);font-weight:700;font-size:.8rem;color:var(--tx)}
#page-rhythm .rh-pattern-meta{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx3)}
/* margin-left:auto retiré (demande explicite : "déplacer l'aperçu sous le titre... avant la description") —
   l'aperçu vivait auparavant à droite de toute la ligne (flex sibling de la colonne titre/meta/tags/refs),
   margin-left:auto le poussait donc en butée droite ; désormais il est DANS le flux vertical de cette
   colonne (cf. app.js rhRenderPatternList), cette règle n'aurait plus de sens. flex-wrap:wrap : 16 cellules
   fixes (cf. commentaire app.js sur le nombre constant) peuvent dépasser la largeur disponible sur mobile
   étroit, préférer un retour à la ligne à un débordement horizontal. */
/* Style repris de l'export PNG du module (exportRhythmPNG, cf. app.js) — demande explicite : "utilise un
   style plus proche de cette capture (plus lisible)", capture montrant l'export PNG. Différences par rapport
   aux 2 itérations précédentes de cet aperçu : chaque case (MÊME une case vide/pad) est maintenant une vraie
   boîte à bordure arrondie individuelle — c'est CETTE bordure par case qui délimite visuellement la grille,
   plus besoin d'un contour extérieur global ni de traits ::after entre les cases (retirés, redondants avec
   les bordures de case elles-mêmes). Le regroupement par temps se fait par un espacement (gap) plus large
   plutôt qu'un trait supplémentaire, même logique que la grille d'édition principale (.rh-cell.beat-start). */
/* Aperçu de la bibliothèque : grille ALIGNÉE (jamais de retour à la ligne arbitraire). 16 cases sur une ligne
   quand la colonne est assez large (>= 440px), sinon 8 + 8 sur deux lignes, la 2e alignée verticalement sur la
   1re. Pistes explicites (4 cases, séparateur de temps 11px + gaps = 17px, 4 cases…) : chaque case est
   placée par grid-column (nth-child) plutôt que par une marge, ce qui garde les colonnes identiques d'une ligne
   à l'autre. Le conteneur est .rh-pattern-main (container-type:inline-size). */
#page-rhythm .rh-pattern-main{container-type:inline-size}
#page-rhythm .rh-pattern-preview{display:grid;grid-template-columns:repeat(4,22px) 11px repeat(4,22px) 11px repeat(4,22px) 11px repeat(4,22px);gap:3px;width:fit-content}
#page-rhythm .rh-mini-cell:nth-child(16n+1){grid-column:1}
#page-rhythm .rh-mini-cell:nth-child(16n+2){grid-column:2}
#page-rhythm .rh-mini-cell:nth-child(16n+3){grid-column:3}
#page-rhythm .rh-mini-cell:nth-child(16n+4){grid-column:4}
#page-rhythm .rh-mini-cell:nth-child(16n+5){grid-column:6}
#page-rhythm .rh-mini-cell:nth-child(16n+6){grid-column:7}
#page-rhythm .rh-mini-cell:nth-child(16n+7){grid-column:8}
#page-rhythm .rh-mini-cell:nth-child(16n+8){grid-column:9}
#page-rhythm .rh-mini-cell:nth-child(16n+9){grid-column:11}
#page-rhythm .rh-mini-cell:nth-child(16n+10){grid-column:12}
#page-rhythm .rh-mini-cell:nth-child(16n+11){grid-column:13}
#page-rhythm .rh-mini-cell:nth-child(16n+12){grid-column:14}
#page-rhythm .rh-mini-cell:nth-child(16n+13){grid-column:16}
#page-rhythm .rh-mini-cell:nth-child(16n+14){grid-column:17}
#page-rhythm .rh-mini-cell:nth-child(16n+15){grid-column:18}
#page-rhythm .rh-mini-cell:nth-child(16n+16){grid-column:19}
@container (max-width:439px){
  #page-rhythm .rh-pattern-preview{grid-template-columns:repeat(4,22px) 11px repeat(4,22px)}
  #page-rhythm .rh-mini-cell:nth-child(8n+1){grid-column:1}
  #page-rhythm .rh-mini-cell:nth-child(8n+2){grid-column:2}
  #page-rhythm .rh-mini-cell:nth-child(8n+3){grid-column:3}
  #page-rhythm .rh-mini-cell:nth-child(8n+4){grid-column:4}
  #page-rhythm .rh-mini-cell:nth-child(8n+5){grid-column:6}
  #page-rhythm .rh-mini-cell:nth-child(8n+6){grid-column:7}
  #page-rhythm .rh-mini-cell:nth-child(8n+7){grid-column:8}
  #page-rhythm .rh-mini-cell:nth-child(8n+8){grid-column:9}
}
#page-rhythm .rh-pattern-preview span{font-size:.7rem;color:var(--tx2);font-family:var(--fm)}
#page-rhythm .rh-mini-cell{width:22px;height:22px;border-radius:5px;border:1.5px solid var(--bd2);background:rgba(255,255,255,.04);display:flex;align-items:center;justify-content:center;font-size:.85rem;font-weight:700;color:var(--tx2);flex-shrink:0}
/* Différence entre temps accentuée (demande explicite, 2e passe — la 1ère, margin+bordure de la seule 1ère
   case du groupe, jugée trop discrète) : espace nettement plus large entre les groupes de 4 (14px, quasi
   5x le gap normal de 3px) + fond légèrement teinté en alternance sur 2 des 4 cases de chaque groupe pour
   que le regroupement se voie même sans compter les cases une à une. nth-child(8n+1/2/3/4) cible le 1er
   temps de chaque paire de temps (groupe 1, 3 sur 4 temps) : alternance visuelle claire entre temps
   consécutifs, plus lisible qu'une teinte uniforme sur TOUS les temps qui ne distinguerait rien. */
#page-rhythm .rh-mini-cell:nth-child(8n+1),#page-rhythm .rh-mini-cell:nth-child(8n+2),#page-rhythm .rh-mini-cell:nth-child(8n+3),#page-rhythm .rh-mini-cell:nth-child(8n+4){background:rgba(255,255,255,.09)}
#page-rhythm .rh-mini-cell:nth-child(4n+1){border-color:var(--tx2);border-width:2px}
/* Couleurs alignées sur l'export PNG (COL_DOWN/COL_UP de exportRhythmPNG) plutôt que sur --c-root (variable
   selon le thème couleur choisi) — la référence demandée est un orange/bleu fixes et saturés, cohérence
   avec CE visuel précis plutôt qu'avec la grille d'édition principale (qui, elle, suit --c-root à dessein). */
#page-rhythm .rh-mini-cell[data-stroke="down"],#page-rhythm .rh-mini-cell[data-stroke="accent"]{color:#ff7e00}
#page-rhythm .rh-mini-cell[data-stroke="up"]{color:#00acec}
#page-rhythm .rh-mini-cell[data-stroke="mute"]{color:var(--tx3)}
#page-rhythm .rh-mini-cell[data-stroke="rest"]{opacity:.4}
/* pad (demande explicite : "toujours afficher 16 blocs, avec des vides si besoin") — case de REMPLISSAGE
   pour un pattern plus court que 16 cellules, distincte d'un vrai 'rest' (silence VOULU dans le rythme).
   Reste une vraie boîte à bordure (cohérent avec le style de référence, cf. commentaire plus haut : CHAQUE
   case a sa bordure, y compris vide) mais plus discrète — bordure atténuée + pas de fond, pour qu'on la
   perçoive comme "case sans contenu" et non comme un pas silencieux VOULU (rest) au sein du rythme. */
#page-rhythm .rh-mini-cell[data-stroke="pad"]{background:transparent;border-color:var(--bd)}

/* ═══ MODULE MÉLODIE (piano-roll, porté depuis melodia.html) ═══ */
/* Séquenceur (refonte 2026-09-24, melodie.js) : barre fixe + corps défilant (pistes, éditeur, outils). */
#page-melodie{overflow:hidden}
#page-melodie.active{display:flex;flex-direction:column}
#page-melodie .sq-bar{flex:none;display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;padding:6px 8px;background:var(--sf);border-bottom:1px solid var(--bd)}
#page-melodie .sq-transport,#page-melodie .sq-opts{display:flex;align-items:center;gap:5px;flex-wrap:wrap;min-width:0}
#page-melodie .sq-opts{margin-left:auto}
#page-melodie .sq-btn{width:32px;height:30px;border-radius:8px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;touch-action:manipulation}
#page-melodie .sq-btn svg{width:16px;height:16px}
#page-melodie .sq-btn:disabled{opacity:.35;cursor:default}
#page-melodie .sq-rec{color:#e5484d}
#page-melodie .sq-rec.on{background:#e5484d;color:#fff;border-color:#e5484d;animation:gslPulse 1s infinite}
#page-melodie .sq-rec.armed{border-color:#e5484d;animation:gslPulse .5s infinite}
#page-melodie .sq-play.on{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-melodie .sq-btn.confirm{background:#e5484d;border-color:#e5484d;color:#fff}
#page-melodie .sq-status{font-family:var(--fm);font-size:.55rem;color:var(--tx2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:40vw}
#page-melodie .sq-bpm,#page-melodie .sq-oct,#page-melodie .sq-cursor{display:inline-flex;align-items:center;gap:4px;font-family:var(--fm);font-size:.62rem;color:var(--tx2)}
#page-melodie .sq-bpm b,#page-melodie .sq-oct b{color:var(--tx);min-width:26px;text-align:center;font-size:.72rem}
#page-melodie .sq-bpm small{font-size:max(10px,.5rem);margin-right:2px}
#page-melodie .sq-mini-btn{display:inline-flex;align-items:center;justify-content:center;min-width:30px;height:30px;border-radius:7px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.7rem;font-weight:700;cursor:pointer;padding:0 6px;touch-action:manipulation}
#page-melodie .sq-body{flex:1;min-height:0;overflow-y:auto;padding:8px;display:block;box-sizing:border-box}
#page-melodie .sq-body>*+*{margin-top:8px}
#page-melodie .sq-guide{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:8px;border:1px dashed color-mix(in srgb,var(--c-root) 50%,var(--bd));font-family:var(--fm);font-size:.66rem;color:var(--tx)}
#page-melodie .sq-guide-lbl{color:var(--tx3);font-size:max(10px,.55rem);text-transform:uppercase}
#page-melodie .sq-guide-list{flex:1;min-width:0;color:var(--c-root);font-weight:700}
#page-melodie .sq-tracks{display:flex;flex-direction:column;gap:5px}
#page-melodie .sq-track{display:grid;grid-template-columns:minmax(80px,140px) minmax(120px,1fr) auto;align-items:center;gap:6px;padding:4px 6px;border-radius:9px;background:var(--s1);border:1px solid var(--bd);border-left:5px solid hsl(var(--gsl-h) 70% 55%)}
#page-melodie .sq-track.sel{border-color:hsl(var(--gsl-h) 70% 55%);background:color-mix(in srgb,hsl(var(--gsl-h) 70% 55%) 9%,var(--s1))}
#page-melodie .sq-track.muted .sq-mini,#page-melodie .sq-track.muted .sq-tname{opacity:.45}
#page-melodie .sq-track.dubbing{box-shadow:inset 0 0 0 1px #e5484d}
#page-melodie .sq-tname{border:none;background:none;color:var(--tx);font-family:var(--fm);font-size:.68rem;font-weight:700;text-align:left;cursor:pointer;padding:6px 2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#page-melodie .sq-mini{position:relative;height:34px;border-radius:5px;background:color-mix(in srgb,var(--bg) 70%,transparent);overflow:hidden;cursor:pointer}
#page-melodie .sq-mini svg{position:absolute;inset:0;width:100%;height:100%}
#page-melodie .sq-mini rect{fill:hsl(var(--gsl-h) 70% 58%)}
#page-melodie .sq-mini rect.dr{fill:hsl(var(--gsl-h) 45% 70%)}
#page-melodie .sq-mini-bar{stroke:var(--bd);stroke-width:2;vector-effect:non-scaling-stroke}
#page-melodie .sq-mini-head{position:absolute;top:0;bottom:0;left:0;width:2px;background:var(--tx);opacity:0;pointer-events:none}
#page-melodie .sq-tctl{display:flex;align-items:center;gap:4px;flex-wrap:wrap;justify-content:flex-end}
#page-melodie .sq-tctl>button{min-width:30px;height:30px;border-radius:7px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.62rem;font-weight:700;cursor:pointer;padding:0 6px;touch-action:manipulation}
#page-melodie .sq-tctl>button.on{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-melodie .sq-tctl .sq-od{color:#e5484d}
#page-melodie .sq-tctl .sq-od.on{background:#e5484d;border-color:#e5484d;color:#fff;animation:gslPulse 1s infinite}
#page-melodie .sq-vol{width:70px;height:18px;accent-color:hsl(var(--gsl-h) 70% 55%);touch-action:none}
#page-melodie .sq-tctl .dpk-select-btn{max-width:150px}
/* largeurs fixes : sinon « Lead Saw » vs « Kit TR-808 » décale la fin de la mini-piste (mesures / tête de lecture désalignées d'une piste à l'autre) */
#page-melodie .sq-tctl .sq-voice+.dpk-select-btn,#page-melodie .sq-tctl .sq-kit+.dpk-select-btn{width:132px;min-width:132px;max-width:132px;justify-content:space-between;overflow:hidden}
#page-melodie .sq-tctl .sq-len+.dpk-select-btn{width:92px;min-width:92px;max-width:92px;justify-content:space-between}
#page-melodie .sq-tctl .sq-more+.dpk-select-btn{width:54px;min-width:54px;max-width:54px}
#page-melodie .sq-empty{font-family:var(--fm);font-size:.66rem;color:var(--tx2);padding:10px;line-height:1.5}
#page-melodie .sq-add{display:flex;gap:6px;flex-wrap:wrap}
#page-melodie .sq-add-btn,#page-melodie .sq-tbtn{height:34px;padding:0 12px;border-radius:8px;border:1px dashed var(--bd2);background:transparent;color:var(--tx);font-family:var(--fm);font-size:.64rem;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;gap:4px;touch-action:manipulation}
#page-melodie .sq-tbtn{border-style:solid;background:var(--s2)}
#page-melodie .sq-tbtn.on{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-melodie .sq-editor{min-height:120px}
#page-melodie #seqEditor:has(.gsl-ed) .sq-empty{display:none}
#page-melodie .sq-tools{border-radius:10px;background:var(--s1);border:1px solid var(--bd);padding:6px}
#page-melodie .sq-tool-tabs{display:flex;gap:4px;margin-bottom:6px}
#page-melodie .sq-tool{flex:1 1 0;height:32px;border-radius:7px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx2);font-family:var(--fm);font-size:.64rem;font-weight:700;cursor:pointer}
#page-melodie .sq-tool.on{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-melodie .sq-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
#page-melodie .sq-fx-knobs{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
/* Potentiomètres (tb303KnobEl) du panneau Effets du Séquenceur : mêmes styles que dans les Paramètres de Surface */
#page-melodie .sq-fx-knobs .tb303-knob-wrap{display:flex;flex-direction:column;align-items:center;gap:2px;width:64px}
#page-melodie .sq-fx-knobs .tb303-knob-label{font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3);text-align:center;white-space:normal;line-height:1.15;min-height:2.3em;display:flex;align-items:flex-end;justify-content:center;width:100%;box-sizing:border-box;overflow-wrap:break-word;min-width:0}
#page-melodie .sq-fx-knobs .tb303-knob-val{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--c-root);font-weight:700}
#page-melodie .sq-fx-knobs .tb303-knob-track{fill:none;stroke:var(--s3);stroke-width:4;stroke-linecap:round}
#page-melodie .sq-fx-knobs .tb303-knob-fill{fill:none;stroke:var(--c-root);stroke-width:4;stroke-linecap:round}
#page-melodie .sq-fx-knobs .tb303-knob-dot{fill:var(--tx)}
#page-melodie .sq-fx-knobs .tb303-knob-svg{touch-action:none}
#page-melodie .sq-panel>*+*{margin-top:6px}
#page-melodie .sq-lbl{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx3);text-transform:uppercase}
#page-melodie .sq-note{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx3)}
#page-melodie .sq-midi{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx3)}
#page-melodie .sq-midi .on{color:var(--c-root)}
#page-melodie .sq-pills{display:flex;flex-wrap:wrap;gap:5px}
#page-melodie .sq-pill{height:32px;padding:0 10px;border-radius:16px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.6rem;cursor:pointer;touch-action:manipulation}
#page-melodie .sq-keys{display:flex;gap:4px;overflow-x:auto;padding-bottom:2px}
#page-melodie .sq-key{flex:1 0 44px;min-width:44px;height:64px;border-radius:8px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;cursor:pointer;touch-action:none;-webkit-user-select:none;user-select:none;font-family:var(--fm)}
#page-melodie .sq-key b{font-size:.7rem}
#page-melodie .sq-key[style]{border-bottom:4px solid var(--nc)}
#page-melodie .sq-key b small{font-size:.75em;opacity:.55;margin-left:1px}
#page-melodie .sq-key span{font-size:max(10px,.55rem);color:var(--tx3)}
#page-melodie .sq-key.root{border-color:color-mix(in srgb,var(--c-root) 60%,var(--bd2))}
#page-melodie .sq-key.dr{font-size:max(10px,.58rem);font-weight:700;flex-basis:70px}
#page-melodie .sq-key.on{background:var(--c-root);color:#111}
#page-melodie .sq-key.on span{color:#111}
@media (max-width:640px){
  #page-melodie .sq-track{grid-template-columns:1fr 1.6fr}
  #page-melodie .sq-tctl{grid-column:1 / -1;justify-content:flex-start}
  #page-melodie .sq-tctl{gap:3px}
  #page-melodie .sq-tctl .dpk-select-btn{max-width:118px}
  #page-melodie .sq-tctl .sq-voice+.dpk-select-btn,#page-melodie .sq-tctl .sq-kit+.dpk-select-btn{width:112px;min-width:112px;max-width:112px}
  #page-melodie .sq-vol{width:52px}
  #page-melodie .sq-tctl>button{min-width:28px}
  #page-melodie .sq-opts{margin-left:0}
  #page-melodie .sq-status{max-width:none;flex:1 0 100%}
}

/* ── Popup flottant Accordeur (iframe vers tuner.html, module standalone séparé) ──
   z-index 8000 : au-dessus du contenu de page normal, en dessous des modales bloquantes
   (.settings-modal, onboarding) qui restent à 9999+ — le popup ne doit jamais masquer une
   modale qu'on ouvrirait par-dessus lui. Position/taille mémorisées en JS (localStorage) pour
   rouvrir au même endroit d'une session à l'autre. */
.tuner-popup{
  position:fixed;z-index:8000;
  /* 340px (min-width d'origine) était trop étroit pour les 5 boutons de la rangée "A/A+S/A+H/A+H+S/✕",
     visible seulement une fois le micro actif — le popup s'ouvrait large de silence (juste "En écoute…")
     et se redimensionnait/recassait brusquement (bouton ✕ poussé sur une 2e ligne) au premier son capté
     (bug rapporté). 400px minimum garantit que cette rangée tient toujours sur une seule ligne. */
  width:400px;min-width:400px;min-height:340px;
  background:var(--sf);border:1px solid var(--bd);border-radius:var(--rl);
  box-shadow:0 12px 40px -8px rgba(0,0,0,.5);
  display:flex;flex-direction:column;overflow:hidden;
  resize:both;
}
.tuner-popup-handle{
  display:flex;align-items:center;justify-content:space-between;
  padding:8px 8px 8px 12px;border-bottom:1px solid var(--bd);
  background:var(--s2);cursor:grab;flex-shrink:0;user-select:none;-webkit-user-select:none;
}
.tuner-popup-handle:active{cursor:grabbing}
.tuner-popup-title{font-family:var(--fm);font-size:.68rem;font-weight:700;color:var(--tx);letter-spacing:.02em}
/* Style visuel commun désormais dans .modal-close-btn/.modal-close-btn-sm (cf. ~ligne 642) — cette classe
   reste posée dans le HTML (aussi utilisée pour le bouton "détacher" ⇱, pas seulement fermer) mais ne
   porte plus les propriétés de base. Hover propre conservé : couleur de texte finale légèrement différente
   (--tx au lieu de --c-root) de celle des autres boutons fermer, sans raison connue de changer ça ici. */
.tuner-popup-close:hover{border-color:var(--c-root);color:var(--tx)}
#tunerFrame{flex:1;width:100%;border:none;background:var(--bg)}
@media(max-width:480px){
  /* Plein écran plutôt que popup flottant redimensionnable : sur un viewport aussi étroit, une boîte
     déplaçable "n'a nulle part où aller" (cf. discussion) — mieux vaut occuper tout l'écran comme une vraie
     vue dédiée, cohérent avec .settings-modal qui fait déjà ce choix pour les Paramètres. Poignée de drag
     désactivée (resize:none, cursor normal) : plus rien à déplacer/redimensionner en plein écran. */
  .tuner-popup{
    width:100vw!important;height:100vh!important;height:100dvh!important;
    min-width:0!important;min-height:0!important;
    top:0!important;left:0!important;right:0!important;bottom:0!important;
    border-radius:0;border:none;resize:none;
    /* 8000 (base desktop) reste sous des éléments flottants comme #btnTabBack (9990) : sur mobile,
       le clic sur ce bouton restait cliquable PAR-DESSUS le popup en plein écran et agissait sur la page
       cachée derrière (bug rapporté — voir aussi tabGoBack/_syncBackBtn dans app.js pour le volet
       "le bouton ne ferme pas le tuner"). 9995 : au-dessus de tout élément flottant non-modal de l'app,
       mais toujours sous les vraies modales bloquantes (.settings-modal 9999, .ob-modal 10000, etc.),
       qui doivent pouvoir s'ouvrir par-dessus lui si jamais l'utilisateur y accède pendant qu'il est ouvert. */
    z-index:9995;
  }
  /* Barre de titre retirée entièrement sur mobile : elle faisait doublon avec le ✕ interne du
     tuner (rangée SILENCE/A+H+S), deux boutons de fermeture quasi identiques empilés en haut à
     droite prêtant à confusion (bug rapporté : "le bouton micro off referme l'accordeur"). Le ✕
     interne devient désormais le seul point de fermeture sur mobile (voir tuner.html,
     _tunerCloseBtnClick + app.js listener fretfaceTunerCloseRequest). */
  .tuner-popup-handle{display:none}
}

/* ── Poignée de la sidebar (cf. _sidebarResizeInit dans app.js) ───────────────────────────────────────────────────
   Trois états posés en classes sur <body> : (aucune) = sidebar complète / largeur libre ≤ 190px ; .sb-icons = pictos seuls
   (point dur, mixeur replié, seul le bouton de popup du mixeur reste, sans texte) ; .sb-hidden = repliée, il ne reste que la
   poignée en onglet à mi-hauteur. */
#sidebarHandle{position:fixed;z-index:1000;left:0;top:50%;width:13px;height:64px;transform:translateY(-50%);box-sizing:border-box;
  background:var(--s2);border:1px solid var(--bd);border-left:0;border-radius:0 8px 8px 0;cursor:col-resize;touch-action:none;
  display:flex;align-items:center;justify-content:center;user-select:none;-webkit-user-select:none}
#sidebarHandle::before{content:'';width:3px;height:26px;border-left:1px solid var(--tx3);border-right:1px solid var(--tx3);box-sizing:border-box;opacity:.7}
#sidebarHandle:hover,#sidebarHandle.dragging{border-color:var(--c-root)}
#sidebarHandle:hover::before,#sidebarHandle.dragging::before{border-color:var(--c-root);opacity:1}
body.sb-hidden #sidebarHandle{width:18px;cursor:pointer}
body.sb-hidden #sidebarHandle::before{content:'';width:11px;height:16px;border:0;background:var(--tx2);opacity:1;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M8 4l8 8-8 8%22/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%223%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M8 4l8 8-8 8%22/%3E%3C/svg%3E") center/contain no-repeat}
body.sb-hidden #sidebarHandle:hover::before{background:var(--c-root)}
body.sb-icons .sidebar-desktop{width:48px!important}
body.sb-icons .sidebar-desktop .tab-label,body.sb-icons .sidebar-desktop .tab-kbd-hint{display:none}
body.sb-icons #sidebarTabs{padding-left:0;border-left:0}
body.sb-icons .sidebar-desktop{scrollbar-width:none}
body.sb-icons .sidebar-desktop::-webkit-scrollbar{display:none}
/* Centrage exact : pas de bordure gauche (l'onglet actif est marqué par un liseré intérieur) et icône centrée sur toute la largeur */
body.sb-icons .sidebar-desktop .tab{padding:6px 0;gap:0;justify-content:center;border-left-width:0;border-radius:0}
body.sb-icons .sidebar-desktop .tab.active{box-shadow:inset 3px 0 0 var(--c-root)}
body.sb-icons .sidebar-desktop .tab-icon{width:auto;font-size:1.05rem}
body.sb-icons #sidebarMixerToggle,body.sb-icons #sidebarMixer{display:none!important}
body.sb-icons #sidebarMixerRow{justify-content:center}
body.sb-icons #sidebarMixerRow{width:100%}
/* !important : le bouton porte son style en inline (cadre 22px, marge droite) dans index.html */
body.sb-icons #sidebarMixerFloatBtn{margin:6px 0!important;width:100%!important;height:21px!important;border:0!important;border-radius:0!important;background:transparent;color:var(--tx2)!important}
body.sb-icons #sidebarMixerFloatBtn:hover{color:var(--tx);background:var(--s2)}
body.sb-icons #sidebarMixerFloatBtn .ft-ico-mixer{width:24px;height:24px}
body.sb-icons #sidebarMixerFloatBtn{height:24px!important}
/* Pictos seuls : 24px (au lieu de 21px en mode complet) */
body.sb-icons #desktopSidebar .tab[data-tab] .tab-icon,body.sb-icons #desktopSidebar .tab[data-ico] .tab-icon,body.sb-icons #desktopSidebar .tab[data-tab] .tab-icon::before,body.sb-icons #desktopSidebar .tab[data-ico] .tab-icon::before{width:24px;height:24px}
body.sb-hidden .sidebar-desktop{width:0!important;padding:0!important;border-right:0!important;overflow:visible!important}
body.sb-hidden .sidebar-desktop>*:not(#sidebarHandle){display:none!important}
/* ── Pictos SVG de la sidebar (BEGIN sb-icons) — grille 24px, trait 1.8, une couleur (mask + currentColor) ────────────────────
   Remplacent les glyphes Unicode des .tab-icon de #desktopSidebar : le texte du glyphe reste dans le DOM (setLang le réécrit) mais
   est masqué (font-size:0) ; le picto est dessiné par ::before. Modules : sélecteur [data-tab] ; pied de sidebar : [data-ico]. */
#desktopSidebar .tab[data-tab] .tab-icon,#desktopSidebar .tab[data-ico] .tab-icon{font-size:0;width:21px;height:21px;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center}
#desktopSidebar .tab[data-tab] .tab-icon::before,#desktopSidebar .tab[data-ico] .tab-icon::before{content:"";width:21px;height:21px;background:currentColor;-webkit-mask:var(--ico) center/contain no-repeat;mask:var(--ico) center/contain no-repeat}
#desktopSidebar .tab[data-tab="scales"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M3 19h5v-4h5v-4h5V7h3%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="chords"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M6 4v16M12 4v16M18 4v16M5 8h14%22/%3E%3Ccircle cx=%2212%22 cy=%2213%22 r=%221.7%22 fill=%22black%22/%3E%3Ccircle cx=%2218%22 cy=%2216.5%22 r=%221.7%22 fill=%22black%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="progressions"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M3 8h10M10 5l3 3-3 3M3 16h15M15 13l3 3-3 3%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="structures"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%223%22 y=%225%22 width=%2218%22 height=%224%22 rx=%221%22/%3E%3Crect x=%223%22 y=%2211%22 width=%2211%22 height=%224%22 rx=%221%22/%3E%3Crect x=%223%22 y=%2217%22 width=%2215%22 height=%223%22 rx=%221%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="circle"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Ccircle cx=%2212%22 cy=%2212%22 r=%229%22/%3E%3Ccircle cx=%2212%22 cy=%2212%22 r=%223%22/%3E%3Cpath d=%22M12 3v3M12 18v3M3 12h3M18 12h3%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="triads"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M12 4l8 15H4z%22/%3E%3Ccircle cx=%2212%22 cy=%224%22 r=%221.5%22 fill=%22black%22/%3E%3Ccircle cx=%2220%22 cy=%2219%22 r=%221.5%22 fill=%22black%22/%3E%3Ccircle cx=%224%22 cy=%2219%22 r=%221.5%22 fill=%22black%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="intervalles"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Ccircle cx=%226%22 cy=%2217%22 r=%222.4%22/%3E%3Ccircle cx=%2218%22 cy=%227%22 r=%222.4%22/%3E%3Cpath d=%22M8 15.3l8-6.6%22 stroke-dasharray=%222 2.2%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="harmony"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M12 4l9 5-9 5-9-5z%22/%3E%3Cpath d=%22M3 14l9 5 9-5%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="metro"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M9 3h6l3.5 18h-13z%22/%3E%3Cpath d=%22M12 17l4-10%22/%3E%3Cpath d=%22M9 12.5h6%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="groove"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%224%22 y=%224%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3Crect x=%2210%22 y=%224%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3Crect x=%2216%22 y=%224%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3Crect x=%224%22 y=%2210%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3Crect x=%2210%22 y=%2210%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3Crect x=%2216%22 y=%2210%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3Crect x=%224%22 y=%2216%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3Crect x=%2210%22 y=%2216%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3Crect x=%2216%22 y=%2216%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="step"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%223%22 y=%228%22 width=%2218%22 height=%228%22 rx=%221.5%22/%3E%3Cpath d=%22M9 8v8M15 8v8%22/%3E%3Crect x=%224.2%22 y=%229.2%22 width=%223.6%22 height=%225.6%22 fill=%22black%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="drone"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M2 12c2-6 4-6 6 0s4 6 6 0 4-6 6 0%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="analyzer"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M5 20v-8M9 20V6M13 20v-6M17 20V4M21 20v-9%22 stroke-width=%222.4%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="rhythm"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M12 3c4 0 7 2.4 7 6 0 5-5 11-7 12-2-1-7-7-7-12 0-3.6 3-6 7-6z%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="spark"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M11 3l1.9 5.6L18.5 10.5l-5.6 1.9L11 18l-1.9-5.6L3.5 10.5l5.6-1.9z%22/%3E%3Cpath d=%22M19 16v5M16.5 18.5h5%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="eartraining"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M4 14v-2a8 8 0 0116 0v2%22/%3E%3Crect x=%223%22 y=%2214%22 width=%224%22 height=%226%22 rx=%221.5%22/%3E%3Crect x=%2217%22 y=%2214%22 width=%224%22 height=%226%22 rx=%221.5%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="melodie"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%223%22 y=%225%22 width=%228%22 height=%223%22 rx=%221%22/%3E%3Crect x=%229%22 y=%2210.5%22 width=%2211%22 height=%223%22 rx=%221%22/%3E%3Crect x=%225%22 y=%2216%22 width=%227%22 height=%223%22 rx=%221%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="tablature"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.6%22 stroke-linecap=%22round%22%3E%3Cpath d=%22M3 5h18M3 9.7h18M3 14.3h18M3 19h18%22/%3E%3Ccircle cx=%228%22 cy=%229.7%22 r=%222.4%22 fill=%22black%22 stroke=%22none%22/%3E%3Ccircle cx=%2215%22 cy=%2214.3%22 r=%222.4%22 fill=%22black%22 stroke=%22none%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-tab="play"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%223%22 y=%223%22 width=%2218%22 height=%2218%22 rx=%224%22/%3E%3Ccircle cx=%2212%22 cy=%2212%22 r=%223.4%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-ico="tuner"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M3.5 18a9 9 0 0117 0%22/%3E%3Cpath d=%22M12 17l3.5-7%22/%3E%3Ccircle cx=%2212%22 cy=%2217%22 r=%221.4%22 fill=%22black%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-ico="metrofloat"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M7 5h4.5l3 15H4z%22/%3E%3Cpath d=%22M9.2 16l3.3-8.5%22/%3E%3Cpath d=%22M15 4h5v5M20 4l-6 6%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-ico="progress"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M8 4h8v5a4 4 0 01-8 0z%22/%3E%3Cpath d=%22M8 6H5a3 3 0 003 4M16 6h3a3 3 0 01-3 4M12 13v4M8 21h8M10 17h4%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-ico="settings"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Ccircle cx=%2212%22 cy=%2212%22 r=%223%22/%3E%3Ccircle cx=%2212%22 cy=%2212%22 r=%226.6%22/%3E%3Cpath d=%22M12 2.5v2.4M12 19.1v2.4M2.5 12h2.4M19.1 12h2.4M5.3 5.3l1.7 1.7M17 17l1.7 1.7M5.3 18.7L7 17M17 7l1.7-1.7%22/%3E%3C/svg%3E")}
#desktopSidebar .tab[data-ico="fullscreen"] .tab-icon::before{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5%22/%3E%3C/svg%3E")}
.ft-ico-mixer{display:inline-block;width:14px;height:14px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M6 4v16M12 4v16M18 4v16%22/%3E%3Crect x=%223.8%22 y=%228%22 width=%224.4%22 height=%223.4%22 rx=%221%22 fill=%22black%22/%3E%3Crect x=%229.8%22 y=%2214%22 width=%224.4%22 height=%223.4%22 rx=%221%22 fill=%22black%22/%3E%3Crect x=%2215.8%22 y=%226%22 width=%224.4%22 height=%223.4%22 rx=%221%22 fill=%22black%22/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M6 4v16M12 4v16M18 4v16%22/%3E%3Crect x=%223.8%22 y=%228%22 width=%224.4%22 height=%223.4%22 rx=%221%22 fill=%22black%22/%3E%3Crect x=%229.8%22 y=%2214%22 width=%224.4%22 height=%223.4%22 rx=%221%22 fill=%22black%22/%3E%3Crect x=%2215.8%22 y=%226%22 width=%224.4%22 height=%223.4%22 rx=%221%22 fill=%22black%22/%3E%3C/svg%3E") center/contain no-repeat}
/* END sb-icons */
/* BEGIN ft-ico-extra — GÉNÉRÉ par side_dev/dev/sync-ico-extra.mjs à partir du bloc sb-icons ci-dessus : ne pas éditer à la main. */
#mainTabs .tab[data-tab] .tab-icon{font-size:0;width:16px;height:16px;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center}
#mainTabs .tab[data-tab] .tab-icon::before{content:"";width:16px;height:16px;background:currentColor;-webkit-mask:var(--ico) center/contain no-repeat;mask:var(--ico) center/contain no-repeat}
.ft-ico,.ft-ico-tuner{display:inline-block;flex-shrink:0;width:16px;height:16px;background:currentColor;-webkit-mask:var(--ico) center/contain no-repeat;mask:var(--ico) center/contain no-repeat}
#mainTabs .tab[data-tab="scales"] .tab-icon::before,.ft-ico[data-ico="scales"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M3 19h5v-4h5v-4h5V7h3%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="chords"] .tab-icon::before,.ft-ico[data-ico="chords"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M6 4v16M12 4v16M18 4v16M5 8h14%22/%3E%3Ccircle cx=%2212%22 cy=%2213%22 r=%221.7%22 fill=%22black%22/%3E%3Ccircle cx=%2218%22 cy=%2216.5%22 r=%221.7%22 fill=%22black%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="progressions"] .tab-icon::before,.ft-ico[data-ico="progressions"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M3 8h10M10 5l3 3-3 3M3 16h15M15 13l3 3-3 3%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="structures"] .tab-icon::before,.ft-ico[data-ico="structures"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%223%22 y=%225%22 width=%2218%22 height=%224%22 rx=%221%22/%3E%3Crect x=%223%22 y=%2211%22 width=%2211%22 height=%224%22 rx=%221%22/%3E%3Crect x=%223%22 y=%2217%22 width=%2215%22 height=%223%22 rx=%221%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="circle"] .tab-icon::before,.ft-ico[data-ico="circle"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Ccircle cx=%2212%22 cy=%2212%22 r=%229%22/%3E%3Ccircle cx=%2212%22 cy=%2212%22 r=%223%22/%3E%3Cpath d=%22M12 3v3M12 18v3M3 12h3M18 12h3%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="triads"] .tab-icon::before,.ft-ico[data-ico="triads"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M12 4l8 15H4z%22/%3E%3Ccircle cx=%2212%22 cy=%224%22 r=%221.5%22 fill=%22black%22/%3E%3Ccircle cx=%2220%22 cy=%2219%22 r=%221.5%22 fill=%22black%22/%3E%3Ccircle cx=%224%22 cy=%2219%22 r=%221.5%22 fill=%22black%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="intervalles"] .tab-icon::before,.ft-ico[data-ico="intervalles"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Ccircle cx=%226%22 cy=%2217%22 r=%222.4%22/%3E%3Ccircle cx=%2218%22 cy=%227%22 r=%222.4%22/%3E%3Cpath d=%22M8 15.3l8-6.6%22 stroke-dasharray=%222 2.2%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="harmony"] .tab-icon::before,.ft-ico[data-ico="harmony"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M12 4l9 5-9 5-9-5z%22/%3E%3Cpath d=%22M3 14l9 5 9-5%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="metro"] .tab-icon::before,.ft-ico[data-ico="metro"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M9 3h6l3.5 18h-13z%22/%3E%3Cpath d=%22M12 17l4-10%22/%3E%3Cpath d=%22M9 12.5h6%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="groove"] .tab-icon::before,.ft-ico[data-ico="groove"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%224%22 y=%224%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3Crect x=%2210%22 y=%224%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3Crect x=%2216%22 y=%224%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3Crect x=%224%22 y=%2210%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3Crect x=%2210%22 y=%2210%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3Crect x=%2216%22 y=%2210%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3Crect x=%224%22 y=%2216%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3Crect x=%2210%22 y=%2216%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3Crect x=%2216%22 y=%2216%22 width=%224%22 height=%224%22 rx=%221%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="step"] .tab-icon::before,.ft-ico[data-ico="step"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%223%22 y=%228%22 width=%2218%22 height=%228%22 rx=%221.5%22/%3E%3Cpath d=%22M9 8v8M15 8v8%22/%3E%3Crect x=%224.2%22 y=%229.2%22 width=%223.6%22 height=%225.6%22 fill=%22black%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="drone"] .tab-icon::before,.ft-ico[data-ico="drone"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M2 12c2-6 4-6 6 0s4 6 6 0 4-6 6 0%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="analyzer"] .tab-icon::before,.ft-ico[data-ico="analyzer"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M5 20v-8M9 20V6M13 20v-6M17 20V4M21 20v-9%22 stroke-width=%222.4%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="rhythm"] .tab-icon::before,.ft-ico[data-ico="rhythm"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M12 3c4 0 7 2.4 7 6 0 5-5 11-7 12-2-1-7-7-7-12 0-3.6 3-6 7-6z%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="spark"] .tab-icon::before,.ft-ico[data-ico="spark"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M11 3l1.9 5.6L18.5 10.5l-5.6 1.9L11 18l-1.9-5.6L3.5 10.5l5.6-1.9z%22/%3E%3Cpath d=%22M19 16v5M16.5 18.5h5%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="eartraining"] .tab-icon::before,.ft-ico[data-ico="eartraining"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M4 14v-2a8 8 0 0116 0v2%22/%3E%3Crect x=%223%22 y=%2214%22 width=%224%22 height=%226%22 rx=%221.5%22/%3E%3Crect x=%2217%22 y=%2214%22 width=%224%22 height=%226%22 rx=%221.5%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="melodie"] .tab-icon::before,.ft-ico[data-ico="melodie"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%223%22 y=%225%22 width=%228%22 height=%223%22 rx=%221%22/%3E%3Crect x=%229%22 y=%2210.5%22 width=%2211%22 height=%223%22 rx=%221%22/%3E%3Crect x=%225%22 y=%2216%22 width=%227%22 height=%223%22 rx=%221%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="tablature"] .tab-icon::before,.ft-ico[data-ico="tablature"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.6%22 stroke-linecap=%22round%22%3E%3Cpath d=%22M3 5h18M3 9.7h18M3 14.3h18M3 19h18%22/%3E%3Ccircle cx=%228%22 cy=%229.7%22 r=%222.4%22 fill=%22black%22 stroke=%22none%22/%3E%3Ccircle cx=%2215%22 cy=%2214.3%22 r=%222.4%22 fill=%22black%22 stroke=%22none%22/%3E%3C/svg%3E")}
#mainTabs .tab[data-tab="play"] .tab-icon::before,.ft-ico[data-ico="play"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Crect x=%223%22 y=%223%22 width=%2218%22 height=%2218%22 rx=%224%22/%3E%3Ccircle cx=%2212%22 cy=%2212%22 r=%223.4%22/%3E%3C/svg%3E")}
.ft-ico[data-ico="tuner"],.ft-ico-tuner{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M3.5 18a9 9 0 0117 0%22/%3E%3Cpath d=%22M12 17l3.5-7%22/%3E%3Ccircle cx=%2212%22 cy=%2217%22 r=%221.4%22 fill=%22black%22/%3E%3C/svg%3E")}
.ft-ico[data-ico="metrofloat"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M7 5h4.5l3 15H4z%22/%3E%3Cpath d=%22M9.2 16l3.3-8.5%22/%3E%3Cpath d=%22M15 4h5v5M20 4l-6 6%22/%3E%3C/svg%3E")}
.ft-ico[data-ico="progress"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M8 4h8v5a4 4 0 01-8 0z%22/%3E%3Cpath d=%22M8 6H5a3 3 0 003 4M16 6h3a3 3 0 01-3 4M12 13v4M8 21h8M10 17h4%22/%3E%3C/svg%3E")}
.ft-ico[data-ico="settings"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Ccircle cx=%2212%22 cy=%2212%22 r=%223%22/%3E%3Ccircle cx=%2212%22 cy=%2212%22 r=%226.6%22/%3E%3Cpath d=%22M12 2.5v2.4M12 19.1v2.4M2.5 12h2.4M19.1 12h2.4M5.3 5.3l1.7 1.7M17 17l1.7 1.7M5.3 18.7L7 17M17 7l1.7-1.7%22/%3E%3C/svg%3E")}
.ft-ico[data-ico="fullscreen"]{--ico:url("data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22black%22 stroke-width=%221.8%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpath d=%22M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5%22/%3E%3C/svg%3E")}
/* END ft-ico-extra */
/* ── Métronome flottant (popup déplaçable, cf. _metroFloatOpen dans app.js) ──
   Même habillage que .tuner-popup MAIS sans son passage en plein écran sous 480px : le métronome flottant reste un petit
   popup déplaçable sur téléphone aussi (échelle réduite par JS pour tenir dans la largeur). */
.metro-float{position:fixed;z-index:8000;background:var(--sf);border:1px solid var(--bd);border-radius:var(--rl);box-shadow:0 12px 40px -8px rgba(0,0,0,.5);display:flex;flex-direction:column;overflow:hidden;resize:both}
.metro-float-handle{display:flex;align-items:center;justify-content:space-between;padding:8px 8px 8px 12px;border-bottom:1px solid var(--bd);background:var(--s2);cursor:grab;flex-shrink:0;user-select:none;-webkit-user-select:none}
.metro-float-handle:active{cursor:grabbing}
#sidebarMixerFloatBtn:hover{border-color:var(--c-root);color:var(--tx)}
#sidebarMixerFloatBtn.on{border-color:#fff;color:var(--tx)}
/* ── Mini-accordeur compact (bouton dans .tb3 + barre fine juste en dessous, alternative légère au popup) ──
   Volontairement minimal : pas de Hz affiché (le popup #tunerPopup reste l'outil précis), juste un curseur
   de justesse + un repère central + un badge de note. Masqué par défaut (display:none), affiché seulement
   quand le mode est actif.
   Le badge de note vit dans cette barre (pas dans .tb3 à côté du sélecteur de tonalité, cf. .mini-tuner-row
   plus bas) : #globalRootRow (le sélecteur) ne doit jamais céder de largeur à quoi que ce soit d'autre — sur
   mobile, ça le forçait à rétrécir/scroller pour laisser de la place au badge (bug rapporté : "le sélecteur
   de tonalité doit toujours être visible en entier"). Cette barre n'apparaît que mini-tuner actif, sur sa
   propre ligne dédiée, jamais en concurrence d'espace avec .tb3. */
/* Survol du sélecteur global de tonalité : les styles d'état (fond/bordure/opacité/soulignement) sont
   posés en inline par syncRootRows() (app.js), donc :hover doit passer par !important pour les battre.
   Ramène l'opacité à 1 (les notes inactives sont assombries à .55 au repos) et ajoute une légère
   élévation, pour un retour visuel clair sans dépendre uniquement de la couleur de fond déjà utilisée
   comme état permanent de chaque bouton. Note déjà active (curseur pas vraiment utile dessus, déjà
   sélectionnée) : effet plus discret, juste l'opacité déjà à 1 → pas de changement visible nécessaire.
   prefers-reduced-motion : la transition existe déjà via var(--tr) sur .rbtn, translateY reste minime
   (2px) donc pas besoin de règle séparée. */
#globalRootRow .rbtn:hover{opacity:1!important;transform:translateY(-2px);box-shadow:0 3px 8px rgba(0,0,0,.35)!important;cursor:pointer}
#globalRootRow .rbtn.active:hover{transform:none}

.mini-tuner-bar{padding:3px 14px 6px}
.mini-tuner-row{display:flex;align-items:center;gap:8px}
/* Niveau de signal en entrée, en fond de la piste — simple repère "y a-t-il du son capté", pas une mesure
   précise. scaleX (transform) plutôt qu'animer width : moins coûteux, pas de reflow à chaque frame de la
   boucle micro. transform-origin:left pour que la barre grandisse depuis la gauche, comme un vumètre
   classique. Couleur du thème (--tx3/--bd2), volontairement discrète — ne doit jamais rivaliser visuellement
   avec le curseur ou le badge de note, qui restent l'information principale. Ancré sur .mini-tuner-track
   (pas toute la ligne) pour ne jamais déborder sous le badge de note à droite. */
.mini-tuner-level{position:absolute;top:0;left:0;right:0;bottom:0;background:var(--tx3);opacity:.14;transform:scaleX(0);transform-origin:left;transition:transform .08s linear}
.mini-tuner-track{position:relative;height:4px;border-radius:2px;background:var(--s2);border:1px solid var(--bd);flex:1;min-width:0;overflow:visible}
.mini-tuner-level-wrap{position:absolute;top:-6px;left:-4px;right:-4px;bottom:-6px;border-radius:2px;overflow:hidden;pointer-events:none}
.mini-tuner-center{position:absolute;top:-3px;left:50%;width:2px;height:10px;margin-left:-1px;background:var(--bd2);border-radius:1px}
.mini-tuner-cursor{position:absolute;top:-4px;left:50%;width:8px;height:12px;margin-left:-4px;border-radius:3px;background:var(--tx3);transition:transform .1s linear,background .12s;box-shadow:0 0 0 2px var(--sf)}
/* Code couleur à 3 paliers selon l'écart en cents (cf. _miniTunerLoop) : vert = juste (±2ct), orange =
   proche, rouge = loin. Remplace l'ancien binaire juste/pas-juste (in-tune) par un dégradé d'information
   plus utile pendant qu'on accorde (voir "de quel côté et à quel point on est loin", pas juste oui/non). */
.mini-tuner-cursor.tune-good{background:#6fb353;box-shadow:0 0 0 2px var(--sf),0 0 8px 1px rgba(111,179,83,.6)}
.mini-tuner-cursor.tune-mid{background:#e0a03e;box-shadow:0 0 0 2px var(--sf),0 0 8px 1px rgba(224,160,62,.6)}
.mini-tuner-cursor.tune-far{background:#e0473e;box-shadow:0 0 0 2px var(--sf),0 0 8px 1px rgba(224,71,62,.6)}
.mini-tuner-cursor.idle{opacity:.35}
/* Badge "note détectée" : sa couleur vient de NOTE_COLORS (couleur de la note, comme partout ailleurs dans
   l'app — pastilles de tonique, diagrammes...), pas du code vert/orange/rouge de justesse, qui reste
   réservé au curseur uniquement (cf. discussion : la couleur "verte = juste" sur le badge prêtait à
   confusion avec une couleur de note qui serait par coïncidence déjà verte, ex. G). La couleur exacte
   (background/color) est posée en JS via _mtNoteBadgeColors, pas ici. */
/* Largeur fixe (pas min-width variable) dimensionnée pour le pire cas "A#4"/"C#3" : sans ça, la largeur du
   badge grandissait pile au moment où une note était détectée (bug rapporté sur mobile — "fait bouger la
   mise en page quand il détecte une note"), et allait grandir une seconde fois à l'ajout de l'octave. Un
   badge toujours à la même taille, vide ou plein, élimine tout décalage de layout autour de lui. */
.mini-tuner-note{
  width:38px;height:26px;padding:0 4px;border-radius:5px;border:1px solid var(--bd);
  display:flex;align-items:center;justify-content:center;font-family:var(--fm);font-weight:700;font-size:.72rem;
  flex-shrink:0;transition:background .12s,border-color .12s,color .12s;
}
.mini-tuner-sep{width:1px;align-self:stretch;margin:4px 2px;background:var(--bd);flex-shrink:0}
/* Étiquette de gamme active, à droite du sélecteur de tonalité global (topbar + ligne dédiée, cf.
   _grSyncScaleLabel dans app.js) — texte compact, jamais coupé (white-space:nowrap), masqué proprement
   (display:none via :empty) tant qu'aucune gamme n'est résolue plutôt que de laisser un espace vide. */
.root-scale-label{font-family:var(--fm);font-size:.6rem;color:var(--tx2);white-space:nowrap;flex-shrink:0;margin-left:2px}
.root-scale-label:empty{display:none}
#miniTunerBtn.on{background:transparent;color:#6fb353;border-color:#6fb353}



#page-rhythm .rh-genre-pill{font-size:max(10px,.55rem);padding:1px 6px;border-radius:999px;background:var(--s3);color:var(--tx2);font-family:var(--fm)}
#page-rhythm .rh-refs{font-size:max(10px,.55rem);color:var(--tx3);font-family:var(--fm);margin-top:.25rem}
#page-rhythm .rh-select.on{background:var(--c-root)!important;color:#111!important;border-color:var(--c-root)!important;font-weight:700}

/* ── RYTHME DICTÉE (module expérimental) ── */
#page-eartraining .rd-wrap{display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden}
#page-eartraining .rd-sticky-player{flex-shrink:0;box-shadow:0 4px 10px -4px rgba(0,0,0,.4);margin:.6rem .6rem 0}
#page-eartraining .rd-scroll-body{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:.6rem;padding:.6rem}
/* Panneaux d'exercice hors Dictée (#etIntervalWrap/#etChordWrap/#etDegreeWrap/#etCadenceWrap/#etScaleWrap)
   ne sont PAS dans .rd-scroll-body (contrairement à Dictée) donc n'héritaient pas de son padding:.6rem —
   leur carte restait pleine largeur de .rd-wrap alors que l'en-tête "Ear Training" garde sa marge inline
   .6rem, créant un décalage visible entre en-tête et corps (retour explicite, capture d'écran à l'appui).
   Même marge horizontale que l'en-tête pour aligner les deux largeurs. */

#page-eartraining .rd-card{border:1px solid var(--bd);border-radius:var(--rl);padding:.7rem;min-width:0;max-width:100%;box-sizing:border-box}
#page-eartraining:not(.module-bg-enabled) .rd-card{background:var(--sf)}
#page-eartraining .rd-card-title{font-family:var(--fd);font-size:.85rem;font-weight:700;color:var(--tx);margin-bottom:.5rem;display:flex;align-items:center;gap:.4rem;flex-wrap:wrap}
#page-eartraining .rd-toolbar{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}
#page-eartraining .rd-select{padding:4px 7px;border-radius:var(--r);border:1px solid var(--bd);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.65rem;cursor:pointer;outline:none}
#page-eartraining .rd-select:hover{border-color:var(--bd2)}
/* État actif du toggle métronome de guidage (rdMetroGuideToggle) — même logique que .rd-play-btn.playing
   (fond plein couleur d'accent) mais appliqué à .rd-select, réutilisé ici plutôt que de créer un
   nouveau style de bouton dédié pour un seul toggle. */
#page-eartraining .rd-select.on{background:var(--c-root);color:#111;border-color:var(--c-root)}
#page-eartraining .rd-play-btn{padding:6px 12px;border-radius:7px;border:2px solid var(--c-root);background:transparent;color:var(--c-root);font-family:var(--fd);font-size:.75rem;font-weight:700;cursor:pointer;transition:all var(--tr);user-select:none}
#page-eartraining .rd-play-btn.playing{background:var(--c-root);color:#111}
/* En file d'attente (rdPlayEvents/_rdQueuedPlay) : cette lecture démarrera dès la fin de celle en cours
   sur l'autre bouton — bordure pointillée pulsante plutôt que le fond plein de .playing, pour distinguer
   clairement "en attente" de "en train de jouer maintenant". */
#page-eartraining .rd-play-btn.queued{border-style:dashed;animation:rd-queued-pulse 1s ease-in-out infinite}
@keyframes rd-queued-pulse{0%,100%{opacity:1}50%{opacity:.55}}
/* Accompagnement en attente du prochain temps 1 du métronome (cf. strPlay/syncDelayMs) — demande explicite
   après le fix "attendre le retour au 1er temps" : le clic est bien reçu, la lecture est en file d'attente
   jusqu'au calage, ce pulse évite que ça paraisse "sans effet" pendant le court délai. Même langage visuel
   que .rd-play-btn.queued (Dictée) — mêmes keyframes (rd-queued-pulse) mais 2x plus rapide (0.5s, demande
   explicite) : la durée de l'attente ici est courte (au plus une mesure), un pulse trop lent y paraissait
   "mou"/indistinct de l'état actif. Appliqué aux DEUX boutons Play de l'accompagnement (#globalStrPlayBtn
   topbar, #strPlayBtn subbar, cf. _strSyncPlayPending dans app.js), retiré dès que la lecture démarre
   réellement (strPlayOnce) ou que Stop est cliqué entre-temps.*/
#globalStrPlayBtn.str-play-pending,#strPlayBtn.str-play-pending{animation:rd-queued-pulse .5s ease-in-out infinite}
#page-eartraining .rd-seg{display:flex;gap:2px;background:var(--s2);border:1px solid var(--bd);border-radius:var(--r);padding:2px}
/* #etExerciseSeg (6 boutons, sélecteur d'exercice) déborde horizontalement sur mobile (rapporté : "les
   entrées dépassent du menu latéralement") — flex-wrap:wrap le passe sur 2 lignes de 3 boutons plutôt qu'un
   scroll horizontal. Scopé à CET id précis (pas .rd-seg générique) : les autres .rd-seg du module (ex.
   #rdModeSeg, 2 boutons seulement) n'ont jamais ce problème et doivent garder leur ligne unique. */
@media(max-width:600px){
  #etExerciseSeg{flex-wrap:wrap;justify-content:center}
  #etExerciseSeg button{flex:1 1 30%}
}
#page-eartraining .rd-seg button{padding:4px 10px;border-radius:5px;border:none;background:transparent;color:var(--tx2);font-family:var(--fm);font-size:.62rem;cursor:pointer;transition:all var(--tr)}
#page-eartraining .rd-seg button.active{background:var(--c-root);color:#111;font-weight:700}
#page-eartraining .rd-events-row{display:flex;flex-wrap:wrap;gap:4px;min-height:64px;align-items:center;padding:.5rem;margin-top:.4rem;border-radius:6px;background:var(--s2);border:1px solid var(--bd)}
#page-eartraining .rd-event{position:relative;display:flex;align-items:center;justify-content:center;width:44px;height:52px;border-radius:6px;background:var(--sf);border:1px solid var(--bd2);cursor:pointer;transition:all .1s;user-select:none;-webkit-tap-highlight-color:transparent;
  /* touch-action:pan-y — même compromis que .prog-block (Progressions) : le scroll vertical natif de la
     page garde la priorité par défaut, le drag horizontal ne s'engage qu'une fois identifié comme tel
     (cf. rdAttachReorderDrag) plutôt que de bloquer tout scroll tactile sur ces cartes. */
  touch-action:pan-y}
#page-eartraining .rd-event:hover{border-color:var(--c-root)}
#page-eartraining .rd-event.insp-dragging{opacity:.15}
/* Glisser-déposer pour réordonner (Construire ET reconstruction Dictée avant révélation, calqué sur le
   même mécanisme que les accords de Progressions — cf. rdAttachReorderDrag/progComputeInsertIdx). Fond
   propre plutôt que réutiliser .insp-drag-ghost tel quel : ce dernier suppose un fond de couleur de
   note/accord (color:#111 fixe) qui n'a pas de sens pour une figure rythmique, sans couleur associée. */
.rd-drag-ghost{background:var(--c-root);color:#111;font-family:var(--fm);font-weight:700;font-size:1.3rem}
#page-eartraining .rd-event.rd-event-rest{opacity:.55}
#page-eartraining .rd-event.current{outline:2px solid var(--c-root);outline-offset:1px;background:var(--s3)}
/* Feedback granulaire après "Révéler" (au lieu d'un simple verdict global bon/faux) : chaque figure de
   la reconstruction est comparée à la figure correspondante du rythme cible, case par case — montre
   PRÉCISÉMENT où ça a dérapé plutôt que de forcer à comparer les deux lignes à l'œil. */
#page-eartraining .rd-event.rd-event-match{border-color:#3d9e78;background:color-mix(in srgb, #3d9e78 15%, var(--sf))}
#page-eartraining .rd-event.rd-event-mismatch{border-color:#d9534f;background:color-mix(in srgb, #d9534f 15%, var(--sf))}
#page-eartraining .rd-event.rd-event-extra{border-color:#d9534f;background:color-mix(in srgb, #d9534f 15%, var(--sf));border-style:dashed}
#page-eartraining .rd-event.rd-event-missing{border-color:var(--tx3);background:transparent;border-style:dashed;opacity:.6}
#page-eartraining .rd-event-glyph{font-size:1.6rem;color:var(--tx);position:relative;line-height:1}
#page-eartraining .rd-dot{position:absolute;right:-9px;bottom:2px;font-size:1.4rem;color:var(--c-root)}
#page-eartraining .rd-tie{position:absolute;right:-11px;top:-2px;font-size:1rem;color:var(--tx3)}
#page-eartraining .rd-measure-info{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx3);margin-top:.35rem}
/* Bandeau de suggestion "passer au niveau supérieur" (rdRenderScore) — affiché seulement le temps
   d'une série de réussites tout juste atteinte, cf. RD_LEVEL_UP_STREAK. */
#page-eartraining .rd-levelup-banner{align-items:center;justify-content:space-between;gap:.6rem;margin-top:.4rem;padding:.45rem .6rem;border-radius:6px;border:1px solid var(--c-root);background:color-mix(in srgb, var(--c-root) 12%, var(--sf));font-family:var(--fm);font-size:max(10px,.58rem);color:var(--tx)}
/* Grille à 2 lignes (notes / silences) × N colonnes (une par valeur RD_VALUES) au lieu d'un flux
   note-silence-note-silence — grid-template-columns posé dynamiquement en JS (rdRenderPalette/
   rdRenderDicteePalette), grid-auto-flow:row par défaut place les 5 notes sur la 1ère ligne puis les 5
   silences juste en dessous, alignés colonne par colonne (demandé : "correspondance verticale
   note/silence"). min-width:0 sur les boutons (cf. .rd-note-btn) nécessaire pour qu'ils rétrécissent au
   lieu de déborder de leur colonne de grille sur petit écran. */
#page-eartraining .rd-palette{display:grid;gap:6px;margin-top:.6rem;padding-top:.5rem;border-top:1px solid var(--bd)}
/* Suggestions de complétion de mesure (rdRenderSuggestions) — même famille visuelle que .rd-palette
   juste en dessous, mais fond légèrement teinté (var(--c-root) très dilué) pour se distinguer d'un
   premier coup d'œil comme une zone d'aide plutôt que la palette d'ajout normale.
   min-height calibrée sur 2 lignes de boutons de suggestion (cas courant à largeur normale, plusieurs
   combinaisons proposées wrap sur 2 rangées) : réserve TOUJOURS cette place, même à vide (placeholder
   discret, bordure pointillée sans fond via .rd-suggestions-empty) — sinon l'apparition des suggestions
   après la 1re figure posée décale le reste de la mise en page (bug rapporté explicitement). Choix
   utilisateur assumé : mieux vaut un peu de vide au repos qu'un saut de layout à l'usage. */
#page-eartraining .rd-suggestions{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:6px;margin-top:.5rem;padding:.5rem;border-radius:7px;background:color-mix(in srgb, var(--c-root) 6%, var(--s2));border:1px solid var(--bd);min-height:86px;box-sizing:border-box}
#page-eartraining .rd-suggestions:not(.rd-suggestions-empty){justify-content:flex-start}
#page-eartraining .rd-suggestions.rd-suggestions-empty{background:transparent;border-style:dashed;border-color:var(--bd)}
#page-eartraining .rd-suggest-placeholder{font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3);opacity:.6}
#page-eartraining .rd-suggest-lbl{font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3);text-transform:uppercase;letter-spacing:.03em;flex-shrink:0}
#page-eartraining .rd-suggest-btn{display:flex;align-items:center;gap:2px;padding:5px 8px;border-radius:6px;background:var(--s2);border:1px solid var(--bd);cursor:pointer;transition:all .1s;-webkit-tap-highlight-color:transparent}
#page-eartraining .rd-suggest-btn:hover{border-color:var(--c-root);background:var(--s3)}
/* .rd-suggest-figure regroupe glyphe+nom empilés verticalement pour chaque figure d'une combinaison —
   demandé explicitement ("afficher... le nom des valeurs de notes en texte, en plus des symboles"), même
   principe visuel que .rd-note-btn/.rd-note-lbl de la palette juste en dessous. */
#page-eartraining .rd-suggest-figure{display:flex;flex-direction:column;align-items:center;gap:1px}
#page-eartraining .rd-suggest-glyph{font-size:1.1rem;color:var(--tx);line-height:1}
#page-eartraining .rd-suggest-name{font-family:var(--fm);font-size:max(10px,.42rem);color:var(--tx3);text-transform:uppercase;letter-spacing:.02em;white-space:nowrap}
#page-eartraining .rd-suggest-plus{font-size:.6rem;color:var(--tx3);margin:0 1px;align-self:center}
/* Repère visuel du temps courant pendant que le métronome de guidage tourne (rdUpdateBeatIndicator) —
   4 points, un par temps de la mesure (4/4 uniquement, cf. RD_BEATS_PER_MEASURE), le temps courant
   s'allume en boucle synchronisé sur le son du métronome. */
#page-eartraining .rd-beat-indicator{display:flex;gap:4px;align-items:center;flex-shrink:0}
#page-eartraining .rd-beat-dot{width:8px;height:8px;border-radius:50%;background:var(--bd);transition:background .08s,transform .08s}
#page-eartraining .rd-beat-dot.active{background:var(--c-root);transform:scale(1.3)}
#page-eartraining .rd-note-btn{display:flex;flex-direction:column;align-items:center;gap:2px;padding:6px 4px;min-width:0;border-radius:7px;background:var(--s2);border:1px solid var(--bd);color:var(--tx);cursor:pointer;transition:all .1s;-webkit-tap-highlight-color:transparent}
#page-eartraining .rd-note-btn:hover{border-color:var(--c-root);background:var(--s3)}
/* Retour visuel de l'appui long (cf. rdPreviewFigure/_rdBindLongPress, app.js) — même couleur d'accent que
   le survol, mais un fond plein pour bien distinguer "en train d'écouter la figure" d'un simple survol. */
#page-eartraining .rd-note-btn.rd-longpress-active{border-color:var(--c-root);background:var(--c-root);color:#111}
#page-eartraining .rd-note-btn .rd-glyph{font-size:1.4rem;line-height:1;font-family:var(--fb)} /* <button> n'hérite pas la police : sans var(--fb), 'FT Music' (cf. @font-face) n'était pas dans la pile */
#page-eartraining .rd-note-btn .rd-note-lbl{font-family:var(--fm);font-size:max(10px,.42rem);color:var(--tx3);text-transform:uppercase;letter-spacing:.03em;white-space:normal;text-align:center;line-height:1.15;overflow-wrap:anywhere} /* normal + anywhere : au plancher de 10px, « Double-croche » ne tenait plus sur une ligne en téléphone — coupure au tiret */
#page-eartraining .rd-rest-btn{opacity:.75}
#page-eartraining .rd-result{font-family:var(--fd);font-size:.85rem;font-weight:700;margin-top:.6rem;min-height:1.2rem}
/* ── Eartrain : panneau QCM commun (eartrain.js, etRender) ──────────────────────────────────────────
   États des boutons de réponse : neutre → faux (barré rouge, secousse) → juste (vert plein, pop) ou révélé
   (vert pointillé). Une fois la question résolue (.et-resolved), chaque réponse devient un bouton d'écoute
   (curseur + ▶ au survol) pour comparer à l'oreille. */
#page-eartraining #etQuizWrap>.rd-card{margin:0 .6rem .6rem}
#page-eartraining .et-quiz{--et-ok:#3d9e78;--et-ko:#d9534f}
#page-eartraining .et-quiz .rd-card-title{margin-bottom:.4rem}
#page-eartraining .et-stats{margin-left:auto;display:flex;align-items:center;gap:.55rem;font-family:var(--fm);font-size:max(10px,.58rem);font-weight:400;color:var(--tx3)}
#page-eartraining .et-stats .et-streak.hot{color:var(--c-root);font-weight:700}
#page-eartraining .et-reset{border:none;background:transparent;color:var(--tx3);cursor:pointer;font-size:.7rem;padding:0 2px}
#page-eartraining .et-reset:hover{color:var(--tx)}
#page-eartraining .et-settings{display:flex;flex-wrap:wrap;gap:.4rem;align-items:center}
#page-eartraining .et-context{margin-top:.5rem;font-family:var(--fm);font-size:.62rem;color:var(--tx2)}
#page-eartraining .et-context b{color:var(--c-root);font-family:var(--fd);font-size:.8rem}
#page-eartraining .et-actions{display:flex;flex-wrap:wrap;gap:.4rem;align-items:center;margin-top:.6rem}
#page-eartraining .et-actions .rd-select:disabled{opacity:.4;cursor:default}
#page-eartraining .et-actions .et-auto{margin-left:auto}
#page-eartraining .rd-play-btn.et-playing,#page-eartraining .rd-select.et-playing{background:var(--c-root);color:#111;animation:rd-queued-pulse 1s ease-in-out infinite}
#page-eartraining .et-next.et-next-ready{background:var(--c-root);color:#111;border-color:var(--c-root);font-weight:700;animation:et-nudge 1.6s ease-in-out infinite}
@keyframes et-nudge{0%,100%{transform:translateX(0)}50%{transform:translateX(3px)}}
#page-eartraining .et-answers{display:grid;grid-template-columns:repeat(auto-fill,minmax(132px,1fr));gap:6px;margin-top:.7rem}
#page-eartraining .et-ans{--et-accent:var(--bd2);position:relative;display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:2px;min-height:46px;padding:6px 10px;border-radius:8px;border:1px solid var(--bd);border-left:3px solid var(--et-accent);background:var(--s2);color:var(--tx);text-align:left;cursor:pointer;transition:background var(--tr),border-color var(--tr),transform .08s,opacity var(--tr)}
#page-eartraining .et-ans:hover{background:var(--s3);border-color:var(--bd2);border-left-color:var(--et-accent)}
#page-eartraining .et-ans:active{transform:scale(.97)}
#page-eartraining .et-ans-label{font-family:var(--fd);font-size:.78rem;font-weight:700;line-height:1.1}
#page-eartraining .et-ans-sub{font-family:var(--fm);font-size:max(10px,.52rem);color:var(--tx3);line-height:1.2}
#page-eartraining .et-ans-wrong{background:color-mix(in srgb,var(--et-ko) 14%,var(--s2));border-color:var(--et-ko);border-left-color:var(--et-ko);opacity:.75}
#page-eartraining .et-ans-wrong .et-ans-label{text-decoration:line-through;text-decoration-color:var(--et-ko)}
#page-eartraining .et-ans-correct{background:var(--et-ok);border-color:var(--et-ok);color:#fff;animation:et-pop .32s ease-out}
#page-eartraining .et-ans-correct .et-ans-sub{color:rgba(255,255,255,.85)}
#page-eartraining .et-ans-correct:hover{background:var(--et-ok)}
#page-eartraining .et-ans-revealed{border:2px dashed var(--et-ok);background:color-mix(in srgb,var(--et-ok) 16%,var(--s2))}
#page-eartraining .et-resolved .et-ans{padding-right:20px}
#page-eartraining .et-resolved .et-ans::after{content:'▶';position:absolute;top:5px;right:7px;font-size:max(10px,.5rem);opacity:0;transition:opacity var(--tr)}
#page-eartraining .et-resolved .et-ans:hover::after,#page-eartraining .et-resolved .et-ans.et-playing::after{opacity:.8}
#page-eartraining .et-resolved .et-ans.et-playing{outline:2px solid var(--c-root);outline-offset:1px}
#page-eartraining .et-shake{animation:et-shake .35s ease-in-out}
@keyframes et-shake{0%,100%{transform:translateX(0)}20%{transform:translateX(-4px)}40%{transform:translateX(4px)}60%{transform:translateX(-3px)}80%{transform:translateX(2px)}}
@keyframes et-pop{0%{transform:scale(1)}45%{transform:scale(1.06)}100%{transform:scale(1)}}
#page-eartraining .et-feedback{margin-top:.7rem;min-height:1.4rem;padding:.45rem .6rem;border-radius:6px;border:1px solid transparent;font-family:var(--fm);font-size:.62rem;color:var(--tx3);line-height:1.45}
#page-eartraining .et-feedback b{font-family:var(--fd);font-size:.82rem}
#page-eartraining .et-fb-ok{color:var(--tx);border-color:var(--et-ok);background:color-mix(in srgb,var(--et-ok) 12%,transparent)}
#page-eartraining .et-fb-ok b{color:var(--et-ok)}
#page-eartraining .et-fb-wrong{color:var(--tx);border-color:var(--et-ko);background:color-mix(in srgb,var(--et-ko) 10%,transparent)}
#page-eartraining .et-fb-wrong b{color:var(--et-ko)}
#page-eartraining .et-fb-revealed{color:var(--tx);border-color:var(--bd2);background:var(--s2)}
#page-eartraining .et-fb-hint{margin-top:.2rem;font-size:max(10px,.55rem);color:var(--tx3)}
#page-eartraining .et-foot{display:flex;align-items:center;gap:.6rem;margin-top:.6rem}
#page-eartraining .et-hist{display:flex;gap:3px;flex-wrap:wrap}
#page-eartraining .et-dot{width:8px;height:8px;border-radius:50%}
#page-eartraining .et-dot.ok{background:var(--et-ok)}
#page-eartraining .et-dot.ko{background:var(--et-ko)}
#page-eartraining .et-kbd-hint{margin-left:auto;font-family:var(--fm);font-size:max(10px,.52rem);color:var(--tx3)}
@media (hover:none){#page-eartraining .et-kbd-hint{display:none}}
@media (max-width:560px){
  #page-eartraining .et-answers{grid-template-columns:repeat(auto-fill,minmax(96px,1fr))}
  #page-eartraining .et-actions .et-auto{margin-left:0}
}

/* ── DÉMARRAGE (Spark) ── module expérimental : questionnaire court → tonalité/gamme/progression
   suggérées. Cartes autosuffisantes (background/border posés ici, pas hérités) pour rester cohérentes
   quel que soit leur conteneur ambiant, même principe que .insp-drop-zone (background:var(--bg) explicite,
   pas hérité — cf. son commentaire). */
#page-spark .spark-card{border:1px solid var(--bd);border-radius:var(--rl);padding:.9rem 1rem;box-sizing:border-box}
#page-spark:not(.module-bg-enabled) .spark-card{background:var(--sf)}
/* Largeur du corps du module (cf. index.html .spark-wrap > .spark-card/#sparkResult) — 560px en mobile/
   desktop étroit (colonne verticale unique, une largeur plus grande n'aiderait pas), élargi au-delà de
   DESKTOP_BREAKPOINT (demande explicite : "utiliser plus de largeur pour le corps du module" — la grille de
   genres/vibes en 3 colonnes restait figée à 560px alors que le viewport desktop laisse beaucoup d'espace
   inutilisé de part et d'autre). Anciennement 3 inline style="max-width:560px" dupliqués sur chaque bloc
   (titre, questions, résultats) — remplacés par cette classe unique pour n'avoir qu'un seul endroit à
   ajuster désormais. 820px choisi pour laisser la grille 3 colonnes (genre/vibe) respirer sans dépasser une
   largeur de lecture confortable pour la liste de résultats (texte sur une ligne, cf. .spark-result-row). */
#page-spark .spark-body-width{width:100%;max-width:560px}
@media(min-width:1024px){#page-spark .spark-body-width{max-width:820px}}
#page-spark .spark-q-label{font-family:var(--fm);font-size:max(10px,.58rem);color:var(--tx3);text-transform:uppercase;letter-spacing:.04em;margin-bottom:5px}
/* Boutons de grille (Objectif/Niveau/Genre) — hauteur fixe partagée (pas héritée de .pg, ces boutons ne sont
   plus dans un conteneur .pg en flex) pour que toutes les cellules d'une même grille s'alignent proprement,
   quel que soit le nombre de mots du libellé (ex. "Jammer" vs "Technique"). */
#page-spark .spark-grid-btn{height:34px;border:1px solid var(--bd);border-radius:var(--r);background:var(--s2);white-space:normal;text-align:center;line-height:1.25}
#page-spark .spark-grid-btn.active{background:var(--c-root);border-color:var(--c-root);color:#111;font-weight:700}
/* Bouton "?" par genre (cf. sparkInit/GEN_GENRE_INFO) — coin supérieur droit de la case, superposé au
   bouton principal via position:absolute sur le wrapper .spark-grid-btn-wrap (position:relative posée
   inline). Même taille/forme que .info-btn ailleurs dans l'app, juste repositionnée ici pour flotter au-
   dessus d'un bouton pleine largeur plutôt qu'en ligne à côté d'un texte. */
#page-spark .spark-grid-btn-wrap .info-btn{position:absolute;top:50%;right:6px;transform:translateY(-50%);z-index:1}
#page-spark #sparkGoBtn:hover{filter:brightness(1.08)}
/* Résultat : reprend le langage visuel des blocs d'accord de Progressions (.prog-block-card) pour la
   cohérence, mais dans son propre namespace #sparkResult — pas de réutilisation littérale de .prog-block
   pour ne pas hériter du drag-and-drop/comportement de suppression qui n'ont pas de sens ici (résultat
   affiché, pas construit interactivement). */
#page-spark .spark-result-hdr{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
#page-spark .spark-result-key{font-family:var(--fd);font-size:1.3rem;font-weight:700}
#page-spark .spark-result-scale{font-family:var(--fm);font-size:.68rem;color:var(--tx3)}
#page-spark .spark-chord-row{display:flex;flex-wrap:wrap;gap:6px}
#page-spark .spark-chord-chip{display:flex;flex-direction:column;align-items:center;gap:1px;padding:7px 12px;border-radius:var(--r);background:var(--s2);border:1px solid var(--bd)}
#page-spark .spark-chord-chip .spark-chord-name{font-family:var(--fb);font-weight:700;font-size:.8rem}
#page-spark .spark-chord-chip .spark-chord-deg{font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3)}
#page-spark .spark-action-row{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px}
#page-spark .spark-action-btn{padding:6px 11px;border-radius:var(--r);border:1px solid var(--bd);background:var(--s2);color:var(--tx2);font-family:var(--fm);font-size:.6rem;cursor:pointer;transition:all var(--tr)}
#page-spark .spark-action-btn:hover{border-color:var(--c-root);color:var(--tx)}
/* Liste compacte de propositions — une ligne par proposition au lieu d'une carte empilée (demande
   explicite : "au lieu d'une liste de blocs qui prennent de la place verticalement, une liste compacte").
   Étoile "Tout appliquer" alignée à droite via margin-left:auto, sans le détail des 3 autres boutons
   d'envoi individuels (gardés uniquement pour l'action groupée, cf. sparkRenderResult). */
#page-spark .spark-result-row{display:flex;align-items:center;gap:8px;padding:6px 4px;border-bottom:1px solid var(--bd)}
#page-spark .spark-result-row:last-child{border-bottom:none}
/* Largeurs fixes (pas juste flex-shrink:0) sur tonique/gamme — demande explicite "aligne les progressions
   (nom des accords)" : sans largeur commune, chaque ligne poussait sa colonne d'accords à une position
   différente selon la longueur du nom de tonalité/gamme (ex. "B" vs "G#", "Penta majeure" vs "Mixolydien"),
   rendant les accords eux-mêmes visuellement décalés d'une ligne à l'autre. */
/* width généreuse (54px) + overflow:hidden explicite (pas seulement flex-shrink:0, qui contraint la boîte
   mais ne coupe pas un contenu texte plus large qu'elle — le texte débordait quand même visuellement par-
   dessus la colonne suivante, ex. "G#m"/"C#m" chevauchant "Blues"/"Locrien", cf. capture). */
#page-spark .spark-result-key-mini{font-family:var(--fd);font-size:.85rem;font-weight:700;flex-shrink:0;width:54px;overflow:hidden;white-space:nowrap}
#page-spark .spark-result-scale-mini{font-family:var(--fm);font-size:max(10px,.58rem);color:var(--tx3);flex-shrink:0;white-space:nowrap;width:82px;overflow:hidden;text-overflow:ellipsis}
/* Élargi au-delà de DESKTOP_BREAKPOINT (cf. .spark-body-width ci-dessus, largeur du corps déjà augmentée à
   820px) — demande explicite : "décaler les progressions d'accords des résultats (vers la droite) pour
   pouvoir afficher le nom de la gamme en entier". 82px tronquait des noms de gamme longs (ex. "Min.
   naturelle" en "Min. na…", visible à l'usage). .spark-result-chips-mini est déjà flex:1;min-width:0 (cf.
   plus bas) : élargir cette colonne AVANT elle dans le flux flex décale mécaniquement les chips d'accords
   vers la droite sans qu'aucun changement supplémentaire ne soit nécessaire sur elles. 150px choisi pour
   couvrir le nom de gamme le plus long du pool réel (7 modes grecs + majeur/mineur, "Min. naturelle" étant
   le plus long) sans empiéter excessivement sur l'espace des chips. */
@media(min-width:1024px){#page-spark .spark-result-scale-mini{width:150px}}
/* Indice pentatonique (cf. app.js sparkRenderResult, pentaHint) — badge rond compact "5" signalant que la
   pentatonique de même tonique (majeure ou mineure selon r.scale.key) est aussi jouable, Spark ne la
   proposant jamais lui-même (cf. _SPARK_SCALE_POOL, pas de triades tertiaires standard pour ces gammes). */
/* Slot de largeur fixe réservé que le badge "5" soit présent ou non (pentaHtml conditionnel selon
   r.scale.key, cf. app.js sparkRenderResult) — sans lui, la colonne .spark-result-chips-mini suivante
   démarrait à un offset horizontal différent selon les lignes (badge présent = colonne décalée à droite),
   cassant l'alignement vertical des accords entre lignes voisines (rapporté : "le badge 5... casse
   l'alignement des accords de la progression"). Même largeur que le badge lui-même (14px) + sa marge
   négative d'origine (-2px), pour ne pas élargir la ligne quand le badge est absent. */
#page-spark .spark-result-penta-hint-slot{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:12px}
#page-spark .spark-result-penta-hint{font-family:var(--fm);font-size:max(10px,.5rem);font-weight:700;color:var(--tx3);border:1px solid var(--bd);border-radius:50%;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;cursor:help}
#page-spark .spark-result-chips-mini{display:flex;flex-wrap:wrap;gap:4px;flex:1;min-width:0}
/* .spark-result-line1/-line2 (cf. sparkRenderResult) : wrapper display:contents sur desktop — invisibles en
   tant que conteneurs, tous les enfants participent DIRECTEMENT au flex-row de .spark-result-row, comportement
   desktop inchangé (même pattern que .str-track-row-l1/l2 dans Structures pour un problème identique). */
#page-spark .spark-result-line1,#page-spark .spark-result-line2{display:contents}
/* Mobile : .spark-result-row (touche+gamme+indice penta+4 accords+tempo+3 boutons, conçue "tout sur une
   ligne" pour desktop, cf. commentaires ci-dessus) déborde du viewport — bug rapporté par capture, puis
   affiné sur plusieurs itérations ("le BPM doit être en 1ère ligne", "les 3 boutons... sur la 2e ligne avec
   les accords", "ramène la ligne des 3 boutons sur la même ligne que la progression"). Un simple order:1
   partagé par des enfants DIRECTS de .spark-result-row (essayé d'abord) ne garantit PAS qu'ils finissent sur
   la MÊME ligne après un flex-wrap — seulement qu'ils passent après les order:0, chacun pouvant sauter à la
   ligne indépendamment selon l'espace restant (constaté : accords et boutons se retrouvaient chacun sur leur
   propre ligne dès que la ligne 1 avait un peu de place libre, ex. "A Majeure"). Layout final robuste : les
   deux groupes sont de VRAIS conteneurs dans le DOM (.spark-result-line1 = ♪+touche+gamme+indice penta+BPM,
   .spark-result-line2 = accords+3 boutons) — .spark-result-row devient elle-même display:flex+flex-wrap en
   mobile, chaque wrapper occupe alors sa propre ligne complète sans ambiguïté possible. */
@media(max-width:1023px){
  #page-spark .spark-result-row{display:flex;flex-wrap:wrap;row-gap:4px}
  #page-spark .spark-result-line1,#page-spark .spark-result-line2{display:flex;align-items:center;gap:8px;width:100%}
  #page-spark .spark-result-chips-mini{flex:1;min-width:0}
  #page-spark .spark-apply-all-btn-mini{margin-left:0!important}
  /* Les 3 boutons "proches les uns des autres" (demande explicite) : la base pose margin-left:auto sur
     CHACUN (cf. sa déclaration, pensé pour n'avoir d'effet qu'une fois sur desktop où ils suivent tempo sur
     la même ligne) — neutralisée ci-dessus (!important, nécessaire pour battre la règle de base à
     spécificité égale) sur les 3 ; regagnée UNIQUEMENT sur le 1er des 3 (juste après .spark-result-chips-mini
     dans le DOM, maintenant tous deux dans .spark-result-line2) — pousse tout le TRIO groupé au bord droit
     de sa ligne (le 1er absorbe l'espace libre, les 2 suivants restent collés à lui), sans écartement
     interne entre eux. */
  #page-spark .spark-result-chips-mini+.spark-apply-all-btn-mini{margin-left:auto!important}
  /* Affinage demandé après vérification : "aligne le BPM sur la droite, et le badge (5) contre le nom de la
     tonalité". .spark-result-scale-mini a une largeur FIXE de 82px sur la base (pensée pour desktop, cf. sa
     déclaration — accueillir les noms de gamme les plus longs sans redimensionner la ligne) : le texte réel
     ("Dorien", "Majeure"...) est presque toujours bien plus court que 82px, laissant un grand vide avant le
     badge qui le suit dans le flux — pas "contre le nom" du tout. Largeur auto en mobile (colle au texte
     réel) résout ça directement, sans toucher .spark-result-penta-hint-slot lui-même (déjà positionné juste
     après dans le DOM, aucun repositionnement nécessaire une fois le vide de la colonne supprimé). */
  #page-spark .spark-result-scale-mini{width:auto;max-width:120px}
  /* BPM à droite de la ligne 1 : .spark-result-tempo-mini suivait directement l'indice penta sans marge —
     margin-left:auto l'envoie au bord droit de sa ligne (.spark-result-line1). */
  #page-spark .spark-result-tempo-mini{margin-left:auto}
}
#page-spark .spark-chord-chip-mini{font-family:var(--fb);font-weight:700;font-size:.62rem;padding:2px 7px;border-radius:999px;border:1px solid var(--bd);background:var(--s2);white-space:nowrap}
/* Tempo + conseil de groove (cf. GEN_GENRE_INFO/_sparkGenerateOne) — le ♫ (présent seulement si un conseil
   de jeu existe pour le genre choisi) signale visuellement que la chip porte une tooltip à consulter,
   plutôt qu'un simple chiffre BPM sans plus d'information exploitable. */
#page-spark .spark-result-tempo-mini{font-family:var(--fm);font-size:.6rem;color:var(--tx3);flex-shrink:0;white-space:nowrap;cursor:help}
/* Desktop uniquement : restaure l'ordre visuel d'origine (accords AVANT le BPM) — le déplacement de
   tempoHtml dans .spark-result-line1 (cf. sparkRenderResult, pour le regrouper avec touche/gamme/BPM en
   mobile) a changé son ordre DOM, qui se répercutait aussi sur desktop via display:contents (bug constaté :
   "5 126 BPM C F G F" au lieu de l'ordre attendu accords-puis-BPM). order:2 le repousse après
   .spark-result-chips-mini (restée order:0 par défaut) sans toucher au DOM ni au comportement mobile
   (@media(max-width:1023px) ne redéfinit jamais order, les 2 lignes sont de vrais conteneurs séparés là-bas). */
/* order:1 (pas 2) : les items order:0 (chips ET boutons, aucun des deux n'a de order explicite) se
   regroupent TOUS avant tout item order:1/2 — un simple order:2 sur le tempo le poussait donc après les
   BOUTONS aussi (constaté par capture : "accords, boutons, PUIS BPM" au lieu de "accords, BPM, boutons").
   order:1 sur le tempo ET order:2 sur les boutons donne enfin la séquence à 3 niveaux voulue. */
@media(min-width:1024px){
  #page-spark .spark-result-tempo-mini{order:1}
  #page-spark .spark-apply-all-btn-mini{order:2}
}
/* Bouton "Tout appliquer" réduit à un simple carré "✦" (garde la même fonction, cf. onclick inchangé côté
   JS) — demande explicite, la version texte complète prenait trop de place sur une ligne déjà chargée. */
#page-spark .spark-apply-all-btn-mini{margin-left:auto;flex-shrink:0;width:26px;height:26px;padding:0;display:flex;align-items:center;justify-content:center;border-radius:var(--r);border:1px solid var(--c-root);background:var(--c-root)22;color:var(--c-root);font-family:var(--fm);font-size:.7rem;font-weight:700;cursor:pointer;transition:all var(--tr)}
#page-spark .spark-apply-all-btn-mini:hover{background:var(--c-root);color:#111}
#page-spark .spark-hint{font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx3);line-height:1.4}
#page-spark .spark-video-suggestions{padding:9px;display:flex;flex-direction:column;gap:7px}
#page-spark .spark-video-suggestions-head{display:flex;align-items:center;justify-content:space-between;gap:8px;font-family:var(--fm);font-size:.6rem;font-weight:700;color:var(--tx2)}
#page-spark .spark-video-match-list{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px}
#page-spark .spark-video-match{min-width:0;display:flex;flex-direction:column;align-items:flex-start;gap:2px;padding:7px 8px;border:1px solid var(--bd);border-radius:var(--r);background:var(--s2);color:var(--tx);text-align:left;cursor:pointer}
#page-spark .spark-video-match:hover{border-color:var(--c-root);background:var(--s3)}
#page-spark .spark-video-match-formula{font-family:var(--fm);font-size:max(10px,.52rem);font-weight:700;white-space:nowrap}
#page-spark .spark-video-match-title{display:block;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--fb);font-size:.65rem;font-weight:600}
#page-spark .spark-video-match-meta{font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3);white-space:nowrap}
.ytb-formula-filter-notice{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:5px 7px;border:1px solid var(--c-root);border-radius:var(--r);background:var(--c-root)0f;color:var(--c-root);font-family:var(--fm);font-size:max(10px,.56rem);font-weight:700}
@media(max-width:700px){#page-spark .spark-video-match-list{grid-template-columns:1fr}#page-spark .spark-video-suggestions-head{align-items:flex-start}}
/* Action principale (Gammes+Drone+Progressions+Structures d'un coup) mise en avant par une bordure
   accentuée (pas un fond plein — couleur d'accent volontairement minimisée sur ce module, demandé
   explicitement), distincte des 3 envois individuels ci-dessous qui restent des actions secondaires sans
   bordure de couleur. Fond neutre (var(--s2), pas var(--c-root)) : élimine au passage l'ancien bug de
   couleur au survol (.spark-action-btn:hover{color:var(--tx)} rendait le texte clair illisible sur un
   fond déjà clair — non applicable ici puisque le fond reste neutre dans les deux états). */
#page-spark .spark-apply-all-btn{background:var(--s2);border:1px solid var(--c-root);color:var(--c-root);font-weight:700;font-size:.68rem;padding:9px 11px}
#page-spark .spark-apply-all-btn:hover{background:var(--s3);color:var(--c-root)}

/* Hover générique pour TOUT bouton HTML natif de l'app (demande explicite : "les boutons de la topbar ont
   un effet au survol, peut-on l'ajouter à tous les boutons de ce style ?") — la grande majorité des
   boutons de l'app sont stylés en CSS INLINE (cf. app.js, construits en JS avec un cssText complet) plutôt
   que via une classe partagée, donc sans aucun ::hover possible directement sur eux (un attribut style="..."
   ne peut jamais définir de pseudo-état). Règle à très basse spécificité (juste "button:hover") plutôt
   qu'un sélecteur ciblant une couleur précise (ex. background:var(--s3)) : un sélecteur d'attribut par
   contenu de style serait fragile (ordre des propriétés CSS non garanti dans le cssText généré) et risquerait
   de rater des variantes. filter:brightness() reste visuellement cohérent quel que soit le fond/la bordure
   déjà en place (fonctionne aussi bien sur var(--s2)/var(--s3)/var(--c-root) actif que sur un fond
   transparent), contrairement à changer background/border-color en dur qui écraserait des couleurs d'état
   (ex. .on actif) déjà posées par le JS. Les boutons ayant DÉJÀ leur propre règle :hover plus spécifique
   (.ibtn:hover, #page-metro .ctrl-btn:hover...) restent prioritaires par la cascade CSS normale — cette
   règle ne s'applique qu'aux boutons qui n'en avaient encore aucun. */
@media(hover:hover){button:hover{filter:brightness(1.15)}}

/* ═══ MODULE JOUER (#page-play) — instrument tactile isomorphe ═══
   Cible PRIORITAIRE : mobile tactile (demande explicite). Tout ce bloc est scopé #page-play (règle du
   projet : jamais de classe utilitaire non scopée, cf. dev/CLAUDE.md). La grille doit remplir tout l'espace
   restant sous la barre de contrôles, sans jamais scroller : on joue dessus, un scroll parasite au doigt
   serait le pire défaut possible. */
#page-play{overflow:hidden}
/* Barre de contrôles compacte : sur mobile elle doit rester sur une seule rangée et ne pas manger la
   hauteur de jeu. flex-wrap:wrap en repli si vraiment trop étroit. */
#page-play .gs-bar{gap:6px;flex-wrap:wrap}
#page-play .gs-bar .sound-select{font-size:.6rem;padding:.25rem .3rem;max-width:130px}
/* margin-right RETIRÉ (rapporté : "espace vide inutile entre le sélecteur octave et le sélecteur
   tonalité") — resté d'une époque où .gs-oct-group regroupait plusieurs boutons (−/valeur/+) qu'il fallait
   distancer du reste de la rangée ; #gsOctSwipeZone (cf. plus bas) est désormais un élément UNIQUE, et
   #gsOctRow pose déjà son propre gap:4px entre tous ses enfants — ce margin-right s'AJOUTAIT à ce gap
   (4+6=10px) au lieu de le remplacer, créant un espace visiblement plus large avant le bouton Tonique que
   partout ailleurs dans la rangée. */
#page-play .gs-oct-group{display:inline-flex;align-items:center;gap:4px}
/* Boutons ± SUPPRIMÉS (demande explicite, cf. _gsOctSwipeBind dans app.js : glisser gauche/droite sur
   #gsOctSwipeZone remplace les 2 boutons). #gsOctSwipeZone reprend le rôle de zone tactile élargie (même
   raison d'être que l'ancien commentaire ci-dessous sur .tbtn : cible mobile confortable), mais comme
   surface de GLISSER plutôt que 2 cibles de tap distinctes. touch-action:pan-y laisse le défilement vertical
   de la page fonctionner normalement tout en réservant l'axe horizontal au geste (sinon le navigateur
   interprèterait aussi un glisser horizontal comme un geste de navigation système sur certains mobiles). */
/* BUG CORRIGÉ (rapporté : "sélecteur d'octave dans la barre de jeu : uniformiser sa taille aux autres
   boutons" — encore inégal après un 1er essai à 28px) — 28px est la hauteur GÉNÉRIQUE de .gs-mode-btn, mais
   #gsOctRow porte sa PROPRE surcharge à 38px pour tous ses .gs-mode-btn (cf. "#page-play #gsOctRow
   .gs-mode-btn{height:38px}" plus bas dans ce fichier, posée pour égaliser Latch/Pitch/Groove/Rec/⤢ avec les
   boutons d'octave surdimensionnés d'origine, 52×38px) : 28px visait la mauvaise référence, ignorant cette
   surcharge plus spécifique déjà en place dans CETTE rangée précise. 38px aligné explicitement ici pour ne
   pas dépendre d'une règle définie ailleurs dans le fichier — cette zone n'est pas un .gs-mode-btn (elle n'a
   pas cette classe), la surcharge de #gsOctRow ne la concernait donc jamais automatiquement. */
/* BUG CORRIGÉ (rapporté : "appui long sur sélecteur octave surface ne fonctionne pas sur mon Mi11 [Android/
   MIUI]. Il fonctionne sur desktop") — touch-action:pan-y AUTORISAIT explicitement le navigateur à
   revendiquer le geste pour un défilement vertical de page, et -webkit-touch-callout (jamais posé ici)
   restait à sa valeur par défaut : sur Android/MIUI Chrome, un doigt immobile ~500ms dans ces conditions
   déclenche la reconnaissance de geste NATIVE du navigateur (menu contextuel/sélection de texte), qui peut
   avaler le toucher ou émettre un pointercancel avant même que le minuteur d'appui long de _gsOctSwipeBind
   (450ms, cf. app.js) n'ait eu le temps de se déclencher — d'où "fonctionne sur desktop" (aucune de ces
   heuristiques tactiles n'existe à la souris) mais pas sur ce téléphone précis. touch-action:none (au lieu de
   pan-y — cette zone est un petit contrôle fixe, pas une liste à faire défiler, un défilement vertical n'a
   jamais eu de sens ici) + -webkit-touch-callout:none : même patron que TOUTES les autres zones à appui long
   du projet (cf. .gs-cell/.gs-chord-cell/.rh-cell, qui n'ont jamais eu ce problème).
   -webkit-tap-highlight-color:transparent ajouté au passage, même raison que ces zones : sans lui, Chrome
   Android affiche un flash de surbrillance gris à chaque tap qui n'a pas lieu d'être sur ce contrôle. */
#page-play .gs-oct-swipe-zone{
  height:38px;cursor:ew-resize;touch-action:none;user-select:none;-webkit-user-select:none;
  -webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;
  padding:0 10px;border-radius:6px;background:var(--pbg,transparent);border:1px solid var(--bd);
}
#page-play .gs-oct-swipe-zone:active{background:var(--s3)}
/* Boutons −/+ autour de la zone d'octave : masqués par défaut, _gsOctPmFit (surface.js) pose .gs-oct-pm-on sur
   #gsOctRow quand la rangée a la place de les afficher sans passer à la ligne. */
#page-play .gs-oct-pm{display:none;min-width:34px;padding:0 8px;font-size:1rem;line-height:1}
#page-play #gsOctRow.gs-oct-pm-on .gs-oct-pm{display:inline-flex;align-items:center;justify-content:center}
@media(max-width:600px){
  /* Miroir de la surcharge mobile de #gsOctRow .gs-mode-btn (32px, cf. plus bas) — même raison, garder la
     cohérence de hauteur à toutes les largeurs, pas seulement desktop. */
  #page-play .gs-oct-swipe-zone{height:32px}
}
/* Taille de police ALIGNÉE sur .gs-mode-btn (.58rem) — demande explicite d'uniformiser la hauteur du
   conteneur, mais un texte manifestement plus gros que ses voisins (.95rem d'origine) aurait quand même
   donné une impression de rangée dépareillée même une fois les hauteurs de boîte identiques. min-width
   élargi conservé pour un libellé de note ("C3", "F#4") : la rangée ne se réajuste pas à chaque changement
   d'octave. */
#page-play .gs-oct-val{font-family:var(--fm);font-size:max(10px,.58rem);font-weight:700;color:var(--tx);min-width:32px;text-align:center;pointer-events:none}
/* width:auto INDISPENSABLE : .tbtn (classe utilitaire partagée) impose width:20px;height:20px pour ses
   boutons carrés ±/↺, et un padding seul ne peut pas contrer une largeur FIXE — le bouton était rendu à 22px
   de large, son libellé « ♪ Accomp. » écrasé/coupé (bug signalé par capture). Piège des classes utilitaires
   réutilisées entre modules, documenté dans dev/CLAUDE.md. */
#page-play .gs-accomp-btn{width:auto;min-width:0;padding:0 10px;height:28px;font-size:.6rem;white-space:nowrap}
#page-play .gs-accomp-btn.on{background:var(--c-root);color:#111;border-color:var(--c-root)}

/* Rangée fine dédiée à l'octave des notes, collée à la grille (demande explicite : "le sélecteur d'octave
   pour les notes se déplace au plus près de la grille, en haut de celle-ci") — pas une vraie subbar (pas de
   bordure basse ni de fond distinct des 2 barres au-dessus, juste un espacement minimal) puisqu'elle ne
   porte qu'UN SEUL contrôle, centré, pour rester visuellement rattachée à la grille plutôt qu'aux barres. */
/* justify-content:center CENTRE le contenu, mais si celui-ci dépasse la largeur disponible il déborde
   SYMÉTRIQUEMENT — le début de la rangée passe alors en coordonnée négative et devient définitivement
   inatteignable (constaté sur mobile 412px : le bouton "octave −" était coupé hors écran à gauche, et la
   fin de rangée tronquée à droite, sans aucun moyen d'y accéder).
   overflow-x:auto rend la rangée défilable dans ce cas ; justify-content:safe center garde le centrage
   quand ça tient, mais bascule sur un alignement au début dès que ça déborde, ce qui préserve l'accès au
   premier élément. Cette rangée porte les contrôles de JEU (octave, mode, latch…) et reste visible en plein
   écran : aucun de ses boutons ne doit jamais devenir inaccessible. */
/* mask-image en dégradé horizontal (demande explicite, répétée 2x : "difficile de scroll... toujours pas
   accès à tous les boutons") — le défilement fonctionnait déjà (overflow-x:auto ci-dessous), mais la
   scrollbar est délibérément masquée (scrollbar-width:none) pour l'esthétique, ne laissant ALORS aucun
   indice visuel qu'il reste du contenu hors champ. Un bord qui s'estompe en fondu (au lieu d'être tranché
   net) signale sans mot ni icône qu'il y a plus à voir de ce côté — technique standard des rangées
   scrollables horizontales (ex. rails de vignettes). scroll-snap-type ajouté en plus : sans lui, un
   défilement au doigt s'arrêtait à une position arbitraire (bouton coupé en 2 pile au bord), scroll-snap
   aligne toujours sur le début d'un bouton entier. */
#page-play .gs-oct-row{
  display:flex;justify-content:safe center;gap:4px;padding:2px 4px;flex-shrink:0;
  overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-webkit-overflow-scrolling:touch;
  scroll-snap-type:x proximity;
  mask-image:linear-gradient(to right,transparent,#000 14px,#000 calc(100% - 14px),transparent);
  -webkit-mask-image:linear-gradient(to right,transparent,#000 14px,#000 calc(100% - 14px),transparent);
}
#page-play .gs-oct-row>*{scroll-snap-align:start}
#page-play .gs-oct-row::-webkit-scrollbar{display:none}
#page-play .gs-oct-row>*{flex:0 0 auto}

/* Conteneur de la grille : prend TOUTE la hauteur restante (flex:1 dans la .page en colonne).
   touch-action:none sur le conteneur ET les cellules — la grille ne doit jamais défiler ni déclencher un
   geste de navigation du navigateur pendant qu'on joue. C'est la différence avec la grille Groovebox, qui
   pose touch-action:pan-x parce qu'elle, doit rester scrollable horizontalement.
   user-select/tap-highlight/touch-callout neutralisés : sans ça, un appui maintenu sur mobile sélectionne
   le texte du degré ou ouvre le menu contextuel au bout de ~500ms, en plein milieu d'une note tenue. */
#page-play .gs-grid-wrap{
  position:relative; /* ancre le séparateur physique #gsChordSeparator (position:absolute), cf. plus bas */
  /* flex-direction:row EXPLICITE (piège du projet déjà documenté ailleurs, dev/CLAUDE.md) : sur desktop
     (≥1150px), la règle générique ".page > *:not(div:first-child):not(.sticky-toolbar)" pose
     flex-direction:column sur cet élément (enfant direct de #page-play) — sans effet visible tant qu'il n'y
     avait qu'UN SEUL enfant flex (.gs-grid, qui remplit 100% dans les 2 sens), mais l'ajout du pitch wheel
     (2e enfant flex, cf. .gs-pitchwheel) l'a révélé : les 2 s'empilaient verticalement au lieu de se
     partager la largeur, écrasant la bande à 2px de haut (retour utilisateur : "je ne le vois pas"). */
  flex:1;min-height:0;display:flex;flex-direction:row;padding:6px;box-sizing:border-box;
  touch-action:none;-webkit-user-select:none;user-select:none;
  -webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;
}
/* Marge de sécurité en BAS, en plein écran seulement (bug rapporté initialement : "la note la plus grave
   subit un délai au lancement, avec parfois des sautes de gain" — la note grave est TOUJOURS la ligne du
   bas, cf. gsRender, et en plein écran cette ligne touche le bord physique inférieur de l'écran, cf.
   body.gs-fullscreen #page-play{bottom:0} plus bas). Sur Android sans bouton physique (geste de retour), le
   système intercepte/retarde parfois le PREMIER toucher dans cette bande, avant même qu'il n'atteigne le
   DOM — indétectable côté JS (mesuré : aucune latence de programmation Web Audio, même à froid). Hors
   plein écran, la barre système/l'espace sous la grille suffisent déjà ; on ne pénalise donc que le mode où
   le risque existe réellement. env() avec repli 12px pour les navigateurs sans zone sûre déclarée. */
body.gs-fullscreen #page-play .gs-grid-wrap{
  padding-bottom:max(12px,env(safe-area-inset-bottom,12px));
}
/* Marge de sécurité à GAUCHE, cette fois EN PERMANENCE (bug rapporté ensuite, plus précis : "les notes les
   plus graves (en bas à GAUCHE) ne réagissent pas comme les autres... difficile à jouer, parfois ne
   déclenche pas, ou avec de la latence"). Sur mobile la sidebar/les onglets passent dans la topbar (cf.
   .main-content{margin-left:0} sous 600px) : le bord GAUCHE de la grille touche alors le bord PHYSIQUE de
   l'écran, exactement la zone que iOS (retour en glissant depuis le bord) et Android (geste de retour)
   interceptent en PRIORITÉ pour leur propre geste système, y compris hors plein écran puisque cette
   interception ne dépend PAS du fullscreen mais de la position à l'écran. La case en bas à gauche cumule
   ainsi le risque du bord bas ET du bord gauche, ce qui explique qu'elle soit signalée comme la pire —
   d'où la marge posée ici sans condition de plein écran, contrairement à la marge du bas ci-dessus. */
#page-play .gs-grid-wrap{
  padding-left:max(6px,env(safe-area-inset-left,6px));
}
/* .gs-func-free-anchor prend désormais la place flex-item que #gsGrid occupait seul avant la SURFACE LIBRE
   (demande explicite, cf. le commentaire détaillé dans index.html) — #gsGrid lui-même redevient un simple
   enfant en position:absolute;inset:0 de cette ancre, plutôt que le flex-item direct de .gs-grid-wrap. */
#page-play .gs-func-free-anchor{flex:1;position:relative;min-height:0}
#page-play .gs-grid{
  position:absolute;inset:0;display:grid;
  grid-template-columns:repeat(var(--gs-cols,7),1fr);
  grid-template-rows:repeat(var(--gs-rows,6),1fr);
  gap:5px;
}
/* Calque de la SURFACE LIBRE (widgets positionnés en %, PAR-DESSUS la grille) — pointer-events:none sur le
   calque LUI-MÊME (jamais de clic parasite dans les zones vides entre 2 widgets, qui doivent laisser passer
   le tap vers la case de la grille en dessous) ; chaque widget individuel (.gs-func-free-widget) redevient
   interactif via pointer-events:auto, cf. plus bas. z-index au-dessus de .gs-cell (aucun z-index propre) pour
   toujours peindre PAR-DESSUS la grille de notes, jamais en dessous. */
#page-play .gs-func-free-layer{position:absolute;inset:0;z-index:5;pointer-events:none;will-change:transform} /* calque permanent (vide = 0 octet) : cf. .gs-chord-separator */
/* Widget individuel de la surface libre — pointer-events:auto RÉACTIVÉ ici (le calque parent les bloque, cf.
   ci-dessus) : chaque widget est une cible tactile indépendante, positionnée en % via style inline
   (_gsRenderFuncFreeWidgets, app.js), jamais par CSS Grid (aucun rapport avec le système de pistes de la
   note-grille en dessous). Même teinte neutre que .gs-func-cell (var(--s1)) pour rester visuellement de la
   MÊME FAMILLE que les boutons de grille — un widget libre n'est pas un concept différent, juste positionné
   différemment. Ombre portée : sans ça, un widget par-dessus une case de note COLORÉE serait parfois
   difficile à distinguer visuellement de la case en dessous. */
#page-play .gs-func-free-widget{
  position:absolute;box-sizing:border-box;pointer-events:auto;
  background:var(--s1);border:1px solid var(--bd2);border-radius:var(--r);
  display:flex;align-items:center;justify-content:center;
  box-shadow:0 2px 8px var(--sh);cursor:pointer;user-select:none;-webkit-user-select:none;
  font-family:var(--fm);color:var(--tx2);
  container-type:size; /* nécessaire pour .gs-func-knob-dial (cqh) et tout futur usage de cqw/cqh à l'intérieur d'un widget libre — même mécanisme que .gs-cell, cf. son propre container-type:size */
}
#page-play .gs-func-free-widget.gs-on{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-play .gs-func-free-widget.gs-func-editable{border-style:dashed;border-color:var(--c-root)}
#page-play .gs-func-free-label{position:relative;z-index:1;font-size:max(10px,.5rem)!important;font-weight:700;padding:0 4px;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}
/* Poignée de redimensionnement (demande explicite : "poignée de coin") — visible/interactive SEULEMENT en
   mode édition (posée conditionnellement par _gsRenderFuncFreeWidgets, jamais en jeu normal). Coin bas-droit,
   convention universelle des interfaces de redimensionnement (fenêtres, formes de dessin vectoriel...). */
#page-play .gs-func-free-resize{
  position:absolute;right:-4px;bottom:-4px;width:16px;height:16px;
  border-radius:50%;background:var(--c-root);border:2px solid var(--bg);
  cursor:nwse-resize;z-index:2;
}
@media(hover:hover){
  #page-play .gs-func-free-widget:not(.gs-on):hover{background:color-mix(in srgb,var(--c-root) 20%,var(--s1))}
}
/* Cellule : cible tactile. Pas d'aspect-ratio — les cases s'étirent pour remplir exactement la surface
   disponible (grid 1fr sur les 2 axes), ce qui donne les plus GROSSES cases possibles sur un écran donné.
   C'est le bon arbitrage pour du tactile : mieux vaut des cases larges et un peu rectangulaires que des
   carrés parfaits entourés de vide perdu. */
/* --gs-on-dur (demande explicite : "mets .gs-cell.gs-on dans les effets éditables dans paramètres", puis
   précisé : "l'activation de la couleur doit être instantané, le paramètre que l'utilisateur peut varier est
   le fadeout uniquement") — durée du FONDU DE RETOUR à la couleur normale quand une case s'éteint (distincte
   de --gs-flash-dur/.gs-flashing, le fondu décoratif indépendant de la note réelle, cf. son commentaire plus
   bas : les 2 courbes coexistent). L'ALLUMAGE (.gs-on ajoutée) reste toujours instantané (transition:none sur
   .gs-cell.gs-on ci-dessous, spécificité égale mais placée APRÈS la règle de base → gagne pour l'attaque) —
   seul le RETRAIT de .gs-on (retour à .gs-cell seule) hérite du fondu réglable, posé sur la règle de base.
   .06s en dur remplacé par une variable CSS posée sur #page-play (cf. gsSetOnColorMs dans app.js).
   transform reste sur .06s FIXE (pas dans la variable) : ce n'est pas la couleur mais l'effet "pad enfoncé"
   (scale .96), un retour tactile immédiat qui n'a pas de raison de suivre le même réglage que la couleur. */
#page-play{--gs-on-dur:.06s}
#page-play .gs-cell{
  display:flex;align-items:center;justify-content:center;
  background:var(--s3);border:1px solid var(--bd);border-radius:8px;
  cursor:pointer;transition:background var(--gs-on-dur),border-color var(--gs-on-dur),--gs-pk .06s;
  /* Rétrécissement "pad enfoncé" animé via --gs-pk (@property) et NON via une transition de transform : cette
     dernière promouvait chaque case touchée en calque GPU le temps de l'animation (flashs noirs mobile, cf.
     .nav-overlay). Une propriété enregistrée interpolée est non composée : même rendu, aucun calque. */
  --gs-pk:1;transform:scale(var(--gs-pk));
  touch-action:none;-webkit-user-select:none;user-select:none;
  -webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;
  min-height:0;overflow:hidden;
}
#page-play .gs-deg{
  font-family:var(--fm);font-size:.7rem;font-weight:700;color:var(--tx3);
  pointer-events:none; /* le closest('.gs-cell') du handler ne doit jamais tomber sur le <span> */
}
/* Tonique : c'est LE repère visuel du module ("où est la maison"), d'où un contraste franc plutôt qu'une
   nuance discrète — un enfant doit la retrouver d'un coup d'œil sans lire quoi que ce soit.
   border-width/border-color sont posés SÉPARÉMENT de color-mix : sur un navigateur qui ne supporte pas
   color-mix (Safari < 16.2, cf. l'audit compat), seule la déclaration `background` est invalide et donc
   ignorée — la bordure épaisse colorée, elle, s'applique quand même et suffit à identifier la tonique.
   Repère dégradé plutôt que perdu. */
#page-play .gs-cell.gs-root{border-color:var(--c-root);border-width:2px}
#page-play .gs-cell.gs-root{background:color-mix(in srgb,var(--s3) 55%,var(--c-root) 45%)}
#page-play .gs-cell.gs-root .gs-deg{color:var(--tx)}
/* Note enfoncée : retour visuel immédiat, indispensable au doigt (on ne voit pas sa propre main).
   transition:background 0s,border-color 0s AJOUTÉ ICI (demande explicite : "l'activation de la couleur doit
   être instantané, le paramètre que l'utilisateur peut varier est le fadeout uniquement") — .gs-cell (règle
   de base juste au-dessus) porte désormais transition:background var(--gs-on-dur) réglable ; sans cette
   règle plus spécifique/plus tardive, l'ALLUMAGE hériterait aussi de ce fondu (transition s'applique aux DEUX
   sens d'un changement de valeur, ajout de classe compris). transform reste sur .06s hérité de la règle de
   base (non répété ici) : seul le retour tactile instantané visé est background/border-color. */
/* --gs-press-scale (demande explicite : "augmente l'effet de rétrécissement du pad touché, mets-le aussi
   dans les paramètres visuels réglables") — .96 en dur (4% de retrait) remplacé par une variable CSS posée
   sur #page-play (cf. gsSetPressScale dans app.js), même patron que --gs-on-dur/--gs-flash-dur. */
#page-play{--gs-press-scale:.85}
@property --gs-pk{syntax:'<number>';inherits:false;initial-value:1}
#page-play .gs-cell.gs-on{background:var(--c-root);border-color:var(--c-root);--gs-pk:var(--gs-press-scale);transition:background 0s,border-color 0s,--gs-pk .06s}
#page-play .gs-cell.gs-on .gs-deg{color:#111}
/* Survol souris seulement (desktop) — @media(hover:hover) évite l'état "collé" au doigt sur mobile, où
   :hover persiste après le relâchement. Même précaution que les 2 autres usages du projet. */
@media(hover:hover){
  #page-play .gs-cell:hover:not(.gs-on){background:color-mix(in srgb,var(--s3) 70%,var(--tx) 30%)}
}

/* ── Mode HEXAGONAL (dev/spec_mode_hexagonal_fret_theory.md) — chemin de rendu SÉPARÉ, cf. app.js ─────────────
   #gsHexGridWrap/#gsHexGrid sont des éléments DISTINCTS de #gsGridWrap/#gsGrid ci-dessus (jamais partagés) :
   une grille hexagonale n'a pas de correspondance native avec grid-template-columns/rows, chaque cellule est
   positionnée par coordonnées calculées en JS (_gsHexToPixel, app.js) via left/top en position:absolute,
   plutôt que par CSS Grid. .gs-hex-grid-wrap réutilise .gs-grid-wrap (padding/touch-action/marges de sécurité
   déjà corrects, cf. commentaires ci-dessus) — seule la règle .gs-hex-grid diffère du pipeline CSS Grid rect. */
/* overflow:auto (pas hidden) : la largeur/hauteur du conteneur est fixée en JS d'après le nuage de cellules
   (gsHexRender, app.js) et peut dépasser l'espace visible selon le rayon/la taille d'écran — un défilement
   natif garantit que TOUTE cellule reste atteignable au doigt plutôt que clippée hors champ. Le réglage fin
   de la taille de cellule pour un ajustement automatique à l'écran est laissé en V1.1 (cf. gsHexCellSize). */
#page-play .gs-hex-grid-wrap{overflow:auto}
#page-play .gs-hex-grid{position:relative;min-height:0;margin:auto}
/* Hexagone flat-top (le grand côté plat en haut/bas) via clip-path — cohérent avec l'orientation choisie
   dans _gsHexToPixel (app.js, formules flat-top). Taille fixée par JS via --gs-hex-size (posée par cellule
   au moment du rendu, cf. gsHexRender). */
/* --gs-hex-size = le RAYON (centre→coin) utilisé par _gsHexToPixel (app.js) pour l'espacement entre centres
   — bug corrigé : "espace énorme entre les cellules" — la largeur affichée doit être 2×size (le diamètre
   d'un hexagone flat-top), pas `size` lui-même, sinon chaque hexagone ne remplit que la moitié de l'espace
   réellement alloué par la formule de positionnement, laissant un vide béant entre voisins. Hauteur
   flat-top = sqrt(3)×size ≈ size×1.732 (PAS ×1.1547, qui est l'inverse — confondre les 2 aplatit l'hexagone
   au lieu de l'étirer, mais l'erreur de largeur était la cause dominante du vide visible). */
/* Étiquettes réutilisées TELLES QUELLES (.gs-deg/.gs-nn, cf. règles #page-play .gs-deg/.gs-nn plus bas dans
   ce fichier) — retour utilisateur : "annotations dans les pads : même chose qu'en mode rectangulaire (note
   et intervalle) + remplir plus d'espace". container-type:size (même patron que #page-play .gs-cell) : les
   clamp(...,Ncqh,...) de ces règles partagées se dimensionnent alors par rapport à LA BOÎTE DU PAD HEX
   lui-même, pas d'un ancêtre plus grand — sans ça le texte resterait minuscule quelle que soit la taille
   réelle de l'hexagone à l'écran. flex-direction:column : empile intervalle (gros) au-dessus du nom de note
   (petit), identique à .gs-grid.gs-has-names.gs-cell côté rect. */
/* Facteur 0.96 (pas 1.0) : retour utilisateur — "il faut un espace entre les pads, très fin mais existant"
   après le correctif précédent qui faisait TOUCHER les hexagones exactement bord à bord (aucun interstice,
   parfois perçu comme un léger chevauchement selon l'arrondi de rendu du navigateur). Un hexagone très
   légèrement plus petit que l'espace qui lui est alloué par _gsHexToPixel (app.js) laisse un fin liseré de
   fond visible entre voisins, sans réintroduire le vide béant du tout premier réglage (60px non multiplié). */
/* Bug corrigé (2 tentatives CSS pur ont échoué avant celle-ci — historique gardé pour éviter de les
   retenter) :
   1. `border` sur un élément avec clip-path : le trait est dessiné le long du RECTANGLE englobant AVANT
      découpe, seuls les segments qui tombent sur une arête horizontale/verticale survivent — contour "à
      moitié dessiné".
   2. Technique "hexagone double" (::before plus grand, z-index:-1, censé rester confiné derrière son propre
      parent) : le fond réel de chaque cellule restait correct, mais le ::before de couleur PLEINE se peignait
      par-dessus des cellules VOISINES — clip-path + z-index négatif ne restait pas scopé à sa pile locale
      malgré position:absolute+z-index:0 sur le parent.
   Solution retenue : chaque cellule est un <svg><polygon></svg> (cf. gsHexRender, app.js) — un trait SVG
   natif (`stroke`) suit intrinsèquement la silhouette réelle du polygone, sans aucun des pièges de clip-path/
   z-index/pseudo-éléments ci-dessus. `--gs-hex-bg`/`--gs-hex-bc`/`--gs-hex-bw` restent les 3 variables
   pilotées par les règles [style*="--gs-c"] plus bas, seule leur CONSOMMATION change (fill/stroke du
   <polygon> au lieu de background/border du <div>). */
/* overflow:hidden AJOUTÉ (rapporté : "attention aux effets visuels qui débordent en mode hexagonal") —
   _gsFxBurst (app.js) crée le même .gs-ripple ICI que sur la grille rectangulaire (aucune branche
   hex-spécifique dans cette fonction, jamais un 2e système d'effets, cf. la règle du projet). Or .gs-ripple
   est dimensionné en % de SA BOÎTE ENGLOBANTE (left:50%;top:50%;width:100%;padding-bottom:100%, cf. sa
   règle), un calcul pensé pour une case CARRÉE (.gs-cell, grille rectangulaire) : appliqué à .gs-hex-cell
   (boîte englobante rectangulaire d'un hexagone dessiné en SVG à l'intérieur, cf. .gs-hex-svg juste en
   dessous), le ripple grandit bien au-delà des bords visibles de l'hexagone lui-même.
   BUG CORRIGÉ (re-rapporté malgré overflow:hidden ci-dessus : "en mode hexagonal, les effets ripple
   débordent de la cellule") — overflow:hidden seul ne clippe qu'à la boîte englobante RECTANGULAIRE, pas à
   la silhouette hexagonale réelle (déjà documenté comme limite connue à l'introduction de ce correctif) :
   un ripple proche d'un coin coupé de l'hexagone continue de peindre dans cette zone, qui appartient
   visuellement à la case VOISINE. clip-path AJOUTÉ ICI avec EXACTEMENT le même polygone que HEX_PTS
   (app.js, gsHexRender : '25,0 75,0 100,50 75,100 25,100 0,50', déjà utilisé pour l'ancien clip-path avant
   son remplacement par un <svg><polygon>, cf. le commentaire au-dessus de HEX_PTS) — les 2 valeurs DOIVENT
   rester identiques, un désaccord entre elles clipperait les effets à une forme différente de celle
   réellement peinte par le SVG. En % (0-100), donc valide quelle que soit la taille réelle de la case.
   overflow:hidden CONSERVÉ en plus (pas redondant) : clip-path seul ne borne pas nécessairement un contenu
   qui ignorerait le containing block dans certains moteurs de rendu plus anciens — les 2 ensemble sont la
   garantie la plus large. */
#page-play .gs-hex-cell{
  position:absolute;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2cqh;
  width:calc(var(--gs-hex-size,60px)*2*0.96);height:calc(var(--gs-hex-size,60px)*1.7320508*0.96);
  container-type:size;
  overflow:hidden;
  clip-path:polygon(25% 0,75% 0,100% 50%,75% 100%,25% 100%,0 50%);
  cursor:pointer;
  touch-action:none;-webkit-user-select:none;user-select:none;
  -webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;
  --gs-hex-bg:var(--s3); /* couleur de fond courante, ajustée par les variantes ci-dessous */
  --gs-hex-bw:1.5px; /* épaisseur de liseré courante */
  --gs-hex-bc:transparent; /* couleur de liseré courante */
}
#page-play .gs-hex-svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
#page-play .gs-hex-poly{
  fill:var(--gs-hex-bg);stroke:var(--gs-hex-bc);stroke-width:var(--gs-hex-bw);
  vector-effect:non-scaling-stroke; /* épaisseur de trait CONSTANTE en px réels, pas à l'échelle du viewBox
    100×100 — sans ça, stroke-width:2 dans un viewBox 100 unités correspondrait à un trait bien plus épais
    qu'attendu une fois le SVG mis à l'échelle réelle (~100px), noyant le contour dans la couleur de fond. */
  transition:fill .06s;
}
#page-play .gs-hex-cell .gs-deg,#page-play .gs-hex-cell .gs-nn{pointer-events:none;position:relative}
#page-play .gs-hex-cell.gs-root{--gs-hex-bg:color-mix(in srgb,var(--s3) 55%,var(--c-root) 45%);--gs-hex-bc:var(--c-root);--gs-hex-bw:2px}
#page-play .gs-hex-cell.gs-root .gs-deg{color:var(--tx)}
#page-play .gs-hex-cell.gs-on{--gs-hex-bg:var(--c-root);--gs-hex-bc:var(--c-root)}
/* --gs-press-scale partagé avec .gs-cell.gs-on (grille rect, cf. sa règle plus bas) — même réglage utilisateur
   (gsSetPressScale, app.js), les 2 formes de grille doivent réagir de façon cohérente au même curseur. */
#page-play .gs-hex-cell.gs-on .gs-hex-svg{transform:scale(var(--gs-press-scale))}
#page-play .gs-hex-cell.gs-on .gs-deg{color:#111}
#page-play .gs-hex-cell.gs-chord-cell{--gs-hex-bc:var(--c-root);--gs-hex-bw:2px}
/* Estompage des notes hors gamme, TOUJOURS actif (bug corrigé, retour utilisateur : "utiliser la couleur des
   pads verrouillés même en mode gamme (non verrouillé)" — .gs-out était auparavant posée seulement quand
   gsHexScaleLock!=='off', copiant à tort le geste "verrou" plutôt que le comportement du mode RECTANGULAIRE
   équivalent, cf. .gs-cell.gs-out côté rect : là, l'estompage n'a JAMAIS dépendu d'un verrou). La cellule
   reste PARFAITEMENT jouable ici (aucune classe ne touche au pointeur/MIDI, cf. gsHexPointerDown qui ne lit
   jamais .gs-out), seulement estompée pour repérer la gamme d'un coup d'œil — ce palier correspond à l'état
   gsHexScaleLock==='off' du bouton "Gamme" (rien de plus qu'avant, juste permanent). */
#page-play .gs-hex-cell.gs-out .gs-hex-poly{opacity:.55}
#page-play .gs-hex-cell.gs-out .gs-deg{color:var(--tx3);font-weight:400}
#page-play .gs-hex-cell.gs-out.gs-on .gs-hex-poly{opacity:1}
/* Palier 'dim' du bouton "Gamme" (2e cran) : demande explicite — reprend l'apparence qu'avait auparavant le
   palier 'block' (estompage plus marqué que le simple .gs-out ci-dessus), MAIS reste jouable (jamais de
   .gs-locked ici, cf. gsHexRender qui ne pose .gs-dim-strong que sur ce palier précis). */
#page-play .gs-hex-cell.gs-dim-strong .gs-hex-poly{opacity:.32}
/* Palier 'block' (3e cran, verrouillage RÉEL) : demande explicite — "block est complètement noirci", un
   palier plus fort que l'ancien 'block' (qui reprend maintenant l'apparence de 'dim' ci-dessus). cursor:
   not-allowed + estompage quasi total, pour ne laisser deviner la forme du pad que par contraste avec ses
   voisins — jamais .gs-on ici (gsHexPointerDown/Move refusent le jeu avant toute création de voix, cf. leur
   définition, donc .gs-on n'est structurellement jamais posée en même temps que .gs-locked). */
#page-play .gs-hex-cell.gs-locked{cursor:not-allowed}
#page-play .gs-hex-cell.gs-locked .gs-hex-poly{opacity:.08}
@media(hover:hover){
  #page-play .gs-hex-cell:hover:not(.gs-on) .gs-hex-poly{fill:color-mix(in srgb,var(--s3) 70%,var(--tx) 30%)}
}
/* ── Code couleur — MÊME MÉCANISME que le mode rectangulaire (retour utilisateur : "reprends les couleurs
   (fond de cellule et étiquette) et effets du mode rectangulaire pour le mode hexagonal") — miroir direct des
   règles #page-play .gs-cell[style*="--gs-c"] (cf. plus bas dans ce fichier) : --gs-c est posée par
   gsHexRender() (app.js) à partir des MÊMES référentiels globaux (_gsCellColor, INTERVAL_COLORS/NOTE_COLORS),
   jamais une palette propre à l'hexagone (spec §6, "ne pas créer un 2e système de couleur"). Contour par
   défaut (liseré à opacité réelle + fond légèrement teinté), variante aplat via .gs-fill (même classe que le
   rect, posée par gsHexRender selon gsColorStyle — un seul réglage utilisateur pour les 2 géométries). */
#page-play .gs-hex-cell[style*="--gs-c"]{
  --gs-hex-bg:color-mix(in srgb,var(--s3) 78%,var(--gs-c) 22%);
  --gs-hex-bc:var(--gs-c);
}
#page-play .gs-hex-cell[style*="--gs-c"] .gs-deg{color:var(--gs-c)}
/* Bug corrigé (retour utilisateur, lisibilité) : .gs-nn (nom de note) n'avait pas de règle de couleur
   spécifique côté hex, contrairement au rect (cf. #page-play .gs-cell[style*="--gs-c"] .gs-nn plus bas dans
   ce fichier) — il retombait sur le gris terne par défaut (var(--tx3)) quel que soit le fond coloré de la
   cellule, illisible sur certains fonds sombres/saturés. Même formule que le rect : mélange à 75% avec la
   couleur d'accent de la cellule, nettement plus contrasté qu'un gris plat uniforme. */
#page-play .gs-hex-cell[style*="--gs-c"] .gs-nn{color:color-mix(in srgb,var(--gs-c) 75%,var(--tx3))}
#page-play .gs-hex-cell.gs-root[style*="--gs-c"]{
  --gs-hex-bg:color-mix(in srgb,var(--s3) 60%,var(--gs-c) 40%);
  --gs-hex-bw:2px;
}
#page-play .gs-hex-grid.gs-fill .gs-hex-cell[style*="--gs-c"]{
  --gs-hex-bg:color-mix(in srgb,var(--s3) 70%,var(--gs-c) 30%);
  --gs-hex-bc:color-mix(in srgb,var(--bd) 40%,var(--gs-c) 60%);
}
#page-play .gs-hex-grid.gs-fill .gs-hex-cell[style*="--gs-c"] .gs-deg{color:color-mix(in srgb,var(--tx) 45%,var(--gs-c) 55%)}
#page-play .gs-hex-grid.gs-fill .gs-hex-cell.gs-root[style*="--gs-c"]{
  --gs-hex-bg:color-mix(in srgb,var(--s3) 45%,var(--gs-c) 55%);
  --gs-hex-bc:var(--gs-c);--gs-hex-bw:2px;
}
/* Note JOUÉE : aplat franc + halo, identique au rect — seul moment où la couleur doit remplir le pad. */
#page-play .gs-hex-cell.gs-on[style*="--gs-c"]{
  --gs-hex-bg:var(--gs-c);--gs-hex-bc:var(--gs-c);--gs-hex-bw:3px;
}
#page-play .gs-hex-cell.gs-on[style*="--gs-c"] .gs-hex-svg{
  filter:drop-shadow(0 0 2px var(--gs-c)) drop-shadow(0 0 8px var(--gs-c));
}
#page-play .gs-hex-cell.gs-on[style*="--gs-c"] .gs-deg,
#page-play .gs-hex-cell.gs-on[style*="--gs-c"] .gs-nn{color:#111}
/* Texte plus grand que le réglage rect par défaut (retour utilisateur : "remplir plus d'espace") — un
   hexagone flat-top a une zone centrale plus généreuse que son rectangle englobant (le clip-path ne mord que
   les coins), les mêmes ratios cqh que .gs-cell paraissent donc petits une fois appliqués ici : plafond de
   clamp relevé pour occuper davantage la surface réellement disponible. */
#page-play .gs-hex-cell .gs-deg{font-size:clamp(.75rem,32cqh,2.6rem)}
#page-play .gs-hex-cell .gs-nn{font-size:clamp(.55rem,22cqh,1.5rem)}
/* PLEIN ÉCRAN sous le seuil desktop — même patron que #page-rhythm (--rh-top posé par _rhSyncTopOffset).
   Sans ça, les conteneurs intermédiaires (.pages-container/.main-content/.app-row) passent en display:block
   et s'ajustent à la HAUTEUR DU CONTENU : mesuré, la page ne faisait que 209px de haut, donc des cases de
   20px — injouables au doigt. Un instrument doit occuper tout l'écran, contrairement aux autres modules qui
   sont des documents à faire défiler.
   Seuil 1150px (DESKTOP_BREAKPOINT) et NON 600px : un mobile en PAYSAGE fait ~844px de large, il serait
   passé à travers un seuil à 600px alors que c'est justement le cas où la hauteur manque le plus (390px).
   --gs-top est posé par _gsSyncTopOffset() (app.js), qui applique la même borne. */
@media(max-width:1023px){
  #page-play.page.active{
    position:fixed;left:0;right:0;bottom:0;top:var(--gs-top,0px);
    z-index:5;overflow:hidden;
  }
}
/* Mobile : cases encore plus grandes (moins de marge perdue) et police de degré lisible à bout de bras.
   padding-left PAS touché par ce raccourci (cf. règle non-conditionnelle plus haut, marge de sécurité
   contre le geste système de retour au bord gauche) : on ne pose ici que haut/droite/bas. */
@media(max-width:600px){
  #page-play .gs-grid-wrap{padding:4px;padding-left:max(6px,env(safe-area-inset-left,6px))}
  #page-play .gs-grid{gap:4px}
  #page-play .gs-deg{font-size:.62rem}
  /* Tout doit tenir sur UNE seule ligne : chaque ligne de barre en plus est autant de hauteur de jeu en
     moins, et la hauteur est la ressource rare sur mobile. Mesuré à 390px : la barre passait à 69px (2
     lignes) une fois le bouton Accomp. rendu à sa vraie largeur — les sélecteurs sont donc resserrés et
     l'octave compactée pour tout ramener à ~37px.
     BUG CORRIGÉ (rapporté : "la subbar n'est pas scrollable horizontalement (elle l'est, difficilement),
     les derniers boutons ne sont pas accessibles, y compris en plein écran") — la compaction ci-dessus
     réduit le débordement mais ne le supprime plus totalement depuis l'ajout de boutons plus récents (ex.
     Aimant. en mode fretless) : overflow:hidden rendait alors les boutons de fin de barre DÉFINITIVEMENT
     inaccessibles (rien à faire glisser, juste rogné) plutôt que difficiles d'accès. overflow-x:auto permet
     enfin un vrai défilement tactile ; -webkit-overflow-scrolling:touch pour un défilement inertiel fluide
     sur iOS/Safari (comportement par défaut ailleurs). */
  #page-play .gs-bar{gap:3px;padding:3px;flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch}
  #page-play .gs-bar .sound-select{max-width:78px;font-size:max(10px,.5rem);padding:.2rem .15rem;min-width:0;flex:0 1 auto}
  #page-play .gs-oct-group{gap:2px}
  #page-play .gs-oct-group .tbtn{width:26px;height:26px;font-size:.75rem}
  #page-play .gs-accomp-btn{padding:0 6px;height:26px;font-size:max(10px,.52rem)}
}

/* ── Jouer : 2e rangée de contrôles (comportement de jeu + panneaux) ────────────────────────── */
/* padding-top rétabli à 4px (comme .subbar de base) + min-height alignée sur la BOÎTE réelle de la 1re barre
   (demande explicite : "toujours 2 subbar différentes (taille) sur desktop") — la 1re barre atteint 46px de
   haut (38px de contenu, tiré par ses gros boutons d'octave surdimensionnés 52×38px volontairement
   martelables, + 8px de padding haut/bas) ; la 2e n'avait que 4px de padding (padding-top:0) autour d'un
   contenu de 35px max, donc une boîte visiblement plus basse même à largeur de conteneur identique. */
#page-play .gs-bar2{border-top:none;min-height:46px}
/* #gsBarsWrap MASQUÉ EN PERMANENCE (demande explicite : "libérer complètement la subbar et la supprimer") —
   son dernier élément visible (gsFsBtn) a rejoint #gsOctRow (cf. index.html) ; tout ce qui reste ici est
   .gs-hud-superseded (les <select> d'origine Tonique/Gamme/Instrument/Polyphonie/Disposition, + les boutons
   Paramètres/Accords/Synthé, cf. leurs commentaires "DÉPLACÉ") — DOM nécessaire à _gsHudOpenPicker/
   gsRenderSelects, mais plus aucun contenu à montrer. Masquer le CONTENEUR entier plutôt que se fier à ce
   que chaque enfant masqué individuellement le réduise visuellement à rien : la barre gardait sinon son
   padding/background propres (cf. sa règle plus bas), un bandeau vide mais visible. */
/* #gsBarsWrap MASQUÉ À TOUTES LES LARGEURS (demande explicite : "libérer complètement la subbar et la
   supprimer") — cette règle unique remplace l'ancien couple base+@media(min-width:1024px) qui gérait la
   fusion visuelle des 2 anciennes rangées en une seule bande sur desktop (display:contents en cascade) :
   cette fusion n'a plus lieu d'être, #gsBarsWrap ne contient plus rien à fusionner (son dernier élément
   visible, gsFsBtn, a rejoint #gsOctRow, cf. index.html). Ce qui reste ici est .gs-hud-superseded (les
   <select> d'origine Tonique/Gamme/Instrument/Polyphonie/Disposition + les boutons Paramètres/Accords/
   Synthé, cf. leurs commentaires "DÉPLACÉ") — DOM nécessaire à _gsHudOpenPicker/gsRenderSelects, jamais à
   afficher. display:none à TOUTES les largeurs (pas seulement desktop) : mobile n'a plus, lui non plus,
   de raison de revenir aux 2 rangées empilées d'origine. */
#page-play #gsBarsWrap{display:none}
/* width:auto pour la même raison que .gs-accomp-btn : .tbtn impose une largeur FIXE de 20px qu'un padding
   ne peut pas contrer, et ces boutons portent des libellés ("Slide", "Synthé"...). */
#page-play .gs-mode-btn{width:auto;min-width:0;padding:0 9px;height:28px;font-size:max(10px,.58rem);white-space:nowrap}
/* Contrôles catégorie B repris par le HUD (Phase 3, refonte UI/UX SURFACE) — le <select> d'origine (classe
   .gs-hud-superseded) reste dans le DOM à sa place habituelle : _gsHudOpenPicker (cf. app.js) en a besoin
   pour retrouver `sel.nextElementSibling` (le bouton .dpk-select-btn généré par dpkAdaptSelect) et lui
   envoyer un pointerdown synthétique. dpkAdaptSelect passe déjà le <select> BRUT en display:none — seul son
   bouton compagnon reste visible par défaut ; on le masque ICI spécifiquement pour ces 5 contrôles, dont le
   HUD est désormais le seul point d'entrée visible (Tonique/Gamme/Timbre/Polyphonie/Disposition). */
#page-play .gs-hud-superseded+.dpk-select-btn{display:none!important}
/* gsSettingsBtn est un <button> natif (pas un <select> adapté) — porte directement la classe, masqué comme
   ci-dessus. Redondant avec le ⚙ du HUD (#gsHudSettingsBtn), qui ouvre le MÊME panneau (gsToggleSettingsPage
   appelle gsToggleSettingsPanel, cf. app.js). */
button.gs-hud-superseded{display:none!important}
#page-play .gs-mode-btn.on{background:var(--c-root);color:#111;border-color:var(--c-root)}
/* Verrou de gamme hexa, palier "block" (notes hors gamme grisées ET injouables) — pas de picto cartoon
   (règle du projet), la distinction avec le palier "dim" passe uniquement par la couleur/bordure du bouton
   lui-même, comme .on ci-dessus mais avec une teinte d'avertissement dédiée. */
#page-play .gs-mode-btn.blocked{background:var(--c-danger,#c0392b);color:#fff;border-color:var(--c-danger,#c0392b)}
#page-play .gs-bar-sep{width:1px;height:18px;background:var(--bd);margin:0 2px;flex-shrink:0}

/* ── HUD SUPPRIMÉ (demande explicite : "la barre au-dessus est superflue, crée une seule ligne de boutons qui
   restent visibles près de la surface de jeu, même en fullscreen") — l'élément #gsHud et toutes ses classes
   (.gs-hud, .gs-hud-seg, .gs-hud-key, .gs-hud-oct, .gs-hud-midi) ont été retirés : Tonique/Gamme/Instrument
   ont rejoint #gsOctRow (l'unique rangée restante), Disposition et Octave y étaient DÉJÀ présents en double
   (gsLayoutChip / #gsOctVal), et l'indicateur MIDI est devenu un vrai statut dans Paramètres/MIDI
   (cf. .gs-midi-status plus bas, et _gsSyncMidiStatus dans app.js). */
/* BUG CORRIGÉ (Phase 4, refonte UI/UX SURFACE) — #gsBarsWrap est TOUJOURS masqué en plein écran (règle plus
   bas, body.gs-fullscreen #page-play #gsBarsWrap{display:none}), et portait le SEUL accès à gsSettingsBtn :
   les Paramètres SURFACE étaient donc déjà INATTEIGNABLES en plein écran avant toute modification de cette
   refonte (gap préexistant, pas une régression introduite ici). Le HUD porte désormais le SEUL bouton ⚙
   visible (gsSettingsBtn lui-même masqué en permanence depuis la Phase 3, cf. .gs-hud-superseded) : le garder
   caché en plein écran aurait aggravé ce gap au lieu de le résoudre. Le HUD reste donc visible en plein
   écran, comme #gsOctRow (même exemption juste en dessous dans ce fichier) — c'est désormais LE seul HUD,
   pas une 2e barre en plus de l'ancienne, cf. la matrice de contrôles du plan : Musical (HUD) doit rester
   consultable/modifiable, y compris quand on joue en plein écran. */

/* ── Panneaux (synthèse / MIDI) ──────────────────────────────────────────────────────
   max-height + overflow-y : le panneau ne doit JAMAIS pousser la grille hors de l'écran. Sur mobile la
   hauteur est la ressource rare ; on plafonne le panneau et on le fait défiler lui-même plutôt que de
   réduire la surface de jeu au point de la rendre injouable. flex-shrink:0 pour qu'il garde sa hauteur
   annoncée dans la .page en colonne. */
#page-play .gs-panel,
#page-step .gs-panel{
  flex-shrink:0;max-height:26vh;overflow-y:auto;
  padding:6px 8px;background:var(--s2);border-bottom:1px solid var(--bd);
}
/* Accords/Synthé EMBARQUÉS dans Paramètres (demande explicite : "déplacer accords et synthé dans les
   paramètres") — #gsChordPanel/#gsParams restent des .gs-panel (aucun changement à leur rendu JS), mais la
   règle ci-dessus (max-height:26vh, pensée pour un dropdown compact par-dessus la grille) n'a plus lieu
   d'être : Paramètres est désormais un overlay PLEIN ÉCRAN qui scrolle déjà lui-même (cf. .gs-settings-page),
   un plafond de hauteur imbriqué ne ferait que créer un double défilement. border-bottom retiré aussi : la
   séparation entre sections est déjà assurée par .gs-settings-section+.gs-settings-section. */
#page-play .gs-settings-embed{max-height:none;overflow:visible;padding:0;background:transparent;border-bottom:none}
#page-play .gs-params-head,
#page-step .gs-params-head{display:flex;align-items:center;justify-content:space-between;gap:6px;flex-wrap:wrap;margin-bottom:5px}
#page-play .gs-params-title,
#page-step .gs-params-title{font-family:var(--fm);font-size:.62rem;font-weight:700;color:var(--tx);text-transform:uppercase;letter-spacing:.04em}
#page-play .gs-params-actions,
#page-step .gs-params-actions{display:flex;gap:4px;flex-wrap:wrap}
#page-play .gs-mini-btn,
#page-step .gs-mini-btn{
  font-family:var(--fm);font-size:max(10px,.52rem);padding:3px 7px;border-radius:5px;
  background:var(--s3);color:var(--tx2);border:1px solid var(--bd);cursor:pointer;white-space:nowrap;
}
#page-play .gs-mini-btn:hover,
#page-step .gs-mini-btn:hover{border-color:var(--c-root);color:var(--tx)}
#page-play .gs-mini-btn.on,
#page-step .gs-mini-btn.on{background:var(--c-root);color:#111;border-color:var(--c-root)}
/* ── Affichage STEP du métronome (cf. toggleMetroStep/_metroStepBuild dans app.js) ────────────────────
   Grille de comptage : 4 lignes (mesures) × N cellules (temps de la mesure). Le "1" de chaque ligne est
   blanc — c'est l'appui, le repère qu'un débutant doit apprendre à sentir ; les autres temps prennent la
   couleur d'accent et restent visuellement subordonnés.
   Aucune animation/transition sur la cellule active : elle change à chaque temps (jusqu'à plusieurs fois
   par seconde), une transition la ferait traîner sur le temps suivant et brouillerait justement le
   comptage — c'est le changement NET qui porte l'information ici. */
.metro-step-grid{display:flex;flex-direction:column;gap:9px;align-items:center;padding:14px 0 4px}
.metro-step-row{display:flex;gap:7px;align-items:center}
/* Respiration entre les 2 mesures d'une même ligne — sans elle, 8 cellules alignées se liraient comme une
   seule mesure de 8 temps, ce qui contredirait la numérotation 1..4 répétée. Un simple écart suffit :
   un trait vertical ajouterait du bruit visuel dans un affichage qu'on lit du coin de l'œil. */
.metro-step-sep{width:14px;flex:0 0 auto}
/* Cellules agrandies (demande explicite) — c'est un repère qu'on lit DU COIN DE L'ŒIL en jouant, souvent
   avec l'instrument en main et l'écran posé à distance : une cible trop petite oblige à fixer l'écran,
   ce qui va contre le but même de l'affichage. */
.metro-step-cell{
  width:62px;height:62px;border-radius:10px;
  display:flex;align-items:center;justify-content:center;
  font-family:var(--fm);font-size:1.15rem;font-weight:700;
  background:var(--s3);border:2px solid var(--bd);color:var(--tx3);
}
/* Temps 1 de chaque mesure : blanc, y compris éteint — le repère doit rester lisible AVANT que la tête de
   lecture n'y arrive, sinon il ne sert pas à anticiper. */
.metro-step-cell.metro-step-downbeat{border-color:var(--tx2);color:var(--tx)}
.metro-step-cell.on{background:var(--c-root);border-color:var(--c-root);color:#111}
.metro-step-cell.metro-step-downbeat.on{background:#fff;border-color:#fff;color:#111}
/* Temps 1 = cellule cliquable (choix de la mesure de départ au prochain démarrage à froid, cf.
   metroStepSetStartMeasure). cursor:pointer et un léger agrandissement au survol signalent l'affordance
   sans dépendre d'une icône — cohérent avec le reste du module (boutons texte, pas de pictogrammes). */
.metro-step-cell.metro-step-downbeat{cursor:pointer;transition:transform .08s}
.metro-step-cell.metro-step-downbeat:hover{transform:scale(1.06)}
/* Marqueur de la mesure choisie — pointillé en couleur d'accent, visible même quand la cellule n'est PAS
   active (.on), sinon le choix serait invisible tant que la tête de lecture n'a pas atteint cette mesure. */
.metro-step-cell.metro-step-startsel{box-shadow:0 0 0 2px var(--c-root)}
.metro-step-cell.metro-step-startsel.on{box-shadow:0 0 0 2px #fff}
@media(max-width:600px){
  /* 8 cellules par ligne (2 mesures de 4) doivent tenir en largeur sans défilement — une grille de
     comptage qu'il faut faire défiler ne remplit plus son rôle. Dimensionnées en vw plutôt qu'en px fixes
     pour rester aussi grandes que la largeur le permet, avec un plafond à la taille desktop.
     Budget : 8 cellules + 6 gaps + 1 séparateur + le padding de la carte. ~9.5vw par cellule laisse la
     marge nécessaire, y compris sur un écran de 360px. */
  .metro-step-cell{
    width:min(9.5vw,52px);height:min(9.5vw,52px);
    font-size:min(3.4vw,1rem);border-radius:7px;border-width:2px;
  }
  .metro-step-row{gap:min(1.1vw,7px)}
  .metro-step-grid{gap:min(1.6vw,9px)}
  .metro-step-sep{width:min(2.4vw,14px)}
}

/* Onglets Audio/MIDI du panneau d'enregistrement — réutilisent .gs-mini-btn (et son état .on ci-dessus)
   plutôt qu'un nouveau composant : même apparence que les autres boutons de panneau, rien à maintenir en
   plus. Les onglets et les commandes du volet actif occupent une seule rangée. */
#page-play #gsRecPanel{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:start;gap:8px}
#page-play #gsRecPanel>.gs-rec-tabs{display:flex;gap:4px;grid-column:1;grid-row:1}
#page-play #gsRecPanel>:is(#gsRecPaneAudio,#gsMidiRecPanel){grid-column:2;grid-row:1;min-width:0}
#page-play #gsRecPanel .gs-midi-bar{flex-wrap:nowrap;overflow-x:auto}
#page-play #gsRecPanel .gs-midi-bar>:not(audio){flex-shrink:0;white-space:nowrap}
/* Barre de lecture MIDI. touch-action:none — le glisser le long de la barre doit piloter la position, jamais
   déclencher le défilement de la page (même raison que .dpk-list-pop/.gs-chord-separator).
   Hauteur 8px + zone de clic élargie par le padding du conteneur : assez fine pour rester discrète, assez
   haute pour être visée au doigt sur mobile. */
#page-play .gs-midi-transport{display:flex;align-items:center;gap:8px;margin-top:6px}
#page-play .gs-midi-progress{
  flex:1;height:8px;border-radius:4px;background:var(--s3);border:1px solid var(--bd);
  cursor:pointer;touch-action:none;overflow:hidden;min-width:80px;
}
#page-play .gs-midi-progress-fill{
  height:100%;background:var(--c-root);border-radius:4px 0 0 4px;
  /* Pas de transition : la largeur est réécrite à chaque frame pendant la lecture (cf.
     _gsMidiProgressTick) — une transition CSS ferait courir l'animation APRÈS chaque écriture, donnant une
     jauge en retard et saccadée au lieu de suivre exactement la tête de lecture. */
}
/* Variante CONTOUR seule (demande explicite pour le verrou du séparateur accords : "juste contour quand le
   bouton est verrouillé, pas couleur pleine") — le fond reste neutre, seule la bordure (et le texte, pour
   rester lisible sur le fond neutre) prennent la couleur d'accent. Distincte de .on ci-dessus, qui remplit le
   bouton en pleine couleur — réservée aux réglages qu'on ne veut pas signaler trop fort visuellement. */
#page-play .gs-mini-btn.gs-mini-btn-contour-on{border-color:var(--c-root);color:var(--c-root)}
#page-play .gs-params-empty,
#page-step .gs-params-empty{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx3);line-height:1.5;padding:4px 0}
/* Grille auto-fit : 1 colonne sur téléphone étroit, jusqu'à 4-5 sur desktop, sans media query — le nombre
   de paramètres affichés varie déjà selon le timbre (cf. `show` de GS_PARAM_DEFS), un nombre de colonnes
   fixe laisserait des trous ou déborderait selon le preset. */
#page-step .gs-params-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(158px,1fr));gap:3px 10px}
/* SURFACE (#page-play) : potentiomètres ronds plutôt que sliders (demande explicite : "plus compacts") —
   items à largeur FIXE (52px, cf. .tb303-knob-wrap) posés en flex-wrap plutôt qu'en grille à colonnes
   élastiques pensée pour des lignes de slider pleine largeur ; l'espace de jeu prime, autant de potards que
   tiennent sur une ligne plutôt qu'un nombre de colonnes imposé. */
#page-play .gs-params-grid{display:flex;flex-wrap:wrap;gap:6px 10px;justify-content:flex-start}
#page-step .gs-param{display:grid;grid-template-columns:1fr auto;grid-template-areas:"lbl val" "sld sld";align-items:center;gap:1px 6px}
#page-play .gs-param-lbl,
#page-step .gs-param-lbl{grid-area:lbl;font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#page-play .gs-param-val,
#page-step .gs-param-val{grid-area:val;font-family:var(--fm);font-size:max(10px,.5rem);color:var(--c-root);font-weight:700;white-space:nowrap}
/* Curseurs surdimensionnés au doigt : 22px de haut de zone tactile pour une piste de 3px. Sur un panneau
   dense, un curseur natif de 4px est impossible à saisir au pouce. */
#page-play .gs-param input[type=range],
#page-step .gs-param input[type=range]{
  grid-area:sld;width:100%;height:22px;margin:0;-webkit-appearance:none;appearance:none;background:transparent;
}
#page-play .gs-param input[type=range]::-webkit-slider-runnable-track,
#page-step .gs-param input[type=range]::-webkit-slider-runnable-track{height:3px;background:var(--bd);border-radius:2px}
#page-play .gs-param input[type=range]::-moz-range-track,
#page-step .gs-param input[type=range]::-moz-range-track{height:3px;background:var(--bd);border-radius:2px}
#page-play .gs-param input[type=range]::-webkit-slider-thumb,
#page-step .gs-param input[type=range]::-webkit-slider-thumb{
  -webkit-appearance:none;appearance:none;width:14px;height:14px;border-radius:50%;
  background:var(--c-root);border:none;margin-top:-5.5px;cursor:pointer;
}
#page-play .gs-param input[type=range]::-moz-range-thumb,
#page-step .gs-param input[type=range]::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:var(--c-root);border:none;cursor:pointer}
#page-play .gs-sel,
#page-step .gs-sel{
  grid-area:sld;width:100%;font-family:var(--fm);font-size:max(10px,.55rem);padding:3px 4px;
  background:var(--s3);color:var(--tx);border:1px solid var(--bd);border-radius:5px;
}

/* ── Effets visuels ──────────────────────────────────────────────────────────────────
   Onde d'attaque : uniquement transform + opacity, les 2 seules propriétés que le compositeur GPU anime sans
   recalcul de layout. Toute autre (width/height/top) forcerait un reflow à chaque frame, et une frame perdue
   s'entend quand l'audio tourne en parallèle. */
#page-play .gs-cell{position:relative}
/* .gs-ripple (onde posée SUR LA CASE) RETIRÉE (demande explicite, après capture annotée : "le même ripple
   que dans les pads, mais qui suive la trajectoire du doigt") — remplacée par .gs-finger-ripple, qui reprend
   ce même style visuel (contour+dégradé+box-shadow, cf. son commentaire) mais suit la position RÉELLE du
   doigt en continu au lieu d'être ancrée à une case. _gsFxBurst (app.js) est conservé en no-op à ses
   nombreux points d'appel plutôt que supprimé partout. Le FLASH de la case (.gs-cell.gs-on, couche
   indépendante juste en dessous) reste inchangé : c'est l'indicateur fonctionnel de "quelle note sonne". */
/* FLASH de la case au moment de l'attaque (cf. l'historique de ce bloc) — c'est ce qui fait "s'allumer"
   le pad plutôt que de simplement poser un halo par-dessus. filter est composé par le GPU comme
   transform/opacity, donc aucun reflow. Posé sur .gs-cell.gs-on (état déjà géré par le moteur de jeu,
   AUCUNE nouvelle classe à poser/retirer en JS : zéro coût supplémentaire par note) et animé une seule fois
   à l'apparition de la classe. La lueur portée (box-shadow) est nettement renforcée par rapport à
   l'ancienne valeur (0 0 16px -2px) : elle déborde franchement sur les cases voisines, ce qui est
   exactement l'effet "matière/réaction" recherché. */
/* --gs-flash-dur (demande explicite : "j'aimerais que ce paramètre soit configurable dans les paramètres
   d'effet [durée du fade]") — variable CSS posée sur #page-play (cf. gsSetFxFlashMs dans app.js), .45s en
   dur remplacé pour rester réglable comme les autres paramètres d'effet (opacité, traînée, ripples max).
   Valeur par défaut identique (--gs-flash-dur:.45s ci-dessous, redéfinie dynamiquement dès qu'un réglage
   utilisateur diffère de la valeur d'origine, cf. gsSetFxFlashMs). */
/* BUG CORRIGÉ (rapporté : "j'aimerais que le pad qu'on vient de toucher continue à fader" — quand le doigt
   se relève avant la fin du fondu) : l'animation vivait sur .gs-cell.gs-on lui-même, donc retirer .gs-on au
   relâchement (comportement voulu — le pad doit reprendre sa couleur normale) coupait aussi le fondu en
   cours, puisqu'une classe CSS retirée arrête net toute animation qu'elle porte. Déplacée sur .gs-flashing,
   une classe SÉPARÉE posée/retirée par son propre minuteur JS (_gsFlashCell, app.js), indépendant de la
   durée réelle de la note — le pad continue de s'éclaircir puis de s'éteindre même après relâchement. */
#page-play{--gs-flash-dur:.45s}
/* --gs-flash-intensity (demande explicite, après le rapport "certaines cases deviennent complètement
   blanches pendant un instant" avec un fondu réglé à 1000ms) — la surbrillance de pointe (brightness(2.2))
   restait FIXE quelle que soit --gs-flash-dur : un fondu long (glisser rapide → cases qui se rallument
   fréquemment) laisse cette pointe très éclaircie bien plus longtemps visible à l'œil, d'où l'impression de
   cases "blanches". Ce réglage sépare enfin les deux : la DURÉE du fondu (--gs-flash-dur, déjà réglable)
   d'un côté, l'INTENSITÉ de la pointe (--gs-flash-intensity, ce réglage) de l'autre — descendre l'intensité
   permet de garder un fondu long et lisible sans blanchiment. 1 = valeur d'origine (brightness 2.2/saturate
   1.8/contrast 1.15 à 0%, cf. gs-cell-flash ci-dessous, INCHANGÉS analytiquement — 1 est un multiplicateur
   neutre appliqué aux ÉCARTS par rapport à 1 : brightness "1 + (2.2-1)*intensity", jamais une interpolation
   linéaire naïve de brightness elle-même, qui donnerait une valeur ABSURDE (négative) en dessous de 0.45).
   Posée sur #page-play (cf. gsSetFxFlashIntensity dans app.js), même patron que --gs-flash-dur juste
   au-dessus. */
/* --gs-flash-b/-s/-c (0%) et -b2/-s2/-c2 (35%) : valeurs DÉJÀ résolues par gsSetFxFlashIntensity (app.js),
   pas calc() ici — l'intensité n'a de sens que multipliée sur l'ÉCART à 1 (brightness "1+(2.2-1)*intensité"),
   un calcul que calc() pourrait exprimer mais que JS fait une seule fois par changement de réglage plutôt
   qu'à chaque frame d'animation. Valeurs ci-dessous = intensité 1 (défaut, comportement d'origine). */
#page-play{--gs-flash-b:2.2;--gs-flash-s:1.8;--gs-flash-c:1.15;--gs-flash-b2:1.5;--gs-flash-s2:1.4;--gs-flash-c2:1.08}
#page-play .gs-cell.gs-flashing{
  animation:gs-cell-flash var(--gs-flash-dur) cubic-bezier(.2,.7,.3,1);
  z-index:2; /* la lueur doit passer PAR-DESSUS les cases voisines, pas dessous */
}
@keyframes gs-cell-flash{
  0%{filter:brightness(var(--gs-flash-b)) saturate(var(--gs-flash-s)) contrast(var(--gs-flash-c))}
  35%{filter:brightness(var(--gs-flash-b2)) saturate(var(--gs-flash-s2)) contrast(var(--gs-flash-c2))}
  100%{filter:none}
}
/* Effets visuels DÉSACTIVÉS (gsEffectsLevel=0, cf. gsSetEffectsLevel dans app.js) — la garde JS de
   _gsFxBurst suffit pour les ondes .gs-ripple (aucun élément créé dès le palier 1), mais PAS pour le flash
   ci-dessus, qui est du CSS pur accroché à .gs-on (classe posée par le moteur de jeu quoi qu'il arrive). On
   le neutralise donc ici. L'anneau/lueur de .gs-on est CONSERVÉ même désactivé : ce n'est pas de l'habillage
   mais l'indicateur fonctionnel de "quelle note sonne", nécessaire pour jouer (cf. le commentaire de
   .gs-cell.gs-on juste en dessous). */
#page-play.gs-fx-off .gs-cell.gs-flashing{animation:none} /* visait .gs-on, devenu inopérant depuis le déplacement de l'animation sur .gs-flashing */
/* BUG "flashs noirs au glissé rapide" (Mi 11 / OnePlus Pad, jamais sur desktop) : la transition de transform
   (.06s) du rétrécissement de .gs-on promouvait CHAQUE case touchée en calque GPU le temps de l'animation
   (puis la rétrogradait). Le rétrécissement reste animé, mais via --gs-pk (@property, cf. #page-play .gs-cell).
   Diagnostic sur appareil : ?gsdbg=noscale,noshadow,noanim,notrans,nocq,nobg (cf. _gsSyncEffectsLevelClasses)
   neutralise chaque suspect séparément pour isoler la cause sans outil de débogage distant. */
#page-play.gsdbg-noscale .gs-cell,#page-play.gsdbg-noscale .gs-hex-svg{transform:none!important}
#page-play.gsdbg-noshadow .gs-cell,#page-play.gsdbg-noshadow .gs-cell::after{box-shadow:none!important}
#page-play.gsdbg-noanim .gs-cell{animation:none!important}
#page-play.gsdbg-notrans .gs-cell{transition:none!important}
#page-play.gsdbg-nocq .gs-cell{container-type:normal!important}
#page-play.gsdbg-nobg::before{display:none!important}
#page-play.gsdbg-nobg{background:var(--bg)!important}
/* Effets RÉDUITS (gsEffectsLevel=1, demande explicite : "plusieurs niveaux de graphismes pour équilibrer
   effets visuels/performances CPU/GPU") — palier intermédiaire : garde le flash (animation ci-dessus,
   filter uniquement, aucun élément DOM créé) mais retire le HALO renforcé de .gs-cell.gs-on (box-shadow à 2
   couches + color-mix, cf. plus bas) — c'est la couche la plus coûteuse à composer en grand nombre
   (plusieurs cases actives à la fois, accords/mode corde), remplacée ici par le halo SIMPLE d'origine
   (1 couche, sans color-mix). L'onde .gs-ripple, elle, ne se crée déjà plus dès ce palier (cf. _gsFxBurst,
   gsEffectsLevel<2). */
#page-play.gs-fx-reduced .gs-cell.gs-on{box-shadow:0 0 0 2px var(--gs-c,var(--c-root))}
/* Halo "sous le doigt" (demande explicite : "un effet qui suit sous le doigt et pas juste sur le pad" — cf.
   _gsFingerGlowStart/Move/End dans app.js pour la logique JS). Créé en document.body (PAS descendant de
   #page-play, cf. _gsFingerGlowStart qui fait document.body.appendChild — même convention déjà établie que
   .gs-picker-pop, cf. sa règle plus bas, jamais scopée #page-play pour la même raison) : sélecteur SANS
   préfixe #page-play, contrairement à tout le reste des effets de ce fichier. position:fixed +
   transform:translate posé en style inline (jamais left/top, qui forceraient un reflow à chaque
   pointermove — même contrainte GPU-only que le reste des effets de ce module) : indépendant de la grille
   (rect ou hex), toujours au-dessus de tout (z-index élevé), jamais gêné par l'overflow:hidden d'une case
   (contrairement à .gs-ripple, qui vit LUI à l'intérieur d'une cellule et en hérite le clipping, cf. le
   correctif overflow sur .gs-hex-cell). pointer-events:none impératif : cet élément est positionné PILE
   sous le doigt, il intercepterait sinon le pointeur lui-même et casserait tout le jeu. */
.gs-finger-glow{
  position:fixed;top:0;left:0;z-index:400;width:64px;height:64px;margin:-32px;
  border-radius:50%;pointer-events:none;
  /* --gs-c posée en inline par _gsFingerGlowStart/Move (app.js), d'après la couleur RÉELLE de la case sous
     le doigt à cet instant (demande explicite : "donner au halo sous le doigt la couleur du pad touché
     également") — repli sur --c-root (accent général) hors de toute case ou en mode Sobre sans code
     couleur actif, même logique que .gs-finger-ripple/.gs-cell.gs-on. */
  background:radial-gradient(circle,color-mix(in srgb,var(--gs-c,var(--c-root)) 55%,transparent) 0%,transparent 72%);
  opacity:0;transition:opacity .12s ease,background-color .08s linear;
  /* translate/scale (pas transform) : la position est posée via la propriété `translate` depuis le
     2026-10-04 — un will-change:transform ne garantit plus la couche GPU dédiée pour ces propriétés. */
  will-change:translate,scale,opacity;
}
.gs-finger-glow.on{opacity:1}
/* Geste EXP (--gs-ex 0-1, --gs-mute 0-1, cf. _gsFingerGlowExpr) : `scale` est une propriété distincte de
   `transform`, qui porte déjà la position inline — les deux se composent sans se remplacer. Sans EXP (vars
   absentes), le halo garde exactement sa taille et son opacité historiques. */
.gs-finger-glow.on{
  opacity:calc(.55 + .45*var(--gs-ex,1));
  scale:calc((.7 + .6*var(--gs-ex,.5)) * (1 - .45*var(--gs-mute,0)));
  transition:opacity .12s ease,background-color .08s linear,scale .06s linear;
}
/* Coupé aux 2 paliers non-Complets ET en reduced-motion — cf. la garde JS équivalente dans
   _gsFingerGlowStart (gsEffectsLevel<2 empêche déjà la CRÉATION de l'élément), cette règle CSS est un 2e
   filet purement défensif si jamais un élément existait encore au moment d'un changement de palier
   (cf. gsSetEffectsLevel, qui les retire déjà explicitement — ceinture et bretelles, coût nul). ~ (frère
   GÉNÉRAL, pas +) plutôt qu'un sélecteur descendant : .gs-fx-off/.gs-fx-reduced sont posées sur #page-play
   (cf. _gsSyncEffectsLevelClasses, app.js), alors que .gs-finger-glow est un FRÈRE de #page-play dans
   document.body (ajouté en fin de <body> par _gsFingerGlowStart), jamais son descendant — un sélecteur
   descendant classique (espace) n'aurait donc jamais matché. */
#page-play.gs-fx-off~.gs-finger-glow,#page-play.gs-fx-reduced~.gs-finger-glow:not(.gs-finger-ring){display:none}
#page-play.gs-rm~.gs-finger-glow:not(.gs-finger-ring){display:none}
/* Cercle (mode par défaut, cf. gsFxFingerMode) : contour net + liseré sombre pour rester lisible sur toutes
   les couleurs de case ; suit l'EXP par sa taille (scale), pas par son opacité. */
.gs-finger-glow.gs-finger-ring{width:58px;height:58px;margin:-29px;box-sizing:border-box;
  background:color-mix(in srgb,var(--gs-c,var(--c-root)) 14%,transparent);
  border:2.5px solid color-mix(in srgb,var(--gs-c,var(--c-root)) 75%,#fff);
  box-shadow:0 0 0 1px rgba(0,0,0,.45),inset 0 0 0 1px rgba(0,0,0,.3)}
.gs-finger-glow.gs-finger-ring.on{opacity:.95}
@media (pointer:fine){.gs-finger-glow.gs-finger-ring{width:40px;height:40px;margin:-20px}}
/* RIPPLE PONCTUEL ORIENTÉ, positionné sur le doigt (6e itération — les 5 précédentes sont documentées dans
   app.js, cf. le commentaire au-dessus de _gsFingerGlowStart, pour ne pas les refaire). Demande explicite
   après identification précise sur la version en ligne : "je parlais de la DOUBLE LIGNE qui s'affichait" —
   c'est .gs-ripple-slide (cf. plus haut dans ce fichier, partagée ici à l'identique) : border-width:3px 0
   ne borde QUE le haut et le bas d'un ovale très aplati, ce qui se lit comme 2 arcs fins qui s'écartent du
   point de contact pendant l'animation, pas comme un anneau plein.
   ENFANT DE LA CASE (demande explicite suivante : "ne jamais sortir de la surface des pads") — pas
   position:fixed sur <body> comme le 1er jet (mesuré : débordait librement sur tout l'écran, par-dessus les
   cases voisines). _gsFingerRippleSpawn (app.js) trouve la case sous le doigt et y insère ce span en
   position:absolute avec des coordonnées RELATIVES à cette case (--gs-fr-x/-y en px depuis son coin
   haut-gauche) : .gs-cell/.gs-hex-cell portent déjà position:relative+overflow:hidden (cf. leurs règles),
   donc le clipping à la surface du pad est automatique, sans rien ajouter ici. Le sélecteur de coupure aux
   paliers réduits change en conséquence : ~ (frère de #page-play) ne matchait que pour un élément vivant
   dans <body> comme l'ancien design — désormais descendant normal de #page-play, un simple espace suffit.
   Couleur : var(--gs-c) posée en inline par _gsFingerRippleSpawn d'après la couleur RÉELLE de la case sous
   le doigt (demande explicite : "l'effet doit avoir la couleur du pad auquel il est superposé"), repli sur
   --c-root si la case n'a pas de code couleur actif (mode Sobre) — même logique que .gs-cell.gs-on. */
.gs-finger-ripple{
  position:absolute;top:0;left:0;z-index:5;width:150px;height:150px;margin:-75px;
  border-radius:40%;border:3px solid var(--gs-c,var(--c-root));border-width:3px 0;
  pointer-events:none;opacity:1;
  /* Durée RÉGLABLE (demande explicite : "fade-out time, durée pendant laquelle l'effet est visible avant de
     s'estomper progressivement") — animation-duration posée en inline par _gsFingerRippleSpawn (app.js)
     d'après gsFxFadeMs, PAS un simple ".34s" figé. Le plateau à pleine opacité (0-45%, cf. les keyframes)
     et le fondu (45-100%) s'étirent tous les 2 PROPORTIONNELLEMENT avec la durée totale : un ripple plus
     lent reste visible plus longtemps ET s'efface plus lentement, cohérent avec "l'effet est visible avant
     de s'estomper" plutôt que 2 réglages séparés à synchroniser. --gs-fr-dur, PAS animation-duration
     inline directement : cette propriété n'accepte pas var() avec fallback en syntaxe raccourcie fiable
     sur tous les navigateurs ciblés, donc postée dans la propriété longhand dédiée ci-dessous. */
  animation-name:gs-finger-ripple-slide;
  animation-duration:var(--gs-fr-dur,.34s);
  animation-timing-function:linear;
  animation-fill-mode:forwards;
}
/* Opacité RELEVÉE (demande explicite : "augmenter un peu l'opacité de l'effet", après comparaison directe
   avec la version déjà déployée) — pleine opacité (1, contre .9) au départ.
   --gs-fr-opacity RÉGLABLE (demande explicite suivante : "ajoute un paramètre... pour régler l'opacité, le
   maximum étant l'état actuel") — facteur multiplicatif 0-1 posé en inline par _gsFingerRippleSpawn d'après
   gsFxOpacity (100 = plateau à 1 inchangé, valeur plus basse l'atténue proportionnellement à CHAQUE étape
   de l'animation). Défaut à 1 (repli) si jamais l'inline manquait.
   FONDU PLUS GRADUEL (demande explicite suivante : "fondu des effets : plus graduel, fondu plus lent") —
   2 changements par rapport au 1er jet (plateau à 45% puis chute directe à 0, avec un easing global
   cubic-bezier appliqué à TOUTE l'animation y compris l'opacité) :
   1. animation-timing-function passée en LINEAR sur l'élément (ci-dessus) — la courbe cubic-bezier(.3,.6,
      .2,1) posée avant s'appliquait à l'opacité ET au scale ensemble (animation-timing-function est un
      réglage GLOBAL à l'animation, pas par propriété), ce qui accélérait le fondu final plutôt que de
      l'étaler. La courbe voulue pour le SCALE (accélération franche en fin de course, cf. le paramètre de
      Bézier d'origine) est maintenant portée par une easing-function PAR ÉTAPE sur transform uniquement
      (chaque étape du keyframe déclare la sienne, cf. ci-dessous), pendant que opacity suit ses propres
      points de contrôle sans dépendre de celle du scale.
   2. Plateau réduit à 20% (au lieu de 45%) ET un point INTERMÉDIAIRE à 60% (opacité encore aux 2/3 de son
      maximum) avant de rejoindre 0% à 100% — au lieu d'un fondu linéaire 45→100% qui, une fois perçu, donne
      une impression de disparition assez nette sur la 2e moitié, cette courbe en 2 segments s'estompe
      D'ABORD lentement (20-60%) puis achève sa disparition (60-100%) : la baisse de perception lumineuse
      n'est PAS linéaire à l'œil (la loi de Weber-Fechner rend une chute de 100%→60% moins visible qu'une
      chute de 60%→0% à valeurs égales), ce point intermédiaire compense cet effet perceptif et donne une
      sensation de fondu bien plus progressive à durée totale INCHANGÉE. */
/* --gs-fr-scale (demande explicite : "largeur du ripple : adapter à la taille des cellules") — facteur
   posé en inline par _gsFingerRippleSpawn (app.js) d'après la taille RÉELLE de la case sous le doigt, défaut
   à 1 (repli = comportement d'origine, taille de référence 150px) si jamais l'inline manquait. Multiplié
   APRÈS les scaleX/scaleY existants (qui restent la forme/l'anamorphose du ripple à taille de référence) —
   un simple facteur d'échelle global supplémentaire, pas une réécriture des valeurs déjà réglées visuellement. */
/* BUG "flashs noirs au glissé rapide" (Mi 11 / OnePlus Pad, palier Complets) : animer transform/opacity
   directement faisait de CHAQUE ripple un calque GPU (raison ActiveTransformAnimation) créé puis détruit à
   chaque pas de traînée — sur un budget de tuiles mobile, cette valse d'allocations évinçait des tuiles de
   la grille, affichées en noir. Mesuré (side_dev/dev/gs_blackflash.js, budget GPU réduit) : réduire le
   plafond de ripples vivants n'y change rien, c'est la création/destruction de calques qui coûte. Les
   keyframes animent donc 2 propriétés personnalisées ENREGISTRÉES (@property, sinon interpolation
   discrète) que transform/opacity lisent : animation non composée, le ripple est peint dans le calque de
   la case, aucun calque créé. --gs-fr-s porte la courbe du scale (cubic-bezier de 0 à 100%), --gs-fr-o la
   courbe d'opacité (plateau 0-20%, point à 60%) — mêmes courbes qu'avant, découplées comme avant. */
@property --gs-fr-s{syntax:'<number>';inherits:false;initial-value:0}
@property --gs-fr-o{syntax:'<number>';inherits:false;initial-value:1}
.gs-finger-ripple{
  transform:translate(var(--gs-fr-x,0),var(--gs-fr-y,0)) rotate(var(--gs-fr-a,0deg)) scale(var(--gs-fr-scale,1)) scaleX(calc(.3 + 1.8*var(--gs-fr-s))) scaleY(calc(.6 - .25*var(--gs-fr-s)));
  opacity:calc(var(--gs-fr-opacity,1)*var(--gs-fr-o));
}
@keyframes gs-finger-ripple-slide{
  0%{--gs-fr-s:0;--gs-fr-o:1;animation-timing-function:cubic-bezier(.3,.6,.2,1)}
  20%{--gs-fr-o:1}
  60%{--gs-fr-o:.45}
  100%{--gs-fr-s:1;--gs-fr-o:0}
}
#page-play.gs-fx-off .gs-finger-ripple,#page-play.gs-fx-reduced .gs-finger-ripple,#page-play.gs-rm .gs-finger-ripple{display:none}
/* Doigt POSÉ mais qui ne sonne pas (mode corde) : il reste « derrière » un doigt plus récent. Sans un état
   visible distinct, on perdrait de vue quels doigts sont encore en place — or c'est toute la mécanique du
   pull-off (lever le doigt du dessus fait re-sonner celui-là). Contour seul, pas de remplissage : « prêt à
   sonner », visuellement en retrait de la note qui sonne vraiment. */
#page-play .gs-cell.gs-held{
  border-color:var(--c-root);border-width:2px;
  background:color-mix(in srgb,var(--s3) 82%,var(--c-root) 18%);
}
#page-play .gs-cell.gs-held .gs-deg{color:var(--tx)}
/* Lueur de la case tenue : indique clairement QUELLE note sonne, y compris quand le doigt la masque — la
   lueur déborde de la case, donc reste visible autour du doigt. */
/* Lueur RENFORCÉE (demande explicite : "retour visuel beaucoup plus marqué, effets de couleur ou de
   contraste") — reprend la couleur PROPRE à la case (--gs-c, posée par gsRender selon le code couleur actif)
   plutôt que l'accent global : la case qui sonne s'allume de SA couleur, ce qui est à la fois plus joli et
   plus informatif (on lit la note/l'intervalle à la lueur elle-même). Repli sur --c-root quand aucun code
   couleur n'est actif (mode Sobre, cf. GS_COLOR_CHOICES). Anneau porté à 3px et halo à 26px : la lueur
   déborde franchement, visible même quand le doigt masque la case. */
#page-play .gs-cell.gs-on{
  box-shadow:0 0 0 3px var(--gs-c,var(--c-root)),
             0 0 26px 2px color-mix(in srgb,var(--gs-c,var(--c-root)) 75%,transparent);
}
/* Note LATCHÉE (appui long, cf. _gsArmNoteLatch dans app.js) : sonne SANS qu'un doigt la tienne. Contour
   pointillé — délibérément distinct de .gs-on (case sous le doigt, trait plein) et de .gs-held (doigt posé
   qui ne sonne pas, mode corde) : les trois états coexistent et doivent rester discernables d'un coup d'œil.
   Pas d'animation : ces cases peuvent être nombreuses et rester affichées longtemps (cf. la règle "pas
   d'animation par frame sur les cellules" — coût GPU permanent sur mobile pour un état purement informatif). */
#page-play .gs-cell.gs-latched{
  outline:2px dashed var(--c-root);outline-offset:-3px;
  box-shadow:0 0 12px -3px var(--c-root);
}
/* Case « écho » (demande explicite : montrer où la MÊME hauteur se retrouve ailleurs sur la grille) —
   contour plein et épais (demande explicite : le pointillé initial manquait de lisibilité) mais toujours
   sans fond ni lueur, pour ne pas se confondre avec .gs-on (la case RÉELLEMENT tenue) ni .gs-held (mécanique
   du pull-off en mode corde), avec lesquels elle peut coexister. */
/* Rendu BLANC + pulsation (demande explicite : "bien plus visible pour tous les modes de couleur") — un
   contour dans --c-root (1re version) se noyait dans les cases dont la couleur de note/intervalle est déjà
   proche de l'accent du thème (ex. un thème orange sur une case colorée orange). Le blanc ne se confond
   AVEC AUCUNE couleur de case possible, quel que soit gsColorMode ; l'anneau extérieur + halo débordant
   (comme .gs-on) le rend repérable même en bord de grille, où un simple contour interne serait moins visible.
   !important sur border/box-shadow : bug corrigé au passage (rapporté "l'effet n'existe plus") — en mode
   couleur note/intervalle, chaque case porte un style inline --gs-c consommé par la règle
   #page-play .gs-cell[style*="--gs-c"] (plus bas dans ce fichier), qui repose SA PROPRE border-color à
   chaque rendu ; même spécificité (1 classe + 1 attribut = 1 classe + 1 classe), donc c'est l'ORDRE dans le
   fichier qui tranchait en faveur de cette règle-attribut, postérieure dans la cascade. */
/* Contour encore épaissi (demande explicite, 2e passe) : 3px -> 5px, halo renforcé en conséquence pour
   rester proportionné à ce contour plus large. */
#page-play .gs-cell.gs-echo{
  border-style:solid;border-color:#fff !important;border-width:5px;
  box-shadow:0 0 0 1px #fff,0 0 12px 2px rgba(255,255,255,.75) !important;
  animation:gsEchoPulse 1.3s ease-in-out infinite;
}
@keyframes gsEchoPulse{
  0%,100%{box-shadow:0 0 0 1px #fff,0 0 10px 2px rgba(255,255,255,.6)}
  50%{box-shadow:0 0 0 1px #fff,0 0 18px 4px rgba(255,255,255,.9)}
}
@media(prefers-reduced-motion:reduce){
  #page-play .gs-cell.gs-echo{animation:none}
}
/* Glow du mode fretless (demande explicite) : la case d'ATTAQUE reste .gs-on tout du long (repère fixe,
   cf. _gsFretlessTrack dans app.js), tandis que cette classe suit la case SURVOLÉE en continu pendant le
   glisser — halo doux et large plutôt que le contour net de .gs-on/.gs-echo, pour lire comme "le doigt est
   ICI maintenant" et non comme une frappe supplémentaire. animation discrète (pulse) pour rester visible
   même quand la couleur de fond de la case est proche de celle du halo. */
/* Rendu plus vif (demande explicite) : anneau BLANC net en plus du halo coloré — un halo seul, même intense,
   reste discret sur un fond déjà teinté proche de --c-root ; le blanc, lui, ne se confond avec aucune
   couleur de case. Halo élargi et bien plus opaque (95% contre 70% avant) aux 2 bornes de l'animation. */
#page-play .gs-cell.gs-fretless-glow{
  box-shadow:0 0 0 3px #fff,0 0 32px 10px color-mix(in srgb,var(--c-root) 95%,transparent);
  animation:gsFretlessPulse 0.85s ease-in-out infinite;
}
@keyframes gsFretlessPulse{
  0%,100%{box-shadow:0 0 0 2px #fff,0 0 24px 6px color-mix(in srgb,var(--c-root) 85%,transparent)}
  50%{box-shadow:0 0 0 4px #fff,0 0 40px 14px color-mix(in srgb,var(--c-root) 100%,transparent)}
}
/* Halo très discret sur les toniques : repère visuel permanent, sans concurrencer l'état "note tenue". */
#page-play .gs-cell.gs-root::after{
  content:'';position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  box-shadow:inset 0 0 12px -4px var(--c-root);
}
/* Case en zone ACCORDS (cf. gsChordZone/_gsIsChordRow dans app.js) : aplat franc de la couleur de la
   RACINE — traité à part de .gs-fill (qui régit le mode note "plein") car une case-accord doit TOUJOURS se
   distinguer visuellement d'une case-note, quel que soit le code couleur choisi par ailleurs pour les notes
   (recherche UX : Maschine/Launchpad ne laissent jamais un pad d'accord se confondre avec un pad de note).
   Bordure plus épaisse : ce pad porte plus d'information (un accord entier) qu'une case-note simple. */
#page-play .gs-cell.gs-chord-cell{
  background:color-mix(in srgb,var(--s3) 55%,var(--gs-c) 45%);
  border-color:var(--gs-c);border-width:2px;
}
#page-play .gs-cell.gs-chord-cell .gs-deg{color:color-mix(in srgb,var(--tx) 30%,var(--gs-c) 70%);font-weight:700}
#page-play .gs-cell.gs-chord-cell .gs-nn{color:var(--tx3);font-size:.65em}
#page-play .gs-cell.gs-chord-cell.gs-on{background:var(--gs-c)}
#page-play .gs-cell.gs-chord-cell.gs-on .gs-deg{color:#111}
/* Badge d'octave (demande explicite, choix confirmé : petit chiffre en coin plutôt qu'un indicateur
   non-verbal) — cf. recherche UX : aucune app référencée n'affiche racine+qualité+octave en clair sur un
   pad, l'octave se lit toujours à part. Coin plutôt que ligne dédiée : ne mange aucun espace du libellé
   principal (racine+qualité), déjà serré sur un petit pad.
   Étendu à TOUTE case (pas seulement .gs-chord-cell, demande explicite : "ajouter l'octave dans le pad...
   pour toutes les notes") et AGRANDI (cf. clamp cqh, même patron que .gs-deg/.gs-nn) — l'ancien 0.5rem fixe
   restait minuscule y compris sur de grands pads, alors que .gs-deg/.gs-nn grandissent déjà avec la case. */
/* Position remise en haut-à-droite du pad (demande explicite : "remettre l'indicateur d'octave en haut à
   droite des pads en mode rectangle, sans changer son apparence actuelle" — un essai précédent l'avait aligné
   sur le centrage vertical du mode hexagonal, mais SEULE la position doit revenir en arrière ici, PAS
   l'apparence : couleur blanche + contour sombre (double text-shadow) conservés tels quels, toujours sans
   pastille/fond plein, contrairement à l'ancien gris uni d'avant ce même changement). */
#page-play .gs-cell .gs-chord-oct-badge{
  position:absolute;top:2px;right:3px;
  font-family:var(--fm);font-size:clamp(.5rem,16cqh,1rem);font-weight:700;
  color:#fff;line-height:1;pointer-events:none;
  text-shadow:0 0 2px rgba(0,0,0,.9),0 0 2px rgba(0,0,0,.9),0 1px 1px rgba(0,0,0,.9);
}
/* Bug corrigé (retour utilisateur : badge affiché HORS du pad) : top:2px;right:3px positionne par rapport au
   RECTANGLE ENGLOBANT de la cellule — pour .gs-cell (rect) le pad occupe déjà tout ce rectangle, donc ça
   tombe naturellement dedans, mais un hexagone flat-top est INSCRIT dans un rectangle bien plus large que sa
   silhouette réelle (le clip-path/SVG ne dessine que 25%→75% de large sur le bord du haut) : le coin
   supérieur droit du rectangle englobant tombe hors de la forme visible. Décalage revu pour ce cas précis :
   top plus grand (pousse vers le bas, dans la partie déjà large de l'hexagone) et right nettement plus grand
   (reste sous le bord obliques 75%→100%, jamais dans le triangle coupé). */
/* Repositionné au milieu vertical du pad (demande explicite) — à cette hauteur (50%, le point le plus large
   de l'hexagone flat-top) l'octave se retrouve juste à côté du libellé principal (.gs-deg/.gs-nn, centrés au
   même endroit) : décalé à droite (right, pas left/right auto) pour ne jamais chevaucher ce libellé, avec
   `transform:translateY(-50%)` pour un centrage vertical exact indépendant de la hauteur du texte lui-même.
   Taille réduite et pastille/cadre retirés (demande explicite) : le contraste sur fond clair (C/C#, "ton sur
   ton") est maintenant assuré par un double text-shadow sombre (contour fin autour du chiffre) plutôt qu'un
   fond plein — reste lisible sur n'importe quelle couleur de cellule sans le poids visuel d'une pastille. */
#page-play .gs-hex-cell .gs-chord-oct-badge{
  position:absolute;top:50%;right:10%;transform:translateY(-50%);
  font-family:var(--fm);font-size:clamp(.5rem,13cqh,.85rem);font-weight:700;
  color:#fff;line-height:1;pointer-events:none;
  text-shadow:0 0 2px rgba(0,0,0,.9),0 0 2px rgba(0,0,0,.9),0 1px 1px rgba(0,0,0,.9);
}
#page-play .gs-cell.gs-on .gs-chord-oct-badge{color:#fff}
#page-play .gs-hex-cell.gs-on .gs-chord-oct-badge{color:#fff}
/* Degré de gamme en chiffre romain (demande explicite : "afficher les degrés des accords") — même traitement
   visuel que le badge d'octave (contour sombre, pas de pastille) mais dans le coin OPPOSÉ (haut-gauche, au
   lieu de droite/centre) pour ne jamais chevaucher ni l'octave ni le libellé principal (racine+qualité,
   centré). Seulement sur .gs-chord-cell (jamais sur les cases notes normales, qui n'ont pas de "degré
   d'accord" à afficher). */
#page-play .gs-cell.gs-chord-cell .gs-chord-deg-badge{
  position:absolute;top:4%;left:6%;
  font-family:var(--fm);font-size:clamp(.5rem,16cqh,1rem);font-weight:700;
  color:#fff;line-height:1;pointer-events:none;
  text-shadow:0 0 2px rgba(0,0,0,.9),0 0 2px rgba(0,0,0,.9),0 1px 1px rgba(0,0,0,.9);
}
#page-play .gs-hex-cell.gs-chord-cell .gs-chord-deg-badge{
  position:absolute;top:15%;left:25%;
  font-family:var(--fm);font-size:clamp(.5rem,13cqh,.85rem);font-weight:700;
  color:#fff;line-height:1;pointer-events:none;
  text-shadow:0 0 2px rgba(0,0,0,.9),0 0 2px rgba(0,0,0,.9),0 1px 1px rgba(0,0,0,.9);
}
#page-play .gs-cell.gs-chord-cell.gs-chord-editable .gs-chord-deg-badge{display:none}
/* Mode ÉDITION PAR CELLULE (demande explicite) : les cases éditables reçoivent un pointillé fin pour
   signaler "tapez ici pour éditer, pas pour jouer" (évite toute ambiguïté avec le jeu normal) ; la case
   SÉLECTIONNÉE reçoit un contour plein et un léger zoom pour la distinguer sans ambiguïté de ses voisines. */
#page-play .gs-cell.gs-chord-editable{border-style:dashed}
#page-play .gs-cell.gs-chord-edit-sel{
  border-style:solid;border-width:3px;border-color:#fff;
  box-shadow:0 0 0 1px #fff,0 0 12px 2px rgba(255,255,255,.6);
  transform:scale(1.03);
}
/* ── Éditeur EN CASE, en 3 TIERS (demande explicite : "chaque cellule doit devenir 3 tiers de cellule, un
   tiers pour la note, un tiers pour la qualité, un tiers pour l'octave") ─────────────────────────────────
   Les 3 tiers REMPLACENT le libellé normal (masqué juste en dessous) plutôt que de s'y ajouter : afficher
   racine+qualité deux fois dans une case déjà petite la rendrait illisible — c'est précisément l'écueil que
   la recherche UX relevait (aucune app n'entasse racine+qualité+octave sur un pad).
   Occupe TOUTE la case (inset:0) pour que chaque tiers soit une cible tactile la plus grande possible :
   sur mobile, 3 zones dans une case de ~60px font déjà à peine 20px de haut chacune. */
#page-play .gs-cell.gs-chord-editable .gs-deg,
#page-play .gs-cell.gs-chord-editable .gs-nn,
#page-play .gs-cell.gs-chord-editable .gs-chord-oct-badge{display:none}
#page-play .gs-chord-edit-segs{
  position:absolute;inset:0;display:flex;flex-direction:column;border-radius:inherit;overflow:hidden;
}
#page-play .gs-chord-seg{
  flex:1 1 0;min-height:0;display:flex;align-items:center;justify-content:center;
  font-family:var(--fm);font-size:max(10px,.55rem);font-weight:700;color:var(--tx);
  background:color-mix(in srgb,var(--s1) 55%,transparent);
  cursor:ns-resize;                 /* signale le geste principal : glisser vertical */
  touch-action:none;                /* le glisser règle la valeur, il ne doit jamais faire défiler la grille */
  user-select:none;-webkit-user-select:none;
  overflow:hidden;white-space:nowrap;text-overflow:ellipsis;
  line-height:1;                    /* sans ça, la hauteur de ligne héritée débordait des 1/3 et rognait le
                                       3e tiers (octave) en bas de case — constaté en capture */
  padding:0 2px;box-sizing:border-box;
}
/* Séparateurs entre tiers : 1px suffit à découper visuellement les 3 zones sans manger de hauteur utile. */
#page-play .gs-chord-seg+.gs-chord-seg{border-top:1px solid color-mix(in srgb,var(--bd) 60%,transparent)}
@media(hover:hover){
  #page-play .gs-chord-seg:hover{background:color-mix(in srgb,var(--gs-c) 35%,var(--s1))}
}
/* Le tiers OCTAVE se distingue des 2 autres (chiffre seul, sinon confondable avec une qualité chiffrée
   type "7"/"9") — préfixé "oct." plutôt que teinté en gris pâle : le gris (--tx3) était illisible sur le
   fond sombre du tiers (constaté en capture), et le préfixe lève l'ambiguïté bien mieux qu'une nuance. */
#page-play .gs-chord-seg[data-seg="octave"]{color:var(--tx2);font-size:max(10px,.48rem);letter-spacing:.02em}
#page-play .gs-chord-seg[data-seg="octave"]::before{content:'oct ';opacity:.65}
/* Menu déroulant natif ouvert au clic sur un tiers (cf. _gsOpenChordSegMenu). Posé en position:fixed par le
   JS ; ici seulement l'apparence, alignée sur les autres selects du module. */
.gs-chord-seg-menu{
  font-family:var(--fm);font-size:.6rem;padding:2px 4px;border-radius:5px;
  background:var(--s2);color:var(--tx);border:1px solid var(--c-root);
}

/* ── CELLULES-BOUTONS DE FONCTION (demande explicite : "remplacer une ou plusieurs cellules de jeu par des
   boutons de fonction... possibilité de scinder une cellule de jeu en 2 ou 4 sous-cellules") — architecture
   visuelle copiée de .gs-chord-edit-segs/.gs-chord-seg juste au-dessus (mêmes contraintes : occuper TOUTE la
   case en position:absolute;inset:0, cibles tactiles maximisées). Teinte NEUTRE (var(--s1)/var(--tx2)), pas
   la couleur de note (--gs-c n'est jamais posée sur une case-bouton, cf. gsRender) : une case-bouton n'a pas
   de hauteur/couleur harmonique à représenter, contrairement à une case de jeu normale. */
#page-play .gs-cell.gs-func-cell{
  background:var(--s1);border-color:var(--bd2);cursor:pointer;
}
/* BUG CORRIGÉ (rapporté avec capture d'écran : "CC valeur 0 sur une ligne, CC valeur >0 prends 2 lignes et
   fait sauter la cellule") — .gs-deg (règle de base, cf. plus haut) n'a ni white-space:nowrap ni de largeur
   contrainte : un libellé plus long qu'une valeur à 1 chiffre ("CC20 102" contre "CC24 0") passait à la ligne
   dans la cellule au lieu de rester sur une seule ligne, changeant visuellement la hauteur perçue du texte
   d'une case-fonction à l'autre. Fix : nowrap + ellipsis + une largeur explicitement bornée à la case elle-
   même (max-width:100%, cf. le padding du parent) plutôt que de laisser le texte définir sa propre largeur. */
/* !important (bug rapporté PERSISTANT malgré la règle ci-dessus, capture à l'appui : "taille des étiquettes
   toujours pas réglée") — .gs-cell porte container-type:size (cf. plus bas dans ce fichier) et la règle
   générique #page-play .gs-deg{font-size:clamp(.7rem,26cqh,2.1rem)} calcule sa taille depuis cqh, une UNITÉ
   DE CONTAINER QUERY : ces unités sont résolues par le moteur de layout à un stade où leur PRIORITÉ effective
   sur une simple valeur fixe (.52rem) peut se comporter différemment de l'intuition "plus de classes = plus
   spécifique" selon le moteur — constaté que la règle fixe ci-dessous ne l'emportait PAS dans la pratique
   malgré une spécificité CSS supérieure sur le papier. !important lève toute ambiguïté : cette valeur gagne
   TOUJOURS sur une case-fonction, quelle que soit l'unité utilisée par une règle plus générique ailleurs. */
#page-play .gs-cell.gs-func-cell .gs-deg{
  color:var(--tx2);font-size:max(10px,.52rem)!important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  max-width:100%;padding:0 4px;box-sizing:border-box;
}
/* BUG CORRIGÉ (rapporté avec capture d'écran : étiquette d'une case-fonction GÉANTE sur un span fusionnant
   plusieurs cases) — .gs-cell porte container-type:size (cf. plus bas dans ce fichier, pour que le texte
   d'une case de JEU normale grandisse avec sa propre taille via cqh) : une case-fonction héritait donc de la
   même règle 26cqh malgré son .6rem fixe ci-dessus PLUS SPÉCIFIQUE (qui aurait dû gagner par la cascade) —
   dans les faits, l'ancre d'un span est un container DE TAILLE tellement démesurée (elle couvre plusieurs
   lignes/colonnes de la grille) que le rendu observé montre malgré tout un texte proportionnel à SA hauteur.
   Fix : re-déclarer .6rem ICI avec !important, ciblé spécifiquement sur .gs-func-span-anchor — jamais de
   cqh sur une ancre de span, sa taille de police reste FIXE quelle que soit l'étendue du rectangle qu'elle
   recouvre. */
#page-play .gs-cell.gs-func-span-anchor .gs-deg{font-size:.6rem!important}
#page-play .gs-cell.gs-func-cell.gs-on{background:var(--c-root);border-color:var(--c-root)}
#page-play .gs-cell.gs-func-cell.gs-on .gs-deg{color:#111}
/* Bordure en pointillés en mode ÉDITION (demande explicite : bouton dédié, cf. gsToggleFuncEditMode) — même
   signal visuel que .gs-chord-editable pour rester cohérent dans tout le module : "cette case est dans un
   état spécial, taper dessus ne joue rien de normal". */
#page-play .gs-cell.gs-func-editable{border-style:dashed;border-color:var(--c-root)}
/* Case VIDE (demande explicite : "permettre de 'vider' des cellules (les rendre noires et inactives) pour y
   mettre des contrôles libres par la suite") — noir UNI, jamais var(--s1) (la teinte neutre habituelle d'une
   case-fonction, trop claire pour servir de socle "invisible" sous un widget libre semi-transparent ou pour
   simplement assombrir une zone de la grille). :not(.gs-func-editable) : en mode édition, la bordure en
   pointillés doit rester visible même sur une case vide (sinon on ne saurait plus où taper pour la
   reconfigurer) — noir pur SEULEMENT hors édition. Pas de :hover ni de curseur pointer implicites à changer,
   ceux de .gs-cell/.gs-func-cell restent : une case vide reste techniquement tapable (pour la reconfigurer en
   mode édition), juste visuellement neutre en jeu normal. */
#page-play .gs-cell.gs-func-blank:not(.gs-func-editable),
#page-play .gs-func-seg.gs-func-blank,
#page-play .gs-func-free-widget.gs-func-blank{background:#000;border-color:#000}
#page-play .gs-func-free-widget.gs-func-blank{box-shadow:none}
#page-play .gs-func-segs{
  position:absolute;inset:0;border-radius:inherit;overflow:hidden;display:grid;gap:0;
}
/* subdiv=2 : 2 bandes HORIZONTALES empilées par défaut (même orientation que .gs-chord-edit-segs, cf. son
   commentaire) — .gs-func-segs-2h (demande explicite : "ajouter 2 (gauche/droite)") est la variante gauche/
   droite, choisie à l'assignation (cf. GS_FUNC_SUBDIV_OPTIONS). subdiv=4 : vraie grille 2×2. */
#page-play .gs-func-segs-2{grid-template-rows:1fr 1fr;grid-template-columns:1fr}
#page-play .gs-func-segs-2h{grid-template-rows:1fr;grid-template-columns:1fr 1fr}
#page-play .gs-func-segs-4{grid-template-rows:1fr 1fr;grid-template-columns:1fr 1fr}
#page-play .gs-func-seg{
  display:flex;align-items:center;justify-content:center;
  font-family:var(--fm);font-size:max(10px,.48rem);font-weight:700;color:var(--tx2);
  background:var(--s1);cursor:pointer;user-select:none;-webkit-user-select:none;
  overflow:hidden;white-space:nowrap;text-overflow:ellipsis;padding:0 2px;box-sizing:border-box;
  container-type:size; /* cf. .gs-func-free-widget : nécessaire pour .gs-func-knob-dial (cqh), un segment de case subdivisée peut aussi héberger un potard */
}
/* Séparateur visuel entre segments (bug rapporté : le gap+background de .gs-func-segs ci-dessus n'était PAS
   assez contrasté avec --s1 dans certains thèmes pour se voir réellement, cf. capture "aucune ligne visible
   entre Latch/Auto/Rec/ARP") — remplacé par une VRAIE bordure sur chaque segment, dans une teinte fixe
   (color-mix contre --tx, jamais --bd) qui reste visible quel que soit le thème clair/sombre. nth-child cible
   la bordure INTÉRIEURE de chaque grille (jamais le bord extérieur de la case, déjà tracé par .gs-cell) :
   pour subdiv=2 (grid-template-rows:1fr 1fr), seul le 1er des 2 segments a une bordure basse ; pour subdiv=4
   (grille 2×2), les 2 segments du haut ont une bordure basse ET les 2 segments de gauche (1er/3e) ont une
   bordure droite — trace un "+" complet au centre de la case. */
#page-play .gs-func-segs-2 .gs-func-seg:nth-child(1){border-bottom:1px solid color-mix(in srgb,var(--tx) 25%,transparent)}
#page-play .gs-func-segs-2h .gs-func-seg:nth-child(1){border-right:1px solid color-mix(in srgb,var(--tx) 25%,transparent)}
#page-play .gs-func-segs-4 .gs-func-seg:nth-child(1),
#page-play .gs-func-segs-4 .gs-func-seg:nth-child(2){border-bottom:1px solid color-mix(in srgb,var(--tx) 25%,transparent)}
#page-play .gs-func-segs-4 .gs-func-seg:nth-child(1),
#page-play .gs-func-segs-4 .gs-func-seg:nth-child(3){border-right:1px solid color-mix(in srgb,var(--tx) 25%,transparent)}
#page-play .gs-func-seg.gs-on{background:var(--c-root);color:#111}
/* Survol (demande explicite : "au survol d'une cellule fonction, la couleur de survol empêche de voir l'état
   des boutons, corriger ceci") — :not(.gs-on) exclut les segments déjà ALLUMÉS : un survol qui repeindrait la
   même teinte que .gs-on masquerait totalement la distinction on/off tant que le curseur reste dessus (bug
   rapporté). Un segment déjà .gs-on garde sa couleur pleine (var(--c-root)) même survolé — seul un segment
   ÉTEINT réagit au survol, avec une teinte volontairement distincte de --c-root plein pour ne jamais se
   confondre avec l'état allumé. Même correction appliquée à .gs-cell.gs-func-cell (case non subdivisée). */
@media(hover:hover){
  #page-play .gs-func-seg:not(.gs-on):hover{background:color-mix(in srgb,var(--c-root) 20%,var(--s1))}
  #page-play .gs-cell.gs-func-cell:not(.gs-on):hover{background:color-mix(in srgb,var(--c-root) 20%,var(--s1))}
}
/* Flash de confirmation au tap (cf. _gsFuncCellFlash, app.js) — GPU-only (transform/opacity), même
   contrainte que les autres effets du module (cf. l'en-tête de la section Effets visuels plus haut). */
@keyframes gs-func-flash{
  0%{filter:brightness(1.9)}
  100%{filter:brightness(1)}
}
#page-play .gs-func-flash{animation:gs-func-flash .3s ease-out}
/* FADER de case-fonction (demande explicite : "les cellules de fonction doivent pouvoir contenir un fader, un
   potentiomètre... pour faire varier des valeurs") — même principe que .gs-pitchwheel-thumb (remplissage
   depuis un bord plutôt qu'un curseur positionné, jamais de risque de déborder visuellement du conteneur) :
   .gs-func-fader-fill grandit en HAUTEUR depuis le bas, piloté par --gs-ff (0-100%, posé par gsRender ET par
   _gsFuncFaderPointerMove pendant le glisser). position:relative sur le parent (case ou segment) + z-index sur
   le libellé : le remplissage reste TOUJOURS lisible sous le texte, jamais par-dessus. */
#page-play .gs-cell.gs-func-cell.gs-func-fader,
#page-play .gs-func-seg.gs-func-fader,
#page-play .gs-func-free-widget.gs-func-fader{position:relative;overflow:hidden}
#page-play .gs-func-fader-fill{
  position:absolute;left:0;right:0;bottom:0;height:var(--gs-ff,0%);
  background:color-mix(in srgb,var(--c-root) 45%,transparent);
  pointer-events:none; /* le geste de glisser est capté par le parent (cell/segment), jamais par ce calque */
}
#page-play .gs-func-seg-label{position:relative;z-index:1}
#page-play .gs-cell.gs-func-cell.gs-func-fader .gs-deg{position:relative;z-index:1}
/* SPANS — groupes de cases fusionnées en un seul widget (mode contrôleur/hybride, demande explicite :
   "réfléchir à des faders plus grands (association de plusieurs cellules)"). Décision confirmée : les cases
   NON-ancre du rectangle restent de VRAIS éléments de grille (nécessaire pour ne pas décaler le placement
   auto des cases suivantes ni casser le hit-test _gsCellFromPoint, cf. le commentaire de gsRender) mais
   invisibles — visibility:hidden plutôt que display:none : ce dernier retirerait la case du flux de la
   grille CSS (grid-auto-flow réassignerait alors TOUTES les cases suivantes une position plus tôt, décalant
   tout le reste de la grille), alors que visibility:hidden garde sa place réservée tout en la rendant non
   peinte. pointer-events:none en plus : un pointerdown qui tomberait dessus (bord d'un geste de glisser en
   ARRIVANT sur le span, avant capture du pointeur par l'ancre) ne doit rien déclencher de spécifique à CETTE
   case cachée — gsPointerDown la traite de toute façon comme un span normal via _gsFuncSpanAt, mais autant
   éviter tout survol/étirement visuel parasite dessus. */
#page-play .gs-cell.gs-func-span-hidden{visibility:hidden;pointer-events:none}
/* L'ANCRE s'agrandit PAR-DESSUS son rectangle via grid-column/row:span (posé inline par gsRender, cf. son
   commentaire) — z-index nécessaire pour peindre par-dessus les bordures/gaps des cases cachées qu'elle
   recouvre visuellement (sans lui, le gap de la grille CSS resterait visible en fines lignes à travers).
   Mêmes styles de fond que .gs-func-cell normal (héritage de classe, rien à redéfinir ici) — seule la TAILLE
   change, jamais l'apparence. */
#page-play .gs-cell.gs-func-span-anchor{z-index:2;position:relative}
/* Aperçu du rectangle en cours de glisser (demande explicite, confirmé : "glisser d'un coin à l'autre en
   mode édition" ; rendu plus visible suite au rapport "la zone highlightée... n'est pas assez visible") —
   BUG CORRIGÉ : l'ancien halo (bordure fine 2px + fond à seulement 25% d'opacité) se distinguait mal sur une
   case de note déjà colorée en dessous (avant que le span ne remplace son rendu). Reconstruit en surbrillance
   FRANCHE : bordure ÉPAISSE en blanc/noir contrasté (jamais --c-root, qui peut lui-même être proche de la
   couleur de la case sous-jacente — ex. une case déjà orange rendrait un contour orange invisible) + fond
   nettement plus opaque + une ANIMATION de pulsation (déjà utilisée ailleurs dans ce module pour attirer
   l'œil sur un état transitoire, cf. gs-func-flash) qui rend la zone reconnaissable même du coin de l'œil
   pendant qu'on regarde son doigt plutôt que l'écran. */
#page-play .gs-cell.gs-func-span-preview{
  box-shadow:inset 0 0 0 3px #fff,inset 0 0 0 5px #000;
  background:color-mix(in srgb,var(--c-root) 55%,var(--s1));
  animation:gs-func-span-preview-pulse 1s ease-in-out infinite;
}
@keyframes gs-func-span-preview-pulse{
  0%,100%{filter:brightness(1)}
  50%{filter:brightness(1.35)}
}
@media(prefers-reduced-motion:reduce){#page-play .gs-cell.gs-func-span-preview{animation:none}}
/* PAD XY (mode contrôleur : "pads xy, faders, potatoes, switches") — .gs-func-xy-dot est un point qui se
   déplace en 2D dans la case, positionné en pourcentage de CHAQUE axe (--gs-ffx/--gs-ffy, cf. _gsFuncSegHtml/
   _gsFuncXyPointerMove) plutôt qu'une barre 1D comme .gs-func-fader-fill — transform:translate(-50%,-50%)
   centre le point SUR sa position plutôt que d'ancrer son coin, seule différence de mécanique avec un fader
   classique (qui grandit depuis un bord, un point n'a pas de "bord" naturel à ancrer). Axe Y en CSS `top`
   calculé depuis LE HAUT (100%-ffy) : convention déjà établie ailleurs dans le module (Pitch Wheel verticale)
   où glisser vers le HAUT augmente la valeur — ffy=100% (valeur max) doit donc placer le point tout en HAUT
   de la case (top:0%), pas en bas. */
#page-play .gs-cell.gs-func-cell.gs-func-xy,
#page-play .gs-func-seg.gs-func-xy,
#page-play .gs-func-free-widget.gs-func-xy{position:relative;overflow:hidden}
#page-play .gs-func-xy-dot{
  position:absolute;left:var(--gs-ffx,50%);top:calc(100% - var(--gs-ffy,50%));
  transform:translate(-50%,-50%);
  width:14px;height:14px;border-radius:50%;
  background:var(--c-root);box-shadow:0 0 0 2px color-mix(in srgb,var(--c-root) 40%,transparent);
  pointer-events:none;
}
/* POTARD (demande explicite : "faders... potatoes", confirmé "variante visuelle du fader, choix de style à
   l'assignation") — même valeur/glisser que .gs-func-fader, rendu en cadran rond avec un repère tournant
   plutôt qu'un remplissage. --gs-fa (angle en degrés, -135 à +135, cf. _gsFuncSegHtml) pilote la rotation du
   repère via transform:rotate — jamais la rotation du cadran entier, qui resterait fixe comme un vrai potard
   physique (seule l'aiguille/le repère bouge). */
#page-play .gs-cell.gs-func-cell.gs-func-knob,
#page-play .gs-func-seg.gs-func-knob,
#page-play .gs-func-free-widget.gs-func-knob{position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center;flex-direction:column}
/* BUG CORRIGÉ (rapporté avec capture d'écran : "arc en forme de virgule", cassé/minuscule) — l'ancien rendu
   (cadran plein + petite aiguille tournante en transform:rotate) produisait en pratique une forme confuse,
   pas un potard reconnaissable. Refonte en ANNEAU ÉPAIS REMPLI (cf. capture de référence fournie, style
   Ableton) : conic-gradient trace un secteur de --gs-fa-deg (0-270°, cf. _gsFuncSegHtml/_gsFuncFaderPointerMove)
   à partir du bord de départ (-135°, tourné via rotate sur l'ÉLÉMENT ENTIER — le dégradé lui-même part
   toujours de 0° en haut), le reste du disque dans une teinte neutre atténuée (la "piste" non remplie du
   potard, toujours visible, contrairement à du transparent qui laisserait deviner un simple camembert plein).
   mask-image (radial-gradient opaque au centre, transparent au bord intérieur de l'anneau) troue le disque
   SANS dépendre de la couleur de fond du conteneur (contrairement à un ::before opaque, qui aurait dû deviner
   quelle teinte de fond utiliser selon .gs-on/case normale/widget libre — fragile) : le trou laisse voir
   littéralement ce qu'il y a DERRIÈRE, quelle que soit sa couleur. -webkit-mask-image dupliqué pour Safari. */
/* BUG CORRIGÉ (rapporté avec capture d'écran : potard minuscule dans une grande case/un span alors qu'il
   pourrait remplir bien plus d'espace) — max-width:40px plafonnait la taille à une valeur FIXE quelle que
   soit la taille réelle de la case/du span qui l'héberge, écrasante sur une grande zone. Remplacé par
   min(60%,60cqh) : s'agrandit avec le conteneur (cqh, container query height — .gs-cell porte déjà
   container-type:size, cf. plus bas dans ce fichier, exactement le mécanisme déjà utilisé pour .gs-deg) sans
   dépasser 60% de sa LARGEUR non plus (aspect-ratio:1 le garde carré, jamais un ovale sur une case très large
   ou très haute). */
#page-play .gs-func-knob-dial{
  position:relative;width:min(60%,60cqh);aspect-ratio:1;
  border-radius:50%;transform:rotate(-135deg);
  background:conic-gradient(var(--c-root) var(--gs-fa-deg,0deg),color-mix(in srgb,var(--tx) 18%,transparent) var(--gs-fa-deg,0deg) 270deg,transparent 270deg 360deg);
  -webkit-mask-image:radial-gradient(circle,transparent 55%,#000 56%);
  mask-image:radial-gradient(circle,transparent 55%,#000 56%);
}
/* Potard ROTATIF : se distingue du potard à glissé vertical par un bouton en relief (capuchon avec aiguille qui
   tourne) au centre de l'anneau, au lieu du simple anneau. L'anneau passe en ::before (masque propre) pour que le
   masque du cadran ne troue pas le capuchon ; le cadran lui-même est déjà tourné de -135° (début de course). */
#page-play .gs-func-knob-dial.gs-func-rot-dial{background:none;-webkit-mask-image:none;mask-image:none}
#page-play .gs-func-rot-dial::before{
  content:"";position:absolute;inset:0;border-radius:50%;
  background:conic-gradient(var(--c-root) var(--gs-fa-deg,0deg),color-mix(in srgb,var(--tx) 18%,transparent) var(--gs-fa-deg,0deg) 270deg,transparent 270deg 360deg);
  -webkit-mask-image:radial-gradient(circle,transparent 70%,#000 71%);mask-image:radial-gradient(circle,transparent 70%,#000 71%);
}
#page-play .gs-func-rot-dial::after{
  content:"";position:absolute;inset:20%;border-radius:50%;
  background:linear-gradient(var(--c-root),var(--c-root)) 50% 6%/9% 34% no-repeat,
    radial-gradient(circle at 35% 30%,color-mix(in srgb,var(--tx) 22%,var(--s3)),var(--s3) 70%);
  border:1px solid var(--bd2);box-shadow:0 2px 5px rgba(0,0,0,.45);
  transform:rotate(var(--gs-fa-deg,0deg));
}
/* Panneau de réglage par ligne (demande explicite : choisir la qualité de CHAQUE accord) — une rangée
   horizontale scrollable, une entrée par ligne de la zone accords. Scroll horizontal plutôt qu'empilement
   vertical : jusqu'à 12 lignes possibles (cf. _gsDims), les empiler prendrait toute la hauteur utile au
   détriment de la grille elle-même, qui doit rester l'élément dominant de l'écran. */
/* flex-wrap (pas overflow-x:auto) — demande explicite : "blocs accords dans les paramètres surface : aller
   à la ligne au besoin, pas de scroll horizontal". #gsChordPanel ne vit plus QUE dans Paramètres/Accords
   désormais (cf. index.html, .gs-settings-embed) : le défilement horizontal avait du sens dans l'ancien
   dropdown compact (largeur limitée, hauteur à préserver), plus dans une colonne de largeur confortable où
   revenir à la ligne est la lecture naturelle. */
/* margin-top (demande explicite : "espace avant les accords") — .gs-chord-rows-panel suit directement le
   dernier .gs-midi-bar (Presets, cf. gsRenderChordPanel dans app.js) sans aucune marge : les boutons
   "Enregistrer"/"Supprimer" touchaient visuellement le 1er bloc d'accord juste en dessous. */
#page-play .gs-chord-rows-panel{
  display:flex;flex-wrap:wrap;gap:6px;padding:4px 8px;margin-top:6px;background:var(--s2);border-bottom:1px solid var(--bd);
  flex-shrink:0;
}
#page-play .gs-chord-row-ctrl{
  display:flex;flex-direction:column;align-items:center;gap:2px;flex-shrink:0;
  padding:3px 6px;border-radius:6px;border:1px solid var(--gs-c);min-width:64px;
}
#page-play .gs-chord-row-label{font-family:var(--fm);font-size:max(10px,.55rem);font-weight:700;color:var(--gs-c);white-space:nowrap}
#page-play .gs-chord-row-ctrl select{font-size:max(10px,.5rem);max-width:70px}

/* ── Mode EDIT : tiroir de blocs d'accords (demande explicite : "les accords apparaissent sous forme de
   blocs... et peuvent être glissés sur des pads de la grille") ── */
#page-play .gs-chord-chip-tray{display:flex;gap:6px;flex-wrap:wrap;padding:4px 8px}
#page-play .gs-chord-chip{
  display:flex;align-items:center;gap:5px;padding:4px 6px;border-radius:6px;
  border:1px solid var(--gs-c);background:color-mix(in srgb,var(--gs-c) 14%,var(--s2));
  font-family:var(--fm);font-size:.62rem;font-weight:700;color:var(--gs-c);
  cursor:grab;touch-action:none;user-select:none;-webkit-user-select:none;
}
#page-play .gs-chord-chip:active{cursor:grabbing}
#page-play .gs-chord-chip-oct{font-size:max(10px,.5rem);font-weight:400;color:var(--tx3);opacity:.8}
#page-play .gs-chord-chip-del{
  background:none;border:none;color:var(--tx3);font-size:.7rem;line-height:1;cursor:pointer;padding:0 2px;
}
#page-play .gs-chord-chip-del:hover{color:var(--tx)}
#page-play .gs-chord-chip-ghost{box-shadow:0 4px 14px var(--sh)}
/* Case survolée pendant un glisser de chip — même famille visuelle que .gs-on (couleur d'accent), mais un
   contour pointillé plutôt qu'un remplissage : signale "cible de dépôt", pas "en train de sonner". */
#page-play .gs-cell.gs-chord-drop-target{outline:2px dashed var(--c-root);outline-offset:-2px}

/* ── Séparateur PHYSIQUE accords/notes (demande explicite : "crée moi un séparateur physique que je peux
   cliquer/glisser pour déplacer... avec un bouton verrou pour empêcher de le déplacer pendant le jeu") ──
   Une barre fine posée EN SURIMPRESSION sur la grille (position:absolute dans #gsGridWrap, cf. sa règle
   position:relative plus haut), repositionnée en JS à chaque gsRender() selon gsChordRegion (cf.
   _gsSyncChordSeparator dans app.js) plutôt que dessinée en CSS pur : sa position dépend du nombre de
   lignes/colonnes RÉEL de la grille courante, calculable seulement une fois les cellules rendues. */
#page-play .gs-chord-separator{
  position:absolute;z-index:20;background:var(--c-root);box-shadow:0 0 0 1px var(--sh);
  will-change:transform; /* calque PROPRE et permanent (4px) : sinon, peint au-dessus de la grille, il était fusionné avec
     .gs-func-free-layer en un calque 412x736 promu/détruit à chaque flash de case — cf. .nav-overlay (flashs noirs) */
  cursor:grab;touch-action:none;
}
#page-play .gs-chord-separator.gs-locked{cursor:default;opacity:.55}
#page-play .gs-chord-separator:active{cursor:grabbing}
/* Orientation HORIZONTALE (région bas/haut) : barre pleine largeur, épaisse verticalement, poignée au
   centre. Orientation VERTICALE (région gauche/droite) : l'inverse. Classe posée par _gsSyncChordSeparator. */
/* left/right (resp. top/bottom) et width/height PAS posés ici en CSS fixe (bug corrigé, rapporté : "il
   s'arrête au milieu d'une ligne... n'est plus entier" en vertical) — un simple left:0;right:0 span TOUTE
   la boîte de #gsGridWrap, padding compris, qui est plus large/haute que la grille RÉELLE (#gsGrid, plus
   étroite à cause de ce padding) : la barre débordait ou s'arrêtait avant les vrais bords de la grille selon
   le côté. _gsSyncChordSeparator (app.js) mesure désormais les coins RÉELS de la grille à chaque
   repositionnement et pose ces 4 valeurs en JS, en pixels exacts. */
/* Épaisseur ≤ au gap RÉEL de la grille (5px desktop, 4px mobile — cf. .gs-grid{gap:...}), bug rapporté :
   "le séparateur est toujours... en leur milieu [d'une cellule]" — la BARRE elle-même est bien positionnée
   dans l'espacement (vérifié en pixels), mais à 6px elle débordait déjà de 1px de chaque côté sur un gap de
   5px ; surtout, sa POIGNÉE ronde de 26px (bien plus grande que ce gap de quelques px) chevauchait
   massivement le contenu des cases voisines, ce qui EST ce que l'utilisateur voit et décrit à raison comme
   "dans une cellule". 4px tient dans les 2 tailles de gap sans déborder.*/
#page-play .gs-chord-separator.gs-sep-h{height:4px;border-radius:2px}
#page-play .gs-chord-separator.gs-sep-v{width:4px;border-radius:2px}
/* Poignée repensée (bug rapporté : même un rond de 12px déborde encore d'un gap de 4-5px, AUCUNE poignée
   plus large que la ligne elle-même ne peut tenir dedans) — au lieu d'une forme séparée à faire "tenir"
   dans le gap, la poignée est maintenant un simple SEGMENT PLUS LARGE de la barre elle-même (même
   épaisseur qu'elle, cf. GS_CHORD_SEP_THICKNESS/.gs-sep-h/v), donc jamais plus haute/large que la ligne —
   elle ne peut PAS déborder sur les cases voisines par construction, contrairement à un rond ou tout autre
   élément dont l'épaisseur propre dépasserait celle de la barre. Se lit comme un repère (segment plus clair/
   opaque) plutôt que comme un bouton posé par-dessus le contenu des cases. */
#page-play .gs-chord-separator-handle{
  position:absolute;top:0;left:50%;bottom:0;transform:translateX(-50%);
  width:22px;background:#fff;opacity:.55;border-radius:inherit;pointer-events:none;
}
#page-play .gs-chord-separator.gs-sep-v .gs-chord-separator-handle{
  top:50%;left:0;right:0;bottom:auto;height:22px;width:auto;transform:translateY(-50%);
}
#page-play .gs-chord-separator.gs-locked .gs-chord-separator-handle{background:var(--tx3)}
/* Zone de préhension INVISIBLE, plus grande que la barre elle-même (cible tactile ~40px malgré une ligne de
   4px) — étend la surface pointerdown SANS agrandir ce qui est visuellement peint, cf. la poignée réduite
   ci-dessus. */
#page-play .gs-chord-separator::before{
  content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  width:40px;height:40px;
}
@media(prefers-reduced-motion:reduce){
  #page-play .gs-cell{transition:none}
  #page-play .gs-cell.gs-fretless-glow{animation:none}
  /* Flash d'attaque ajouté avec le renforcement visuel (cf. gs-cell-flash) — même traitement que les ondes
     ci-dessus : l'animation est retirée, l'anneau/lueur STATIQUE de .gs-on reste (information, pas décor). */
  #page-play .gs-cell.gs-on{animation:none}
}

/* ── Pitch wheel tactile (demande explicite : "ajouter une surface tactile qui fait office de pitch wheel
   pour les deux modes, désactivable via un bouton dans subbar") ────────────────────────────────────────────
   Bug corrigé (retour utilisateur : "je ne le vois pas") — la 1re version posait la bande en
   position:absolute PAR-DESSUS la grille (qui, elle, remplit 100% de .gs-grid-wrap en display:flex), donc la
   bande se retrouvait plaquée SUR la dernière colonne de cases plutôt qu'à côté : visuellement noyée dans les
   bordures de cellules, et volant en plus une partie de leur surface tactile. Fix : .gs-pitchwheel devient un
   VRAI ENFANT FLEX de .gs-grid-wrap (flex:none, largeur fixe), au lieu d'un absolute — il prend alors sa
   propre place dans la ligne, et la grille (flex:1, inchangée) RÉTRÉCIT d'autant pour lui laisser la place,
   exactement comme le ferait une barre latérale classique. Toujours un <div> DUPLIQUÉ dans #gsGridWrap ET
   #gsHexGridWrap (jamais un élément unique partagé, même convention que #gsGrid/#gsHexGrid) : chacun est un
   flex-item de son PROPRE wrap. */
/* Largeur ÉLARGIE (demande explicite : "élargir la zone de pitch" — 34px jugé trop étroit pour un geste de
   glisser précis au doigt) — 64px, cohérent avec les autres cibles tactiles surdimensionnées du module (cf.
   .gs-oct-group .tbtn plus haut dans ce fichier, même raison : un geste continu répété en jouant mérite une
   zone large, pas seulement un clic ponctuel). border-radius suit la largeur (moitié) pour rester une pilule
   complète, pas juste agrandie en gardant l'ancien rayon de coin.
   POSITION RÉGLABLE (demande explicite : "ajouter position PW : haut/bas/gauche/droite") — .gs-grid-wrap
   (déjà en display:flex, cf. sa règle) reçoit une classe .gs-pw-top/-bottom/-left/-right posée par
   _gsSyncPitchWheelPos (app.js), qui pilote À LA FOIS flex-direction du conteneur ET la forme de la bande
   elle-même (verticale à gauche/droite, horizontale en haut/bas) — un seul point de vérité (la classe sur le
   wrap) plutôt que 2 réglages à synchroniser (orientation du conteneur + forme de la bande). 'right' est le
   comportement d'ORIGINE (aucune classe ajoutée ne le change, cf. le repli implicite ci-dessous). */
#page-play .gs-pitchwheel{
  flex:none;width:64px;align-self:stretch;position:relative;margin-left:6px;
  background:var(--s2,rgba(255,255,255,.06));border:1px solid var(--bd2);border-radius:32px;
  touch-action:none;-webkit-user-select:none;user-select:none;
  /* overflow:hidden AJOUTÉ (bug rapporté avec capture : "en butée, la poignée de la pitchwheel dépasse du
     container") — la poignée fixe (pilule 28px, cf. l'ancienne .gs-pitchwheel-thumb) était toujours centrée
     sur sa position (transform:translateY(-50%)), donc la moitié de sa hauteur débordait hors du conteneur
     dès que top approchait 0%/100% (butée haute/basse). Remplacée par un REMPLISSAGE (cf. .gs-pitchwheel-thumb
     ci-dessous) qui grandit DEPUIS le centre plutôt que d'être un bloc de taille fixe qui se déplace — par
     construction, un remplissage ne peut jamais dépasser les bords qu'il remplit. overflow:hidden reste posé
     ici en filet de sécurité (le border-radius de la pilule ne devrait de toute façon plus produire de
     dépassement, mais coûte zéro et évite toute régression future sur ce point précis). */
  overflow:hidden;
}
/* 'left' : même forme verticale que 'right' (repli implicite), seul l'ORDRE dans le flex change (la bande
   doit apparaître AVANT la grille dans le sens de lecture) — cf. order ci-dessous, jamais de réordonnancement
   du DOM lui-même (plus simple, purement visuel). */
#page-play .gs-grid-wrap.gs-pw-left .gs-pitchwheel{margin-left:0;margin-right:6px;order:-1}
#page-play .gs-grid-wrap.gs-pw-left .gs-grid,
#page-play .gs-grid-wrap.gs-pw-left .gs-hex-grid{order:0}
/* 'top'/'bottom' : la bande devient HORIZONTALE — le conteneur bascule en colonne (flex-direction:column),
   la bande prend la largeur pleine et une hauteur fixe au lieu de l'inverse. margin-left retiré (plus de
   sens en disposition colonne), remplacé par margin-bottom/-top selon le côté. */
#page-play .gs-grid-wrap.gs-pw-top,
#page-play .gs-grid-wrap.gs-pw-bottom{flex-direction:column}
#page-play .gs-grid-wrap.gs-pw-top .gs-pitchwheel,
#page-play .gs-grid-wrap.gs-pw-bottom .gs-pitchwheel{
  width:auto;height:44px;align-self:auto;margin-left:0;
}
#page-play .gs-grid-wrap.gs-pw-top .gs-pitchwheel{margin-bottom:6px;order:-1}
#page-play .gs-grid-wrap.gs-pw-bottom .gs-pitchwheel{margin-top:6px;order:1}
#page-play .gs-grid-wrap.gs-pw-top .gs-grid,#page-play .gs-grid-wrap.gs-pw-top .gs-hex-grid{order:0}
#page-play .gs-grid-wrap.gs-pw-bottom .gs-grid,#page-play .gs-grid-wrap.gs-pw-bottom .gs-hex-grid{order:0}
/* REMPLISSAGE plutôt qu'une poignée fixe (demande explicite après le bug de dépassement en butée, cf.
   .gs-pitchwheel ci-dessus : "n'utilisons plus cette poignée, utilisons un remplissage coloré à la place,
   dans le container uniquement") — la barre grandit DEPUIS LE CENTRE (repos, 0 demi-ton) vers le bord touché,
   comme un VU-mètre bidirectionnel : bend positif remplit vers le haut (vertical) / vers la droite
   (horizontal), négatif vers le bas / la gauche. gsPitchWheelPointerMove (app.js) pose top+height (vertical)
   ou left+width (horizontal) directement — jamais transform+une hauteur/largeur FIXE comme l'ancien curseur,
   qui est justement ce qui causait le dépassement (une pilule de taille constante centrée sur sa position, au
   lieu d'un remplissage dont la taille elle-même varie et reste par construction dans les bords). */
/* --gs-pw-c (demande explicite : "lui donner la couleur de la note qu'il affecte") — posée en inline sur
   .gs-pitchwheel (PAS sur .gs-pitchwheel-thumb, cf. gsPitchWheelPointerDown dans app.js) d'après la note
   tenue au moment où le doigt se pose sur la bande, avec repli sur --c-root (accent général) si aucune note
   n'est tenue ou si aucun code couleur n'est actif (mode Sobre) — même convention que .gs-cell.gs-on/
   .gs-finger-ripple. */
#page-play .gs-pitchwheel-thumb{
  position:absolute;left:2px;right:2px;top:50%;height:0;
  background:var(--gs-pw-c,var(--c-root));border-radius:0 0 3px 3px;pointer-events:none;
}
/* Coins arrondis inversés en HORIZONTAL (le remplissage part du centre vers gauche/droite plutôt que du
   centre vers haut/bas) — cf. gsPitchWheelPointerMove qui pose left+width sur ce même élément. */
#page-play .gs-grid-wrap.gs-pw-top .gs-pitchwheel-thumb,
#page-play .gs-grid-wrap.gs-pw-bottom .gs-pitchwheel-thumb{
  border-radius:3px 0 0 3px;
}
/* Repère central (demande explicite : "ajouter un repère au centre sous forme d'une ligne qui coupe la PW
   en deux") — trait fin FIXE au milieu de la bande, distinct du curseur (.gs-pitchwheel-thumb, qui se
   déplace) : marque visuellement le point de repos (0 demi-ton) même quand le curseur s'en est éloigné, un
   repère que le curseur seul ne donne pas une fois qu'il a bougé. ::before plutôt qu'un élément DOM
   supplémentaire : purement décoratif, aucune interaction, aucun état à synchroniser en JS. z-index implicite
   au-dessus du fond de la bande mais SOUS le curseur (.gs-pitchwheel-thumb est un élément DOM postérieur
   dans le flux, donc peint après par défaut — pas de z-index explicite nécessaire ici). */
#page-play .gs-pitchwheel::before{
  content:'';position:absolute;left:8px;right:8px;top:50%;height:1px;
  background:var(--bd2);pointer-events:none;
}
#page-play .gs-grid-wrap.gs-pw-top .gs-pitchwheel::before,
#page-play .gs-grid-wrap.gs-pw-bottom .gs-pitchwheel::before{
  left:50%;right:auto;top:8px;bottom:8px;height:auto;width:1px;
}
/* Variante horizontale du remplissage — mêmes proportions tournées 90° que la variante verticale de base
   (top:50%;height:0, cf. .gs-pitchwheel-thumb plus haut) : ici c'est left:50%;width:0 au repos, la LARGEUR
   grandissant depuis le centre au lieu de la hauteur. gsPitchWheelPointerMove (app.js) écrit `left`+`width`
   (au lieu de `top`+`height`) sur ce même élément dès que la bande est horizontale.
   BUG CORRIGÉ (rapporté avec capture, position Bas : un carré plein orange visible AU REPOS, avant tout
   geste) — cette règle datait de l'ANCIENNE poignée fixe (pilule de taille CONSTANTE, retirée depuis au
   profit d'un remplissage dynamique, cf. l'historique de .gs-pitchwheel-thumb) et n'avait jamais été mise à
   jour en conséquence : width:28px codé en dur ici forçait un bloc plein visible dès le premier rendu, AVANT
   qu'aucun geste n'ait eu l'occasion de poser width:0 en inline (_gsPitchWheelReset ne s'exécute qu'APRÈS un
   1er relâchement). width:0 explicite ci-dessous reproduit l'état de repos voulu dès le rendu initial, sans
   dépendre d'un geste préalable pour l'atteindre. */
#page-play .gs-grid-wrap.gs-pw-top .gs-pitchwheel-thumb,
#page-play .gs-grid-wrap.gs-pw-bottom .gs-pitchwheel-thumb{
  left:50%;right:auto;top:6px;bottom:6px;height:auto;width:0;
}
/* Plein écran du Séquenceur (seqToggleFullscreen, melodie.js) : même principe que Surface ci-dessous. */
body.seq-fullscreen .topbar,
body.seq-fullscreen .desktop-root-row,
body.seq-fullscreen .sidebar,
body.seq-fullscreen .sidebar-desktop,
body.seq-fullscreen .mobile-nav{display:none !important}
body.seq-fullscreen #page-melodie.page.active{position:fixed;left:0;right:0;top:0 !important;bottom:0;z-index:60;overflow:hidden;background:var(--bg)}
#page-melodie .sq-btn.on{background:var(--c-root);border-color:var(--c-root);color:#111}
/* ── Plein écran ─────────────────────────────────────────────────────────────────────
   La classe vit sur <body> parce que les éléments à masquer (topbar, sidebar) sont HORS de #page-play.
   !important est nécessaire ici : ces éléments ont des règles d'affichage propres à chaque point de rupture
   (cf. règle projet sur les !important desktop, dev/CLAUDE.md), qu'une simple règle scopée ne battrait pas
   à toutes les largeurs. Le top:0 remplace le top:var(--gs-top) : la topbar n'occupe plus de place. */
body.gs-fullscreen .topbar,
body.gs-fullscreen .desktop-root-row, /* sélecteur de tonalité en « ligne dédiée » : masqué comme la topbar (Surface a sa tonique dans sa barre) */
body.gs-fullscreen .sidebar,
body.gs-fullscreen .sidebar-desktop,
body.gs-fullscreen .mobile-nav{display:none !important}
/* top:0 en !important : la règle plein écran MOBILE (@media max-width:1023px) pose top:var(--gs-top)
   avec une spécificité comparable, et une règle en media query l'emporte sur celle-ci à spécificité
   égale. Sans !important, la page restait décalée de la hauteur d'une topbar pourtant masquée (mesuré :
   top:113px), laissant une bande vide en haut de l'écran. */
body.gs-fullscreen #page-play.page.active{position:fixed;left:0;right:0;top:0 !important;bottom:0;z-index:60;overflow:hidden}
/* Vrai plein écran SURFACE (demande explicite : "uniquement la surface de jeu, sans menus, avec juste un
   petit bouton pour sortir du fullscreen") — masque le chrome propre à SURFACE le moins essentiel en plein
   jeu (les 2 sous-barres fusionnées dans #gsBarsWrap, tout panneau ouvert), qu'il s'agisse de la grille
   rectangulaire ou hexagonale (même règle pour les 2, aucune ne connaît la géométrie active ici).
   #gsOctRow, LUI, reste désormais VISIBLE en plein écran (demande explicite : "déplacer les boutons latch,
   pitch et octave dans la zone de jeu, à conserver en plein écran" — il porte maintenant Octave ET Latch/
   Pitch, cf. index.html) : seuls #gsBarsWrap/les panneaux disparaissent, #gsOctRow et #gsGridWrap/
   #gsHexGridWrap (lequel des 2 est affiché reste décidé par _gsSyncGridShapeVisibility(), pas par ce bloc)
   plus le bouton de sortie dédié restent visibles. */
body.gs-fullscreen #page-play #gsBarsWrap,
body.gs-fullscreen #page-play .gs-panel:not(#gsSettingsPanel):not(#gsSettingsPanel *){display:none !important}
/* BUG CORRIGÉ (Phase 4, refonte UI/UX SURFACE) — #gsSettingsPanel EXCLU de la règle ci-dessus depuis que le
   HUD (seul accès restant à gsToggleSettingsPanel, cf. .gs-hud-superseded sur gsSettingsBtn) reste visible en
   plein écran : sans cette exception, cliquer ⚙ togglait bien la classe .on du bouton mais le panneau
   lui-même restait forcé à display:none par CETTE règle, plus prioritaire que le style inline posé par
   gsToggleSettingsPanel (mesuré : bouton "actif" visuellement, aucun contenu affiché — la classe .on sans
   effet visible est le symptôme classique d'un !important concurrent). Quand ouvert, il prend le dessus sur
   la grille via son propre z-index (cf. .gs-panel de base) — acceptable ici : consulter les Paramètres
   suspend nécessairement le jeu, contrairement à Latch/Pitch/Octave qui restent, eux, utilisables SANS
   quitter la grille des yeux.
   :not(#gsSettingsPanel *) AJOUTÉ (bug rapporté : "sur mobile en fullscreen, les paramètres ne sont pas
   entiers (accords et synthé n'ont rien dans leur menus)") — #gsChordPanel/#gsParams portent TOUJOURS la
   classe .gs-panel (héritée de leur ancien statut de panneaux dropdown indépendants, cf. index.html) mais
   sont désormais des SECTIONS TOUJOURS DÉPLIÉES à l'intérieur de #gsSettingsPanel (demande explicite plus
   ancienne : "déplacer accords et synthé dans les paramètres") — le commentaire ci-dessus affirmant qu'ils
   "restent HORS plein écran comme avant" est resté écrit après ce déplacement mais était devenu FAUX : ils
   sont maintenant des DESCENDANTS de #gsSettingsPanel, que cette règle masquait quand même puisqu'ils
   matchent individuellement .gs-panel:not(#gsSettingsPanel) (ils NE SONT PAS #gsSettingsPanel eux-mêmes,
   seulement ses enfants). Leur contenu HTML était bien généré (gsRenderChordPanel/gsRenderParams tournent
   normalement), seul l'affichage était coupé — d'où "n'ont rien dans leur menus" alors que les données
   existaient réellement en mémoire. */
/* La barre de jeu et son parent .gs-play-bar restent visibles en plein écran. */
body.gs-fullscreen #page-play .gs-grid-wrap{height:100%}

/* Sur DESKTOP, une règle globale met en colonne tout enfant de .page qui n'est pas le premier :
   `.page > :not(div:first-child):not(.sticky-toolbar) { flex-direction:column }` (@media min-width:1024px).
   La 1re barre y échappe en tant que premier enfant ; la 2e, elle, empilait ses boutons verticalement
   (mesuré : barre de 369px de haut, un bouton par ligne). Le même piège frappe les 2 panneaux, dont la
   grille de curseurs se retrouvait étirée sur toute la largeur. On rétablit donc explicitement ici.
   Piège des règles globales desktop, déjà rencontré ailleurs dans le projet (cf. dev/CLAUDE.md). */
@media(min-width:1024px){
  /* flex:0 0 auto annule le `flex:1` de cette règle globale : c'est LUI qui étirait la barre sur toute la
     hauteur restante (369px mesurés), la mise en colonne ne faisant qu'y répartir les boutons. Seule la
     grille doit absorber la hauteur libre — barres et panneaux gardent leur hauteur naturelle. Même piège
     retrouvé sur la barre d'octave : son parent .gs-play-bar porte désormais sa propre exemption pour
     garder la barre à sa hauteur naturelle, collée à la grille. */
  #page-play .gs-bar2{flex-direction:row;flex:0 0 auto;overflow:visible}
  #page-play .gs-panel{display:block;flex:0 0 auto}
  /* overflow-x reste `auto` (et non `visible` comme .gs-bar2 ci-dessus) : cette rangée porte des contrôles
     de JEU qui doivent tous rester atteignables, y compris sur une fenêtre desktop étroite où elle peut
     déborder — cf. le commentaire de sa règle de base. Seule la mise en ROW (contre le passage en colonne
     de la règle globale) est rétablie ici. */
  #page-play .gs-oct-row{flex-direction:row;flex:0 0 auto;overflow-x:auto;overflow-y:hidden}
  /* Retour utilisateur : "entre hexa et rectangulaire, la disposition des boutons change... pas de saute dans
     l'interface" — les contrôles PROPRES à chaque forme (gsLayoutSelect/gsShapeSelect/gsHexMappingSelect/
     gsHexScaleLockBtn/gsHexSizeSelect/gsGridSizeGroup) n'ont pas la même largeur cumulée d'un mode à l'autre,
     ce qui décalait tout ce qui les suit (Synthé/Rec/Plein écran) de quelques pixels à chaque bascule. On
     ancre donc ce groupe COMMUN (identique dans les 2 modes) à l'extrémité droite de la barre plutôt que de
     le laisser suivre le flux — sa position reste fixe par rapport au bord du conteneur, peu importe la
     largeur de ce qui précède. Seul #gsParamsBtn porte la marge : c'est le premier du groupe qui ne change
     jamais entre rect et hex. */
  #page-play .gs-bar2 #gsParamsBtn{margin-left:auto}
}
@media(max-width:600px){
  /* La 2e barre porte 9 contrôles : mesurée à 448px pour 390px de large, elle débordait de 58px et le
     bouton plein écran demandait un balayage pour être atteint. On la laisse donc REVENIR à LA LIGNE
     (2 rangées serrées) plutôt que de la faire défiler : tout reste visible d'un coup d'œil, au prix d'une
     vingtaine de pixels de hauteur — le bon compromis pour une barre qu'on consulte plus qu'on ne la
     manipule en jouant. Même arbitrage que la subbar Groovebox scindée en 2 sur mobile. */
  #page-play .gs-bar2{gap:3px;padding:2px 3px;flex-wrap:wrap;overflow:visible;row-gap:2px}
  #page-play .gs-bar2 .gs-bar-sep{display:none}   /* les séparateurs n'ont plus de sens une fois réparti */
  #page-play .gs-mode-btn{padding:0 6px;height:24px;font-size:max(10px,.5rem)}
  #page-play .gs-panel{max-height:30vh;padding:5px 6px}
  /* Plus de règle .gs-params-grid ici : les potentiomètres ronds (flex-wrap, cf. règle générale plus haut)
     se reflowent naturellement à toute largeur, contrairement à l'ancienne grille à colonnes de sliders. */
  #page-play .gs-settings-embed{max-height:none;padding:0}
}

/* ── Pulsation du bouton métronome (topbar) ───────────────────────────────────────────────────────────────
   Témoin de battement visible depuis n'importe quel module (cf. _pulseMetroButton). transform + box-shadow
   uniquement : composées par le GPU, aucun reflow par frame — ce bouton bat en permanence pendant qu'on
   joue, il ne doit rien coûter. La classe est retirée à animationend côté JS.
   Le temps 1 est plus marqué que les autres (même logique que l'accent sonore). */
@keyframes metro-beat{
  0%{transform:scale(1.18);box-shadow:0 0 0 0 var(--c-root)}
  100%{transform:scale(1);box-shadow:0 0 0 6px transparent}
}
@keyframes metro-beat1{
  0%{transform:scale(1.3);box-shadow:0 0 0 0 var(--c-root)}
  100%{transform:scale(1);box-shadow:0 0 0 9px transparent}
}
#globalMetroBtn.metro-beat,#gsMetroBtn.metro-beat,#tbeClickBtn.metro-beat{animation:metro-beat .18s ease-out}
#globalMetroBtn.metro-beat1,#gsMetroBtn.metro-beat1,#tbeClickBtn.metro-beat1{animation:metro-beat1 .24s ease-out}
@media(prefers-reduced-motion:reduce){
  /* Sans mouvement : on garde l'information (un battement a lieu) via la seule couleur. */
  @keyframes metro-beat{0%{background:var(--c-root)}100%{background:transparent}}
  @keyframes metro-beat1{0%{background:var(--c-root)}100%{background:transparent}}
}

/* ── Bouton de synchro inter-modules ──────────────────────────────────────────────────────────
   L'état (synchro active/débrayée) se lit maintenant sur le GLYPHE lui-même (chaînons liés/déliés, cf.
   _syncGlobalSyncBtn dans app.js) — plus de surlignage dédié à styler ici (l'ancien contour blanc en mode
   débrayé, .sync-off, a été retiré avec le passage à l'icône SVG). */

/* ── Topbar très étroite : logo réduit pour libérer de la place ────────────────────────────────────────────
   Le logo est un mot-clé graphique de 150px de large (ratio 1600×234), soit 38% de la rangée à 393px — la
   place manquait pour le bouton de synchro, que _syncBtnFitTopbar() masquait alors (mesuré : rangée pleine
   dès 393px, bouton à l'aise seulement à partir de 430px).
   Le réduire à 17px de haut lui fait rendre ~34px de large, ce qui suffit à faire tenir le bouton sur les
   téléphones courants (393-412px) sans rien retirer. En dessous de 380px on le réduit encore : là, la
   rangée déborde déjà par elle-même, indépendamment du bouton de synchro.
   !important : la hauteur est posée en style INLINE sur l'<img> (cf. index.html), qu'une règle de feuille ne
   peut pas contrer autrement. */
@media(max-width:429px){
  .tb1 .logo img{height:21px!important}
}
/* Audit 2026-10-02 : sous 380px, le logo-mot rendait encore 103px de large (ratio réel ~6,8:1, pas les
   ~34px estimés plus haut) et la rangée faisait 369px minimum — la page ENTIÈRE était alors forcée à 369px
   sur les téléphones 320/360px (dézoom ou défilement horizontal sur tous les modules). Les boutons de la
   topbar ne doivent jamais rétrécir (règle plus haut) : c'est le logo qui cède, remplacé par l'icône carrée
   de l'app (26px). overflow-x:clip en filet de sécurité : si la rangée déborde encore (écran < 300px), elle
   se coupe au lieu d'élargir la page — clip (pas hidden) ne crée pas de conteneur de défilement et laisse
   l'axe vertical libre pour les éléments qui dépassent vers le bas. */
@media(max-width:379px){
  .tb1{overflow-x:clip}
  .tb1 .logo img{display:none!important}
  .tb1 .logo::before{content:'';display:block;width:26px;height:26px;border-radius:6px;background:url(icons/icon-192.png) center/contain no-repeat}
}

/* ── SURFACE : boutons d'octave surdimensionnés ────────────────────────────────────────────────────────────
   Ce ne sont pas de simples réglages qu'on clique une fois : ils sont conçus pour être MARTELÉS en jouant
   (octaves montantes/descendantes à plusieurs doigts, demande explicite), donc traités comme des cibles de
   jeu, pas comme des boutons de barre d'outils. D'où une taille très au-delà des 20px de .tbtn — au-dessus
   même du seuil tactile usuel de 44px sur la largeur, la plus sollicitée dans un aller-retour au pouce.
   touch-action:manipulation supprime le délai de 300ms et le zoom au double-tap : sans lui, marteler le
   bouton déclenche un zoom de page au lieu de jouer. user-select/tap-highlight neutralisés pour la même
   raison que les cellules de la grille. */
#page-play .gs-oct-group .tbtn.gs-oct-btn{
  width:52px;height:38px;font-size:1.25rem;font-weight:700;
  touch-action:manipulation;-webkit-user-select:none;user-select:none;
  -webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;
}
#page-play .gs-oct-group .tbtn.gs-oct-btn:active{background:var(--c-root);color:#111;border-color:var(--c-root)}
/* Formats égalisés (demande explicite : "boutons dans la surface de jeu : égaliser les formats") — Latch/
   Pitch/Groove (.gs-mode-btn, cf. sa règle générique de 28px de haut ailleurs dans ce fichier) tranchaient
   nettement avec les boutons d'octave surdimensionnés (38px) une fois rassemblés dans la même rangée
   #gsOctRow : on les aligne ici sur la MÊME hauteur que .gs-oct-btn, sans toucher à la règle .gs-mode-btn
   générique (qui reste 28px partout ailleurs dans l'app, ex. la subbar). Scopé #gsOctRow uniquement. */
#page-play #gsOctRow .gs-mode-btn{height:38px;font-size:.62rem;padding:0 12px}
@media(max-width:600px){
  /* Mobile : on garde une cible large (le geste y est le plus utile) tout en rognant la hauteur, ressource
     rare ici — c'est la largeur qui compte pour un aller-retour au pouce. Latch/Pitch/Groove suivent la
     même hauteur réduite, même raison que la règle desktop ci-dessus (formats égalisés dans #gsOctRow). */
  #page-play .gs-oct-group .tbtn.gs-oct-btn{width:46px;height:32px;font-size:1.1rem}
  #page-play #gsOctRow .gs-mode-btn{height:32px;font-size:max(10px,.56rem);padding:0 9px}
}

/* ── SURFACE : presets utilisateur ─────────────────────────────────────────────────────────────────────────
   Pastilles plutôt qu'une liste déroulante : peu nombreuses par timbre, et un rappel doit se faire d'un
   seul geste quand on cherche un son en jouant. Le × de suppression est volontairement plus petit et
   discret que le nom : on clique 100 fois pour charger, une fois pour supprimer. */
#page-play .gs-preset-row,
#page-step .gs-preset-row{display:flex;flex-wrap:wrap;gap:4px;margin:0 0 6px}
#page-play .gs-preset,
#page-step .gs-preset{display:inline-flex;align-items:stretch;border:1px solid var(--bd);border-radius:6px;overflow:hidden}
#page-play .gs-preset-load,
#page-step .gs-preset-load{
  font-family:var(--fm);font-size:max(10px,.54rem);padding:3px 8px;border:none;
  background:var(--s3);color:var(--tx2);cursor:pointer;white-space:nowrap;max-width:150px;
  overflow:hidden;text-overflow:ellipsis;
}
#page-play .gs-preset-load:hover,
#page-step .gs-preset-load:hover{background:var(--c-root);color:#111}
#page-play .gs-preset-del,
#page-step .gs-preset-del{
  font-family:var(--fm);font-size:max(10px,.54rem);padding:3px 5px;border:none;border-left:1px solid var(--bd);
  background:var(--s3);color:var(--tx3);cursor:pointer;line-height:1;
}
#page-play .gs-preset-del:hover,
#page-step .gs-preset-del:hover{background:#a33;color:#fff}

/* ── SURFACE : barre MIDI compacte ─────────────────────────────────────────────────────────────────────────
   Une seule ligne au lieu du pavé explicatif précédent : ce panneau s'ouvre au-dessus de la surface de jeu,
   chaque ligne y est prise sur la hauteur jouable (demande explicite : « pas de message qui prend de
   l'espace »). Le détail d'installation d'un port virtuel passe en infobulle. */
#page-play .gs-midi-bar{display:flex;align-items:center;flex-wrap:wrap;gap:5px}
/* Espacement entre 2 .gs-midi-bar consécutifs (demande explicite : "un léger espace entre les lignes entrée
   midi/muet local/panique et la ligne aucun périphérique/coupure/apprendre") — gsRenderMidiUi (app.js)
   ferme un 1er .gs-midi-bar puis en ouvre un 2e juste après (2 groupes logiquement distincts : réglages
   MIDI généraux, puis détail de l'entrée MIDI activée) sans aucune marge entre les deux, les 2 lignes se
   touchaient visuellement. */
#page-play .gs-midi-bar+.gs-midi-bar{margin-top:6px}
#page-play .gs-midi-lbl{font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3);white-space:nowrap}
#page-play .gs-midi-note{font-family:var(--fm);font-size:max(10px,.52rem);color:var(--tx3);cursor:help}
#page-play .gs-sel-inline{width:auto;max-width:180px;min-width:0;flex:0 1 auto}
#page-play .gs-sel-narrow{max-width:56px}

/* ── SURFACE : panneau Paramètres (MIDI + affichage + taille de grille regroupés, demande explicite) ── */
/* Zébrage "une ligne sur 2" (demande explicite : "utiliser le même aspect que dans les paramètres globaux
   de f-t pour améliorer la lisibilité") — même patron EXACT que .settings-row des Paramètres globaux
   (cf. son commentaire détaillé, ~ligne 1855) : fond alterné très subtil, débordant jusqu'aux bords via
   margin/padding négatifs compensés, :nth-child(An+B OF S) plutôt qu'un :nth-child simple pour rester
   cohérent même quand un titre de section (.gs-settings-section-title) s'intercale entre 2 groupes de
   réglages sans casser la parité pair/impair. */
#page-play .gs-settings-group{display:flex;align-items:center;flex-wrap:wrap;gap:5px;padding:6px 10px;margin:0 -10px;border-radius:6px}
#page-play .gs-settings-group:nth-child(odd of .gs-settings-group){background:rgba(255,255,255,.025)}
#page-play .gs-settings-group:nth-child(even of .gs-settings-group){background:rgba(255,255,255,.055)}
/* Tiroir "Effets visuels" (#gsFxDrawerPanel) FONDU dans le même style que les lignes de réglage ci-dessus
   (bug rapporté avec capture : "c'est l'encadrement qui vient du menu déroulant [effets visuels] qui bug"
   — le tiroir ressortait comme une carte à part, bordure + coins arrondis, alors que les lignes normales
   n'ont qu'un fond alterné SANS bordure) — .ref-table-panel (styles.css, patron partagé avec les tableaux
   de référence Gammes/Accords) porte border:1px solid + border-radius PAR DÉFAUT, pensé pour un panneau
   VRAIMENT séparé du reste. Neutralisés ICI spécifiquement pour ce tiroir, qui doit au contraire se fondre
   dans la colonne de réglages qui l'entoure — seul le chevron ▴/▾ signale encore qu'on peut le plier/
   déplier, plus aucun cadre visuel distinct. */
#page-play #gsFxDrawerPanel{border:none;background:transparent;border-radius:0}
/* Fond repris de .gs-settings-group (même opacité que le palier "pair", cf. ci-dessus) plutôt que
   transparent : sans lui, le libellé "Effets visuels" flottait nu dans la colonne, seule ligne du panneau
   sans AUCUN fond alterné — cohérent avec le zébrage du reste tout en restant visuellement distinct d'une
   ligne de réglage normale (c'est un TITRE cliquable, pas un curseur). */
#page-play #gsFxDrawerPanel .ref-table-handle{padding:6px 10px;border-radius:6px;background:rgba(255,255,255,.055)}
#page-play #gsFxDrawerPanel .ref-table-body{border-top:none}
#page-play .gs-settings-group>.gs-midi-lbl{flex:0 0 100%;font-size:max(10px,.52rem);text-transform:uppercase;letter-spacing:.03em}
/* Menus déroulants du tiroir "Effets visuels" (demande explicite : "utiliser toute la largeur de la colonne
   pour le menu") — .dpk-select-btn (bouton compagnon généré par dpkAdaptSelect à la place du <select> masqué,
   cf. app.js) reste width:auto par défaut PARTOUT ailleurs dans l'app (dimensionné à son contenu, adapté à
   une barre d'outils où plusieurs contrôles se partagent une ligne) — mais DANS ce panneau Paramètres, où
   .gs-midi-lbl force déjà le label sur sa propre ligne pleine largeur (flex:0 0 100% ci-dessus) et où
   .gs-volume-slider (les curseurs du même tiroir) prend déjà toute la largeur restante, un menu qui reste
   étroit sous son label paraît incohérent avec le reste de la colonne. flex:1 1 auto aligne son comportement
   sur celui des curseurs : il grandit pour occuper toute la largeur disponible de sa ligne.
   Les boutons compagnons des selects "superseded" par le HUD (Tonique/Gamme/Instrument/Poly/Disposition,
   cf. .gs-hud-superseded+.dpk-select-btn{display:none!important}) restent invisibles quoi qu'il arrive : ce
   flex-grow supplémentaire ne change rien pour eux, display:none l'emporte toujours. */
#page-play .gs-settings-group>.dpk-select-btn{flex:1 1 auto;width:auto;min-width:0}
/* Volume SURFACE (cf. #gsVolumeSlider dans index.html) — le curseur prend toute la largeur restante de sa
   ligne, la valeur en % garde une largeur fixe pour que le curseur ne se redimensionne pas à chaque
   changement de chiffre (100% → 99% → 9% ferait sauter la mise en page à chaque pas sinon). */
#page-play .gs-volume-slider{flex:1 1 auto;min-width:0;height:20px;-webkit-appearance:none;appearance:none;
  border:1px solid var(--bd);background:var(--s3);border-radius:4px;accent-color:var(--c-root);cursor:pointer}
#page-play .gs-volume-val{flex:0 0 40px;text-align:right;font-size:.6rem;color:var(--tx2);font-variant-numeric:tabular-nums}
/* Boîte de valeur ÉDITABLE compacte pour Volume (notes)/Volume accords (Phase 5, refonte UI/UX SURFACE —
   demande explicite : "condenser... avec input text box comme pour les zoom de modules"). Même patron que
   .modvis-zoom-val-wrap (Paramètres > Modules) : le "%" est intégré DANS la boîte via ::after plutôt qu'un
   <span> séparé après le champ, pour que le curseur (flex:1, cf. .gs-volume-slider ci-dessus) et cette boîte
   partagent une seule ligne étroite. PAS de margin-right ici (contrairement à .modvis-zoom-val-wrap, qui
   réserve la largeur d'un bouton info absent de ce contexte) : cette boîte est la dernière chose de sa ligne. */
#page-play .gs-vol-val-wrap{position:relative;display:inline-flex;flex-shrink:0}
#page-play .gs-vol-val-wrap::after{content:"%";position:absolute;right:6px;top:50%;transform:translateY(-50%);font-family:var(--fm);font-size:.6rem;color:var(--tx3);pointer-events:none}
/* BUG CORRIGÉ (rapporté avec capture : "100%" affiché tronqué en "10(%") — width:46px laissait trop peu de
   place au texte ("100" + le padding-right de 16px réservé au "%" superposé, cf. ::after ci-dessus) : à
   3 chiffres pleins, le dernier caractère chevauchait déjà le "%" absolu. Aligné sur la largeur du patron
   équivalent .modvis-zoom-val-wrap (Paramètres > Modules, même mécanisme %-intégré-dans-la-boîte) qui
   utilise 52px pour un contenu identique (3 chiffres + %) — jamais tronqué là-bas. */
#page-play .gs-vol-val-wrap .settings-val-input{padding-right:16px;width:52px}
/* Variante "px" (Espacement de la traînée, cf. index.html) — même boîte que .gs-vol-val-wrap (%), suffixe
   différent seulement : "px" est 1 caractère plus large que "%", d'où +4px de large ici et de padding. */
#page-play .gs-vol-val-wrap-px{position:relative;display:inline-flex;flex-shrink:0}
#page-play .gs-vol-val-wrap-px::after{content:"px";position:absolute;right:6px;top:50%;transform:translateY(-50%);font-family:var(--fm);font-size:.6rem;color:var(--tx3);pointer-events:none}
#page-play .gs-vol-val-wrap-px .settings-val-input{padding-right:24px;width:66px}
/* Variante "ms" (Fondu des effets, cf. index.html) — même patron, suffixe 2 caractères comme "px" mais
   jusqu'à 4 chiffres (1200 max, cf. GS_FX_FADE_MAX dans app.js) donc légèrement plus large. */
#page-play .gs-vol-val-wrap-ms{position:relative;display:inline-flex;flex-shrink:0}
#page-play .gs-vol-val-wrap-ms::after{content:"ms";position:absolute;right:6px;top:50%;transform:translateY(-50%);font-family:var(--fm);font-size:.6rem;color:var(--tx3);pointer-events:none}
#page-play .gs-vol-val-wrap-ms .settings-val-input{padding-right:24px;width:72px}
/* Variante SANS suffixe (Ripples simultanés max., cf. index.html) — un simple compte, pas une unité
   physique. Annule le "%" hérité de .gs-vol-val-wrap (posée EN PLUS sur le même élément, pas à la place —
   mêmes dimensions de boîte que .gs-vol-val-wrap sinon, juste le pseudo-élément neutralisé). */
#page-play .gs-vol-val-wrap-plain::after{content:none}
#page-play .gs-vol-val-wrap-plain .settings-val-input{padding-right:6px}
/* Statut MIDI (demande explicite : "status midi dans les paramètres") — remplace l'ancien point ●/○ du HUD
   supprimé. Neutre par défaut (aucun périphérique), accent quand réellement connecté : le texte lui-même
   porte déjà ●/○ (cf. _gsSyncMidiStatus), la couleur ne fait que renforcer la lecture au coup d'œil. */
#page-play .gs-midi-status{font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx3);white-space:normal}
#page-play .gs-midi-status.on{color:var(--c-root)}

/* ── SURFACE : page Paramètres à 6 sections (refonte UI/UX Phase 2, cf. side_dev/dev/
   FRET-THEORY_SURFACE_UIUX_REFACTOR_v2.md §7) — Layout/Play-Touch/MIDI/Appearance (Musical et Data restent
   vides pour l'instant : Musical vit déjà dans le HUD depuis la Phase 1, Data attend la Phase suivante pour
   les presets/import-export). .gs-settings-section réutilise .gs-settings-group EN INTERNE (aucune règle
   dupliquée pour l'espacement des lignes) : seul un titre de section + une séparation plus marquée entre
   sections qu'entre lignes d'une même section sont ajoutés. */
/* BUG CORRIGÉ (rapporté avec screenshot : "beaucoup d'espace inutilisé") — #gsSettingsPanel est un enfant
   NON-PREMIER de #page-play (le HUD, Phase 1, occupe désormais ce rang) : il hérite donc de la règle
   générique ".page > *:not(div:first-child):not(.sticky-toolbar)" (cf. son commentaire en tête de ce
   fichier) qui lui impose width:100%;max-width:1600px — bien plus large que le contenu réel d'un panneau de
   réglages (selects/boutons compacts), d'où le vide flagrant à droite sur un écran large. 2 corrections
   combinées : (1) max-width RÉDUIT ici spécifiquement pour ce panneau (960px, assez pour 2 colonnes de
   réglages sans devenir un mur de texte edge-to-edge) ; (2) .gs-settings-section passe en grille 2 colonnes
   à partir de 700px de large PROPRE au panneau (@container, pas une media query sur la fenêtre — le panneau
   peut être étroit sur un écran large si la sidebar app est ouverte, cf. .page déjà en layout sidebar
   ailleurs dans ce fichier) : Layout+Play/Touch d'un côté, MIDI+Appearance de l'autre, plutôt qu'un long
   déroulé vertical qui n'exploite jamais la largeur gagnée. */
/* PLEINE FENÊTRE (demande explicite : "à l'ouverture des paramètres, déplier la fenêtre en grand pour voir
   tous les paramètres d'un coup, ne pas essayer de préserver la surface de jeu") — contrairement aux 3
   autres .gs-panel (Accords/Synthé/Rec, restés en dropdown compact 26vh de haut, cf. règle générique plus
   haut) : #gsSettingsPanel bascule en overlay PLEIN ÉCRAN quand ouvert, la grille de jeu n'a plus besoin de
   rester visible dessous puisqu'on ne joue pas en même temps qu'on règle les Paramètres. position:fixed
   couvre TOUTE la fenêtre (z-index au-dessus de la topbar/sidebar de l'app, pas seulement de #page-play) ;
   :not([style*="display:none"]) inutile ici car le display:none inline existant (cf. index.html) suffit à
   masquer tout l'overlay quand fermé — la règle ci-dessous ne s'applique qu'à l'état ouvert de fait. */
#page-play .gs-panel#gsSettingsPanel{
  /* ESSAI ABANDONNÉ (garder cette note pour ne pas retenter à l'identique) : remplacer inset:0 par
     top:var(--gs-settings-top) pour laisser la topbar/barre d'onglets visibles pendant que ce panneau est
     ouvert (demande explicite : "la page paramètres recouvre tout le haut de la page... alors que les
     paramètres ne commencent qu'à hauteur de layout/apparence"). Diagnostic exhaustif (aucune propriété de
     containing block trouvée sur la chaîne d'ancêtres — transform/filter/contain/perspective/will-change/
     container-type/zoom tous "none"/"static"/vérifiés vidés) : top calculé CORRECTEMENT par
     getComputedStyle (81px) mais IGNORÉ par le moteur de rendu (getBoundingClientRect().top restait 0)
     — reproductible même avec position/top/left/right/bottom TOUS posés en inline avec la plus haute
     priorité possible. Seul un déplacement du NŒUD DOM lui-même hors de #page-play (vers <body>, même
     patron que .gs-finger-glow/.gs-picker-pop) a fiablement résolu le rendu en test isolé — mais ce
     panneau est stylé via des sélecteurs "#page-play .gs-panel#gsSettingsPanel{...}" sur une dizaine de
     règles : le déplacer casserait tout son style sans une réécriture plus large de ces sélecteurs, un
     chantier reporté (décision explicite : "revenir en arrière pour l'instant, traiter les autres
     demandes"). inset:0 restauré ci-dessous — la topbar reste donc masquée pendant que ce panneau est
     ouvert, comportement d'origine avant cette tentative. */
  position:fixed;inset:0;z-index:500;max-height:none;
  /* margin:0 auto CONSERVÉ (hérité de la règle générique ".page > *:not(div:first-child):not(.sticky-
     toolbar){margin:0 auto}", cf. son commentaire en tête de ce fichier) — RETIRER ce margin (1er essai en
     implémentant "fermer en cliquant en dehors de la fenêtre") a été tenté puis annulé : bug rapporté
     immédiatement après ("elle ne pop pas toujours en place... elle pop à gauche") — measuré : sans ce
     margin:auto, la boîte (limitée à max-width:1100px par .gs-settings-page, MÊME élément, cf. plus bas)
     restait plaquée au bord GAUCHE au lieu de rester centrée après un redimensionnement de fenêtre. Le
     bon fix pour "clic en dehors" n'est donc PAS de désactiver ce centrage (essentiel), mais de calculer la
     zone de contenu réelle par géométrie côté JS plutôt que de supposer que tout le viewport est occupé par
     cet élément — cf. le listener pointerdown dédié dans app.js, qui compare les coordonnées du clic au
     getBoundingClientRect() de .gs-settings-page au lieu de tester e.target===panel. */
  /* flex-direction PAS déclaré ici (volontairement) — cette règle porte 2 sélecteurs d'ID de spécificité
     ((#page-play + #gsSettingsPanel) > #page-play .gs-settings-page), donc toute valeur posée ICI gagnerait
     à coup sûr contre la règle dédiée .gs-settings-page (colonne sur mobile, ligne dès 700px, cf. plus bas)
     — bug vécu : "column" posé ici SANS !important battait quand même flex-direction:row de la media query
     700px, par pure spécificité de sélecteur plus que par ordre de cascade. #gsSettingsPanel ET
     .gs-settings-page sont le MÊME élément (2 classes sur 1 div, cf. index.html) : laisser cette 2e règle,
     plus spécifique au nom, être la seule à toucher flex-direction. align-items:flex-start (pas center) —
     dès 700px (2 colonnes, ligne), "center" centrerait verticalement les 2 colonnes selon leur propre
     hauteur (généralement différente) — mesuré : la colonne la plus courte flottait, décalée vers le bas
     par rapport au haut de l'autre. flex-start aligne les 2 sur leur bord SUPÉRIEUR commun. */
  display:flex!important;align-items:flex-start;
  /* padding-top aligné sur le bas MESURÉ de .topbar (113px, cf. le commentaire de .gs-settings-close) —
     l'overlay couvre bien toute la fenêtre (fond translucide compris, pour masquer la grille), mais son
     CONTENU commence sous la chrome de l'app, sinon les 2 premières lignes de réglages passent dessous
     (rapporté : "le bouton fermer est caché sous les menus (topbar, onglets modules et sélecteur tonalité)"
     — même cause pour le contenu que pour le bouton). */
  /* background OPAQUE, MÊMES COULEURS que le menu Paramètres globaux (demande explicite : "reprends les
     mêmes couleurs que le menu paramètres globaux") — var(--sf), la couleur de surface utilisée par
     .settings-box (le panneau Paramètres global, cf. sa règle plus haut dans ce fichier), pas var(--bg) qui
     est le fond de PAGE (plus sombre, contrastant moins avec .gs-settings-group en dessous). Avant ça,
     var(--bg) pur (suite à une 1re demande "augmenter l'opacité du fond", qui remplaçait elle-même un
     color-mix translucide de l'ancien dropdown compact) : cohérent en opacité mais pas en teinte avec le
     reste de l'app. */
  padding:124px 24px 32px;background:var(--sf);
  /* 124px pensé pour le MOBILE (topbar empilée + onglets + sélecteur de tonalité, cf. le commentaire
     ci-dessus) — sur desktop (≥1150px), la topbar tient sur une seule ligne fine (mesuré : 81px de haut,
     cf. .topbar), laissant un vide inutile au-dessus de Layout/Appearance (rapporté avec capture : "elle
     commence en haut de page et masque les subbars inutilement... elle peut commencer juste au-dessus de
     layout/appearance"). Override desktop juste en dessous, dans son propre @media — le close button
     (padding-top MIROIR obligatoire, cf. son commentaire) et .gs-settings-page (même élément, hérite du
     même padding) suivent automatiquement. */
  /* height:100% CONSERVÉ (cf. son historique — correctif d'un bug de débordement antérieur). L'essai de le
     remplacer par height:auto (pour laisser top+bottom seuls déterminer la hauteur) n'a PAS résolu le vrai
     problème sous-jacent : même avec top calculé correctement à 81px (vérifié par getComputedStyle), le
     panneau restait rendu à top:0 quelle que soit la valeur de `height` — symptôme d'un containing block
     qui n'était PAS le viewport malgré une vérification exhaustive de tous les déclencheurs CSS connus
     (transform/filter/contain/perspective/will-change/container-type/zoom sur toute la chaîne d'ancêtres,
     tous "none"/"static"). Seul un déplacement du NŒUD DOM lui-même hors de #page-play (cf. gsToggleSettingsPanel,
     app.js — même patron que .gs-finger-glow/.gs-picker-pop, déjà déplacés en enfants de <body> pour la
     même classe de problème) a fiablement corrigé le rendu — le vrai fix n'est donc pas dans CE bloc CSS. */
  height:100%;
  overflow-y:auto;
}
/* APERÇU TRANSLUCIDE (demande explicite : "paramètres qui changent l'aspect de la surface de jeu (décalage
   demi-tons, lignes/colonnes) : rendre la fenêtre paramètres translucide pour permettre un aperçu, comme
   pour les zooms de modules individuels dans les paramètres globaux") — même valeur EXACTE que
   .settings-box-preview (Paramètres globaux, cf. sa règle plus haut dans ce fichier) : color-mix(in srgb,
   var(--sf) 20%,transparent), pour rester visuellement cohérent avec ce mécanisme déjà établi ailleurs dans
   l'app plutôt que d'inventer une 2e valeur de transparence. Classe posée/retirée par
   _gsSettingsPreviewStart/End (app.js) au pointerdown sur les selects Lignes/Colonnes et Décalage
   (demi-tons), PAS en permanence : contrairement au menu Paramètres global qui reste un OVERLAY par-dessus
   le module, ce panneau masque ENTIÈREMENT la grille de jeu en temps normal (cf. sa règle juste au-dessus),
   la transparence ne doit donc s'activer que pendant l'ajustement actif d'un réglage qui a un impact visuel
   sur la grille. Sélecteur de même spécificité que la règle ci-dessus (#page-play .gs-panel#gsSettingsPanel),
   déclaré APRÈS elle dans le fichier : gagne par ordre de cascade à spécificité égale. */
#page-play .gs-panel#gsSettingsPanel.gs-settings-preview{
  background:color-mix(in srgb,var(--sf) 20%,transparent);
}
/* Ligne EN COURS D'AJUSTEMENT exemptée de la baisse d'opacité (demande explicite : "toute la fenêtre
   paramètres doit se mettre en transparence, sauf le slider et l'input box concernés par le paramètre qu'on
   est en train de modifier") — même principe que .zoom-preview-active des Paramètres globaux (cf. son
   historique) : le reste du panneau plonge à faible opacité pour laisser voir la grille à travers, MAIS la
   ligne qu'on manipule reste PLEINEMENT lisible, avec son propre fond opaque (sinon on perdrait de vue la
   valeur qu'on est justement en train de choisir — contre-productif). .gs-settings-preview-active posée par
   _gsSettingsPreviewStart (app.js) sur le .gs-settings-group concerné à chaque pointerdown sur son slider. */
#page-play .gs-panel#gsSettingsPanel.gs-settings-preview .gs-settings-group:not(.gs-settings-preview-active){opacity:.35}
#page-play .gs-panel#gsSettingsPanel.gs-settings-preview .gs-settings-group.gs-settings-preview-active{
  position:relative;background:var(--sf);border-radius:6px;box-shadow:0 0 0 1px var(--bd);
}
/* display:flex!important ci-dessus court-circuite le display:none inline posé par gsToggleSettingsPanel —
   la VISIBILITÉ réelle de l'overlay est donc reprise ici via une classe dédiée plutôt que l'ancien style
   inline, cf. gsToggleSettingsPanel (app.js) qui bascule désormais .gs-settings-open au lieu de style.display
   directement pour ce panneau précis (les 3 autres .gs-panel gardent l'ancien mécanisme inchangé). */
#page-play .gs-panel#gsSettingsPanel:not(.gs-settings-open){display:none!important}
/* align-self:stretch + les sections en largeur pleine — l'overlay parent est en flex-direction:column avec
   align-items:center (pour CENTRER la carte de 960px dans la fenêtre) ; sans ces 2 déclarations, ce
   centrage se propageait à chaque .gs-settings-section, chacune se centrant sur SA propre largeur de
   contenu : Layout démarrait à ~305px, MIDI à ~57px, Appearance à ~325px — d'où l'impression de désordre
   rapportée ("rien n'est ordonné dans les paramètres") alors même que l'ordre vertical était correct.
   Tout est désormais aligné sur un bord gauche commun. */
/* max-width élargi à 1100px (contre 960px pour la colonne unique précédente) — 2 colonnes de contenu ont
   besoin de plus de largeur totale pour ne pas se retrouver comprimées l'une contre l'autre. */
/* position:relative RETIRÉ ICI (bug trouvé en implémentant "fermer en cliquant en dehors de la fenêtre") —
   #gsSettingsPanel ET .gs-settings-page sont le MÊME élément (cf. le commentaire plus haut sur cette
   convention) : cette règle, à spécificité de sélecteur ÉGALE à "#page-play .gs-panel#gsSettingsPanel"
   ci-dessus mais placée APRÈS elle dans la cascade, gagnait sur `position:fixed;inset:0` posé là-bas — MÊME
   piège que celui déjà documenté pour flex-direction juste au-dessus. Conséquence mesurée : la boîte
   redevenait `position:relative`, sa largeur RÉELLE retombant à max-width:1100px centré par align-self, au
   lieu du plein viewport attendu — un clic hors de ces 1100px (mesuré : x=1850 sur 1920px) retombait donc
   directement sur la grille de jeu SOUS l'overlay plutôt que sur le fond du panneau, empêchant toute
   fermeture par clic extérieur. position non déclarée ici : le panneau garde le fixed;inset:0 d'origine (son
   fond couvre bien tout le viewport), seule .gs-settings-page (styles.css, largeur/alignement du CONTENU à
   l'intérieur) reste limitée à 1100px. */
#page-play .gs-settings-page{overflow-y:visible;max-width:1100px;width:100%;align-self:center;text-align:left}
#page-play .gs-settings-section{width:100%}
/* 2 COLONNES EXPLICITES (demande explicite : "2 colonnes seront plus lisibles qu'une seule", cf. le
   commentaire détaillé dans index.html sur pourquoi un découpage HTML plutôt qu'une astuce CSS pure) —
   display:flex sur .gs-settings-page (qui N'EST PLUS elle-même en column, cf. la règle ci-dessus qui ne le
   déclare pas) fait de #gsSettingsPanel > .gs-settings-page un simple conteneur flex ROW pour ses 2 enfants
   directs .gs-settings-col, chacun un flux vertical indépendant. gap remplace visuellement l'ancien
   column-gap. */
#page-play .gs-settings-page{display:flex;flex-direction:column;gap:0}
#page-play .gs-settings-col{flex:1 1 0;min-width:0;display:flex;flex-direction:column}
@media(min-width:700px){
  #page-play .gs-settings-page{flex-direction:row;gap:32px}
}
/* BUG CORRIGÉ (rapporté : "impossible d'en sortir sur mobile, le bouton fermer est caché sous les menus
   (topbar, onglets modules et sélecteur tonalité)") — ce bouton était position:absolute dans
   .gs-settings-page, donc placé à ~32px du haut de la fenêtre (le padding de l'overlay) : pile sous la
   topbar/les onglets de l'app. Corrigé une 1re fois en position:fixed ancré au viewport (au-dessus de toute
   la chrome d'app) — puis RETOUR ARRIÈRE suite à une 2e demande explicite : "bouton fermer dans la surface
   du menu, pas en dehors" — le bouton flottait alors effectivement au-dessus de tout, mais visuellement
   DÉTACHÉ du panneau lui-même (au-dessus de son bord arrondi), pas perçu comme un contrôle DU panneau.
   Repositionné en absolute PAR RAPPORT À .gs-settings-page elle-même (position:relative posée sur elle,
   cf. sa règle plus haut) plutôt qu'au viewport : ancré au coin haut-droite du CONTENU (qui commence déjà
   sous la chrome d'app grâce au padding-top de l'overlay, cf. la règle de #gsSettingsPanel), toujours visible
   à l'ouverture, sans dépendre du scroll ni sortir jamais de la surface visible du panneau. */
/* BUG CORRIGÉ (rapporté avec capture : le bouton restait au-dessus du panneau, chevauchant la topbar de
   l'app, malgré le passage à position:absolute) — .gs-settings-page ET #gsSettingsPanel sont le MÊME
   élément (2 classes sur 1 div, cf. index.html) : top:0 sur un enfant position:absolute se positionne au
   bord du PADDING-BOX de son parent, jamais après le padding-top de 124px de ce même parent (cf. sa règle)
   — un enfant absolument positionné ignore le padding de son ancêtre de référence par définition CSS. top
   remplacé par le MÊME décalage que le padding-top du panneau, pour que le bouton apparaisse au ras du haut
   du CONTENU réel plutôt qu'au ras du haut de la boîte (qui, lui, remonte sous la chrome de l'app). */
/* right:0 → right:8px (bug rapporté avec capture : le bouton dépasse de la page Paramètres, à moitié coupé
   par le bord de l'écran) — right:0 collait le bord DROIT du bouton exactement sur le bord droit du panneau
   (lui-même souvent calé sur le bord de l'écran, cf. .gs-settings-page en pleine largeur sous 700px), sans
   aucune marge de respiration ; sur un écran étroit, cela plaçait visuellement plus de la moitié du cercle
   hors de la zone tactile confortable. 8px de marge, cohérent avec le padding horizontal du panneau
   (padding:124px 24px 32px, cf. #gsSettingsPanel) sans pour autant s'aligner pile sur ce padding (qui
   laisserait le bouton trop loin du coin). */
#page-play .gs-settings-close{
  position:absolute;top:124px;right:8px;z-index:2;
  width:40px;height:40px;min-width:40px;min-height:40px;border-radius:999px;
  background:var(--c-root);border:1px solid var(--c-root);color:#111;font-size:1rem;cursor:pointer;
  display:flex;align-items:center;justify-content:center;transition:all var(--tr);
}
#page-play .gs-settings-close:hover{filter:brightness(1.12)}
/* padding-top réduit sur DESKTOP (≥1150px, seuil topbar 1 ligne de l'app — cf. DESKTOP_BREAKPOINT en tête de
   ce fichier) — rapporté avec capture : "elle commence en haut de page et masque les subbars inutilement...
   elle peut commencer juste au-dessus de layout/appearance". 124px (posé sur la règle de base plus haut,
   pensé pour la topbar EMPILÉE mobile) laissait ~43px de vide au-dessus du contenu sur desktop, où la topbar
   mesure 81px de haut sur une seule ligne (mesuré). 96px choisi avec une marge de respiration au-dessus de ce
   81px plutôt que collé pile dessus. .gs-settings-close (bouton fermer) suit ce même décalage — cf. son
   propre commentaire plus haut sur pourquoi son `top` est un miroir obligatoire du padding-top du panneau.
   PLACÉ ICI, APRÈS les 2 règles de base (#gsSettingsPanel/.gs-settings-close) — bug trouvé en implémentant ce
   correctif : posé PLUS TÔT dans le fichier (juste après le bloc @media(min-width:700px) au-dessus), cette
   même règle perdait silencieusement contre la règle de base de .gs-settings-close (spécificité ÉGALE, mais
   déclarée APRÈS dans la cascade) — .gs-settings-close restait bloqué à 124px malgré le media query actif,
   seul le padding du panneau lui-même changeait réellement. Media query ou pas, l'ordre de cascade prime à
   spécificité égale : cette règle doit venir après TOUT ce qu'elle doit battre. */
@media(min-width:1024px){
  #page-play .gs-panel#gsSettingsPanel{padding-top:96px}
  #page-play .gs-settings-close{top:96px}
}
#page-play .gs-settings-section{margin-bottom:14px}
#page-play .gs-settings-section+.gs-settings-section{padding-top:14px;border-top:2px solid var(--bd)}
#page-play .gs-settings-section-title{font-family:var(--fm);font-size:max(10px,.56rem);font-weight:700;color:var(--c-root);text-transform:uppercase;letter-spacing:.05em;margin-bottom:2px}
/* Historique : 2 tentatives de disposition en 2 colonnes avaient échoué avant la version actuelle
   (.gs-settings-col, 2 <div> explicites, cf. index.html) — CSS Grid (grid-auto-flow:column) partage la
   hauteur de rangée entre colonnes, et les colonnes CSS (column-count) coupaient les sections en 2 malgré
   break-inside:avoid. Repassé un temps en colonne unique le temps de confirmer que l'ORDRE et
   l'ALIGNEMENT étaient corrects, avant de rétablir 2 colonnes par découpage HTML explicite (fiable, cf.
   règles ci-dessus) suite au retour "2 colonnes seront plus lisibles qu'une seule". */

/* ── SURFACE : bouton compact + popover custom pour Progression/Presets (remplace un <select> natif qui ne
   respecte pas toujours le plein écran et s'étire sur son option la plus longue, demande explicite). Le
   popover est ajouté à document.body (pas au panneau) pour ne jamais être coupé par le overflow:auto/
   max-height du panneau parent — cf. _gsOpenPickerPopover, qui le positionne lui-même en fixed. */
.gs-picker-btn{white-space:nowrap}
.gs-picker-pop{
  /* z-index:1500, PAS 500 (bug rapporté : "s'affiche SOUS la sidebar sur desktop") — .sidebar-desktop est en
     position:fixed;z-index:999, donc dans la MÊME couche d'empilement que ce popover (tous deux enfants
     directs de body, aucun ancêtre commun n'isole de contexte) : 500 perdait face à 999 dès qu'ils se
     chevauchaient visuellement. 1500 dépasse confortablement toute la sidebar/topbar (max 1001 dans ce
     fichier) sans entrer en conflit avec les VRAIS calques modaux du projet (6999+, cf. .settings-modal). */
  position:fixed;z-index:1500;background-color:var(--sf,#1c1c26);border:1px solid var(--bd2);border-radius:8px;
  box-shadow:0 8px 28px rgba(0,0,0,.55),0 0 0 1px var(--bd);overflow-y:auto;padding:4px;
  opacity:1;-webkit-backdrop-filter:none;backdrop-filter:none;
}
.gs-picker-group-lbl{
  font-family:var(--fm);font-size:max(10px,.5rem);text-transform:uppercase;letter-spacing:.03em;color:var(--tx3);
  padding:5px 8px 2px;
}
/* 2 colonnes (demande explicite : "menu rapide en appui long sur sélecteur de tonalité : utiliser 2
   colonnes, gauche : gammes, droite : accords") — découpage HTML explicite par _gsOpenPickerPopover
   (twoCol), pas de CSS Grid/column-count ici : même choix déjà tranché ailleurs dans le fichier pour un
   besoin similaire (cf. .gs-settings-col, historique en commentaire plus haut). La largeur elle-même
   (280px→480px pour ce mode) est posée en JS via pop.style.width (cf. _gsOpenPickerPopover, maxW) — la
   classe .gs-picker-pop-2col ne porte donc aucune règle de largeur ici, seul le sous-conteneur .gs-picker-
   cols organise les 2 groupes en colonnes flex ; chaque .gs-picker-col scrolle sa propre hauteur
   indépendamment si une liste est plus longue que l'autre. */
.gs-picker-cols{display:flex;gap:4px}
.gs-picker-col{flex:1;min-width:0;display:flex;flex-direction:column}
.gs-picker-col+.gs-picker-col{border-left:1px solid var(--bd);padding-left:4px}
.gs-picker-item{
  display:block;width:100%;text-align:left;background:none;border:none;color:var(--tx);
  font-family:var(--fm);font-size:.62rem;padding:7px 8px;border-radius:5px;cursor:pointer;
}
.gs-picker-item:hover,.gs-picker-item:focus-visible{background:var(--s3)}
/* Entrée reflétant l'état ACTUELLEMENT sélectionné (demande explicite : "j'aimerais qu'il reflète l'état de
   gamme/accords, highlight sur l'entrée actuellement sélectionnée" — menu rapide tonalité, appui long sur
   une pastille) — posée par _gsOpenPickerPopover via son paramètre currentValue, optionnel et
   rétrocompatible (tout appelant existant qui ne le passe pas garde un popover sans aucune entrée active,
   comportement inchangé). Gras + couleur d'accent, même vocabulaire que .rbtn.active plutôt qu'un nouveau
   style dédié. --c-root (pas une nouvelle variable) : c'est déjà la couleur d'accent "état actif" utilisée
   partout ailleurs dans le projet (ex. .mixer-channel-label.playing). */
.gs-picker-item.active{font-weight:700;color:var(--c-root)}
.gs-picker-item.gs-disabled,.gs-picker-item:disabled{opacity:.35;cursor:not-allowed;pointer-events:none}
/* Action destructive dans un menu (ex. « Supprimer la page », menu de page de SURFACE) : en rouge. */
.gs-picker-item.gs-picker-danger{color:#e5484d}
.gs-picker-item.gs-picker-danger:hover,.gs-picker-item.gs-picker-danger:focus-visible{background:color-mix(in srgb,#e5484d 18%,var(--s3))}
.gs-picker-empty{font-family:var(--fm);font-size:max(10px,.58rem);color:var(--tx3);padding:8px}

/* .gs-arp-row/.gs-arp-lbl/.gs-arp-val : structure interne (label+contrôle) partagée par le panneau ARP
   ANCRÉ (#gsArpPinnedRect/Hex, cf. plus bas) — le popover FLOTTANT qui utilisait aussi ces classes
   (.gs-arp-panel) a été retiré (demande explicite : "appui long sur arp... plus besoin de celui là"),
   remplacé entièrement par le panneau ancré depuis _gsArpBtnBind. */
.gs-arp-row{display:flex;align-items:center;gap:6px}
.gs-arp-lbl{font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx3);width:52px;flex-shrink:0}
.gs-arp-row select{flex:1;min-width:0}
.gs-arp-row input[type="range"]{flex:1;min-width:0}
.gs-arp-val{font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx3);width:32px;text-align:right;flex-shrink:0}
/* Panneau ARP ANCRÉ (demande explicite : "panneau ARP toujours visible à côté de la surface de jeu") — même
   réutilise .gs-arp-row/.gs-arp-lbl/.gs-arp-val (structure interne label+contrôle) — position:relative en
   flex-item de .gs-grid-wrap (comme .gs-pitchwheel, cf. son commentaire dans index.html sur pourquoi un
   flex-item plutôt qu'un absolute qui flotterait par-dessus la grille), désormais le SEUL panneau ARP (le
   popover flottant qui existait avant a été retiré, demande explicite ci-dessus). Largeur fixe assez large
   pour les 2 <select> (Rate/Type) sans les comprimer au point d'être illisibles. */
#page-play .gs-arp-pinned{
  flex:none;width:150px;align-self:stretch;margin-left:6px;
  background:var(--s2,rgba(255,255,255,.06));border:1px solid var(--bd2);border-radius:8px;
  padding:8px;display:flex;flex-direction:column;gap:6px;overflow-y:auto;
}
/* Tiroir de bibliothèque (surface libre, demande explicite : "une bibliothèque de contrôleurs") — même
   patron flex-item que .gs-arp-pinned juste au-dessus (largeur fixe, la grille flex:1 rétrécit d'autant),
   visible seulement en mode édition (cf. gsToggleFuncEditMode, app.js). */
#page-play .gs-func-free-drawer{
  flex:none;width:110px;align-self:stretch;margin-left:6px;
  background:var(--s2,rgba(255,255,255,.06));border:1px solid var(--bd2);border-radius:8px;
  padding:8px;display:flex;flex-direction:column;gap:6px;overflow-y:auto;
}
#page-play .gs-func-free-drawer-item{
  padding:8px 4px;border-radius:6px;border:1px dashed var(--c-root);
  background:var(--s1);color:var(--tx2);font-family:var(--fm);font-size:.6rem;font-weight:700;
  text-align:center;cursor:grab;touch-action:none;user-select:none;-webkit-user-select:none;
}
/* Bascule Grille/Libre (demande explicite, confirmée : "2 zones de dépôt séparées... une bascule Grille/
   Libre en haut du tiroir qui change le comportement de TOUTES les icônes en dessous") — cf.
   gsSetFuncFreeDrawerTarget/app.js. Choisit AVANT de glisser si le dépôt remplace des cases de la grille ou
   pose un widget en position libre par-dessus, les 2 destinations étant visuellement superposées (aucun
   moyen de déduire l'intention depuis le seul point de relâchement). */
#page-play .gs-func-free-drawer-toggle{display:flex;gap:3px;margin-bottom:2px}
#page-play .gs-func-free-drawer-toggle button{
  flex:1;padding:4px 2px;border-radius:5px;border:1px solid var(--bd2);
  background:var(--s2);color:var(--tx2);font-family:var(--fm);font-size:max(10px,.5rem);font-weight:700;
  cursor:pointer;
}
#page-play .gs-func-free-drawer-toggle button.on{background:var(--c-root);border-color:var(--c-root);color:#111}
/* Fantôme suivi pendant le glisser DEPUIS le tiroir (cf. _gsFuncFreeDrawerDragStart, app.js) — position:fixed
   centré SOUS le doigt (translate -50%,-50%), au-dessus de tout (z-index élevé, doit rester visible même
   par-dessus le tiroir/la grille pendant le geste). pointer-events:none : ne doit jamais lui-même intercepter
   le pointermove/pointerup destinés au geste en cours. */
#page-play .gs-func-free-drawer-ghost{
  position:fixed;z-index:2000;transform:translate(-50%,-50%);
  padding:8px 14px;border-radius:6px;border:1px solid var(--c-root);
  background:var(--c-root);color:#111;font-family:var(--fm);font-size:.62rem;font-weight:700;
  pointer-events:none;opacity:.9;
}

/* ── Variante B du picker (dpkOpenList, cf. app.js) : liste + défilement au maintien, mêmes classes visuelles
   que le dropdown classique (.gs-picker-pop/.gs-picker-item), seuls quelques ajustements pour le maintien
   tactile (touch-action:none, item non-<button> donc cursor explicite) et le surlignage de survol pendant
   le glisser (dpk-list-hover, DISTINCT de :hover — actif aussi tant que le doigt reste posé sans bouger). */
.dpk-list-pop{touch-action:none}
/* Entrées ESTOMPÉES par défaut, l'entrée retenue en pleine opacité — même principe de contraste que le
   picker roue (.dpk-row/.dpk-row.dpk-centered ci-dessous), aligné sur demande explicite : « peux-tu adapter
   le style (highlight couleur d'accent) du premier au second ». Le fond plein couleur d'accent précédent
   était plus lourd (un pavé opaque qui masquait la lecture des entrées voisines, et rendait le libellé
   difficile à lire en couleur inversée) ; ici c'est le CONTRASTE qui désigne l'entrée active, la couleur
   d'accent restant un marqueur (texte + liseré) plutôt qu'un aplat. */
.dpk-list-item{
  cursor:pointer;-webkit-user-select:none;user-select:none;
  opacity:.55;transition:opacity .1s,color .1s;
  border-left:3px solid transparent;
}
.dpk-list-item.dpk-list-hover{
  opacity:1;color:var(--c-root);font-weight:700;
  border-left-color:var(--c-root);background:var(--s3);
}
/* Entrée DÉJÀ sélectionnée (point de départ) — un simple point d'accent, pas un surlignage : elle ne doit
   jamais entrer en concurrence visuelle avec .dpk-list-hover, qui indique ce qu'on choisirait en relâchant.
   Légèrement plus lisible que les autres entrées estompées, pour rester repérable pendant le défilement. */
.dpk-list-item.dpk-list-current{opacity:.8}
.dpk-list-item.dpk-list-current::after{
  content:'';width:5px;height:5px;border-radius:50%;background:var(--c-root);
  margin-left:auto;flex:0 0 auto;
}
.dpk-list-item.dpk-list-current{display:flex;align-items:center;gap:6px}

/* ── Composant PARTAGÉ dpk (dropdown picker press-drag-release) ────────────────────────────────────────────
   Pas scopé à un module (contrairement au bloc .gs-* ci-dessus) : utilisé par SURFACE ET le sélecteur de
   tonalité, cf. dpkOpen() dans app.js. NON scopé sous #page-play à dessein. */
.dpk-wrap{
  /* z-index:1500, même raison que .gs-picker-pop juste au-dessus (sidebar-desktop en fixed;z-index:999). */
  position:fixed;z-index:1500;overflow:hidden;background:var(--s2);border:1px solid var(--bd);
  border-radius:8px;box-shadow:0 6px 20px var(--sh);touch-action:none;
}
.dpk-list{position:absolute;left:0;right:0;top:0;will-change:transform}
.dpk-row{
  height:38px;display:flex;align-items:center;padding:0 10px;font-family:var(--fm);font-size:.68rem;
  color:var(--tx3);box-sizing:border-box;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  opacity:.55;transition:opacity .1s,color .1s;
  /* Liseré transparent réservé sur TOUTES les lignes : sans lui, l'apparition du liseré de 3px sur la ligne
     active décalerait son texte à chaque changement de ligne — un frémissement permanent au défilement. */
  border-left:3px solid transparent;
}
/* Ligne active de la roue — MÊME traitement que .dpk-list-item.dpk-list-hover du picker liste (demande :
   « menu synthé : prendre le style du menu gammes ») : couleur d'accent + liseré gauche + fond légèrement
   relevé, au lieu du simple texte blanc en gras. Les 2 composants deviennent ainsi lisibles de la même
   façon, ce qui compte d'autant plus qu'ils s'ouvrent sur le même geste depuis la même barre. */
.dpk-row.dpk-centered{
  color:var(--c-root);opacity:1;font-weight:700;
  background:var(--s3);border-left-color:var(--c-root);
}
/* Entrée DÉJÀ sélectionnée dans la roue — même rôle et même traitement que .dpk-list-current dans la
   variante liste (cf. son commentaire) : un point d'accent en bout de ligne, jamais un surlignage qui
   rivaliserait avec .dpk-centered. Les deux pickers partagent ainsi le même vocabulaire visuel. */
.dpk-row.dpk-row-current{opacity:.8}
.dpk-row.dpk-row-current::after{
  content:'';width:5px;height:5px;border-radius:50%;background:var(--c-root);
  margin-left:auto;flex:0 0 auto;
}
.dpk-row.dpk-centered.dpk-row-current{opacity:1}
/* État "on relâche = rien ne change" (écart latéral > DPK_CANCEL_DX, cf. app.js) — commun aux 2 pickers.
   Retour visuel indispensable : sans lui, l'annulation serait un geste invisible dont on ne saurait jamais
   s'il est armé avant d'avoir déjà relâché. Estompage + désaturation : le menu recule visuellement, ce qui
   se lit immédiatement comme "inactif", sans ajouter de texte ni d'icône à l'écran. */
.dpk-wrap.dpk-cancelling,.dpk-list-pop.dpk-cancelling{opacity:.4;filter:grayscale(1)}
/* Hauteur IDENTIQUE aux lignes normales (38px, cf. .dpk-row et DPK_ROW_H côté JS) — surtout PAS 22px.
   BUG CORRIGÉ : tout le calcul de la roue (offsetY, centerIdx, minOffset/maxOffset) suppose une hauteur
   UNIFORME de DPK_ROW_H pour TOUTES les lignes, séparateurs compris. Avec des séparateurs plus courts, la
   position réelle à l'écran dérivait de 16px par groupe traversé : la ligne effectivement centrée n'était
   plus celle que le calcul désignait, et sur une liste à plusieurs groupes (les timbres en ont 4+) le
   décalage cumulé faisait tomber le relâchement sur un séparateur, donc sur une sélection ratée.
   Le libellé reste visuellement distinct (petite capitale estompée, aligné en bas de sa ligne) sans avoir
   besoin d'une hauteur différente. */
.dpk-row.dpk-sep{
  color:var(--tx3);opacity:.4;font-size:max(10px,.5rem);text-transform:uppercase;letter-spacing:.03em;
  pointer-events:none;align-items:flex-end;padding-bottom:4px;
}
/* touch-action:none sur le bouton DÉCLENCHEUR lui-même (bug rapporté : "dès que mon doigt sort de la zone
   du bouton, le menu se referme") — sans ça, le navigateur peut engager son propre geste de scroll/retour
   dès que le doigt quitte la zone du bouton d'origine, avant même que setPointerCapture (cf. dpkAdaptSelect
   dans app.js) n'ait eu l'occasion de router l'événement ailleurs. Les 2 correctifs sont complémentaires :
   celui-ci empêche le navigateur d'intercepter le geste, setPointerCapture garantit que le bouton continue
   de recevoir pointermove/pointerup quel que soit l'élément survolé une fois le geste en cours. */
/* BUG CORRIGÉ (rapporté avec screenshot : "les dropdowns de groovebox sont cassés", texte tronqué/superposé
   illisible façon "TR-8...") — .dpk-select-btn hérite de .tbtn (width/height:20px FIXES, pensé pour une
   icône d'1 caractère, cf. sa définition), et le VRAI assouplissement de taille (width:auto, min-width:0,
   padding, white-space:nowrap) vit uniquement dans une règle scopée "#page-play .gs-mode-btn" — jamais
   appliquée hors de SURFACE. Les boutons dpk de la Groovebox (ajoutés ensuite, demande explicite : "mettre
   à jour le style des menus déroulants dans groovebox, utiliser ceux de surface/instrument") héritaient donc
   du gabarit 20×20px SANS l'assouplissement, un libellé complet ("Synthèse (défaut) ▾") s'écrasant dans une
   boîte bien trop petite pour lui, débordant sur le bouton voisin. Fix : poser l'assouplissement ICI, sur
   .dpk-select-btn lui-même (non scopé à une page), pour que tout futur usage de dpkAdaptSelect en bénéficie
   par défaut plutôt que de redécouvrir ce même bug ailleurs.
   2e BUG CORRIGÉ (rapporté ensuite : "le style des dropdowns dans groovebox n'est pas encore le même que
   dans surface (taille du bouton)") — font-size manquait de ce 1er correctif : "#page-play .gs-mode-btn"
   pose .58rem, jamais repris ici, donc les boutons Groovebox retombaient sur le .8rem de base de .tbtn —
   nettement plus grand (mesuré : 20.8px rendus contre 15-16px sur les boutons Surface équivalents), d'où un
   gabarit visuellement différent malgré une hauteur/un padding déjà identiques. Ajouté ici. */
.dpk-select-btn{touch-action:none;width:auto;min-width:0;height:28px;padding:0 9px;font-size:max(10px,.58rem);white-space:nowrap;flex-shrink:0}
/* #gsHudRoot/#gsHudKey/#gsHudVoice (HUD SURFACE) : boutons .tbtn.gs-mode-btn écrits en dur dans le HTML,
   jamais générés par dpkAdaptSelect, donc jamais couverts par la règle .dpk-select-btn ci-dessus — même
   bug, même cause (touch-action par défaut = auto, le navigateur engage son propre scroll de #gsOctRow,
   qui est scrollable horizontalement en permanence, dès que le doigt bouge, avant que setPointerCapture
   n'ait pu agir). Corrigé ici plutôt que sur tout .gs-mode-btn (qui couvrirait aussi gsSlideSelect, déjà
   protégé via .dpk-select-btn). */
#gsHudRoot,#gsHudKey,#gsHudVoice{touch-action:none}
/* #trkGroup (Track + son select Audio/MIDI, module Gammes) : .pg impose height:26px fixe (pensé pour ses
   propres boutons .pill de 26px, cf. .pg) — le .dpk-select-btn généré pour #trkSourceSelect y déborderait
   sinon de 2px (28px de haut dans une boîte de 26px). Réduit ICI seulement (pas globalement sur
   .dpk-select-btn, qui doit rester à 28px partout ailleurs, hors d'un groupe .pg). */
#trkGroup .dpk-select-btn{height:22px}
/* La source est proposée pendant l'écoute ; couvrir aussi le bouton du menu adapté. */
#trkGroup:not(.trk-source-visible) #trkSourceSelect,
#trkGroup:not(.trk-source-visible) .dpk-select-btn{display:none!important}
/* #diatonicPanel (barre "Accords diatoniques" — module Gammes) : BUG CORRIGÉ (rapporté : "accords
   complets/triades et vitesse d'arpège n'ont pas la même taille" — le bouton compagnon .dpk-select-btn
   généré pour #scaleArpSpeedBtn (dpkAdaptSelect masque le <select> réel et insère ce bouton à côté, cf.
   son commentaire) ignore tout style inline posé sur le <select> masqué (height:22px/padding:0 6px/
   font-size:max(10px,.5rem), cf. app.js buildDiatonicPanel) — seule la règle générique .dpk-select-btn (28px/9px/
   .58rem) s'applique réellement, d'où l'écart visible avec le bouton voisin #diatVoicingModeBtn (styles
   inline, eux, bien appliqués). Même patron déjà utilisé pour #trkGroup ci-dessus : correctif scopé à ce
   conteneur précis, pas une modification globale de .dpk-select-btn (qui doit rester à 28px ailleurs). */
/* font-weight:700 ajouté (demande explicite : "régler la différence de police... accords complets/triades
   semble être en gras") — #diatVoicingModeBtn (styles inline) est font-weight:700, .tbtn/.dpk-select-btn
   n'ont aucun poids explicite (400 par défaut du navigateur) sans cet ajout. */
#diatonicPanel .dpk-select-btn{height:22px;padding:0 6px;font-size:max(10px,.5rem);font-weight:700}

/* ── SURFACE : code couleur des cases ──────────────────────────────────────────────────────────────────────
   --gs-c est posée par gsRender() sur chaque case, à partir des référentiels de TOUTE l'application
   (INTERVAL_COLORS ou NOTE_COLORS selon gsColorMode) — jamais d'une palette propre au module : une quinte
   doit avoir ici la couleur qu'elle a dans Gammes et Accords, sinon le code couleur n'apprend plus rien.
   Au repos la couleur reste une TEINTE légère (color-mix) : 30 cases en aplat saturé seraient illisibles et
   fatigantes. C'est l'état joué qui passe en aplat franc. */
/* CONTOUR par défaut (demande explicite), affiné : la couleur est portée par un liseré FIN (1.5px) à son
   OPACITÉ RÉELLE — la teinte identificatrice reste franche, jamais délavée par un color-mix — tandis que le
   fond du pad reprend la teinte LÉGÈRE de la version précédente (22% de --gs-c sur --s3). Combiner les deux
   donne un repère de fond doux à l'œil sur toute la grille, sans perdre la précision de la couleur exacte
   (portée uniquement par le trait, jamais diluée) :
   — la bordure reste FRANCHE, donc identifiable. Mélangée à du gris comme le fond, deux teintes voisines du
     référentiel (le jaune de mi et l'orange de fa, par exemple) deviendraient presque indiscernables, ce qui
     viderait le code couleur de son sens ;
   — le fond teinté aide à repérer une famille de notes en un coup d'œil, sans concurrencer le texte (le
     contraste texte/fond reste géré par --gs-deg, pas par le fond coloré).
   Fine (1.5px) et non épaisse : à cette taille elle reste bien visible (couleur pleine, pas un dégradé),
   et laisse plus de présence visuelle au fond teinté et au texte agrandi. */
#page-play .gs-cell[style*="--gs-c"]{
  background:color-mix(in srgb,var(--s3) 78%,var(--gs-c) 22%);
  border-color:var(--gs-c);border-width:1.5px;
}
#page-play .gs-cell[style*="--gs-c"] .gs-deg{color:var(--gs-c)}
#page-play .gs-cell.gs-root[style*="--gs-c"]{
  background:color-mix(in srgb,var(--s3) 60%,var(--gs-c) 40%);
  border-width:2px;
}
/* Variante APLAT : le pad entier teinté. Plus doux à l'œil, moins précis sur la teinte — proposé pour qui
   préfère, jamais imposé. */
#page-play .gs-grid.gs-fill .gs-cell[style*="--gs-c"]{
  background:color-mix(in srgb,var(--s3) 70%,var(--gs-c) 30%);
  border-color:color-mix(in srgb,var(--bd) 40%,var(--gs-c) 60%);border-width:1px;
  box-shadow:none;
}
#page-play .gs-grid.gs-fill .gs-cell[style*="--gs-c"] .gs-deg{color:color-mix(in srgb,var(--tx) 45%,var(--gs-c) 55%)}
/* Hors gamme, en APLAT : demande explicite — le fond restait trop proche en saturation du fond des cases
   DANS la gamme (30% de teinte, cf. règle ci-dessus), rendant les deux difficiles à distinguer d'un coup
   d'œil. On réduit ici la part de couleur du fond (12% au lieu de 30%) SPÉCIFIQUEMENT pour ce cas — vient
   s'ajouter à l'opacité globale de .gs-out (cf. plus bas), qui seule ne suffisait pas à assez creuser l'écart
   perçu sur un fond déjà teinté. */
#page-play .gs-grid.gs-fill .gs-cell.gs-out[style*="--gs-c"]{
  background:color-mix(in srgb,var(--s3) 88%,var(--gs-c) 12%);
}
#page-play .gs-grid.gs-fill .gs-cell.gs-root[style*="--gs-c"]{
  background:color-mix(in srgb,var(--s3) 45%,var(--gs-c) 55%);
  border-color:var(--gs-c);border-width:2px;box-shadow:none;
}
/* Note JOUÉE : aplat franc dans les deux styles — c'est le seul moment où la couleur doit remplir le pad,
   parce qu'elle signale un état instantané qu'on doit voir du coin de l'œil, doigt posé dessus. */
#page-play .gs-cell.gs-on[style*="--gs-c"]{
  background:var(--gs-c);border-color:var(--gs-c);border-width:3px;
  box-shadow:0 0 0 2px var(--gs-c),0 0 16px -2px var(--gs-c);
}
/* Palier RÉDUITS (gsEffectsLevel=1) : GAP TROUVÉ en vérifiant "les 3 modes sont-ils bien actifs ?" — la
   simplification de halo existait déjà pour la grille NORMALE (#page-play.gs-fx-reduced .gs-cell.gs-on, cf.
   plus haut dans ce fichier) mais cette règle-ci, plus spécifique (attribut [style*="--gs-c"] en plus,
   nécessaire dès qu'un CODE COULEUR de grille est actif), la battait silencieusement quelle que soit
   gsEffectsLevel — le halo à 2 couches restait donc toujours "Complets" en mode code couleur, jamais réduit.
   Même simplification qu'en mode normal : 1 seule couche, sans le halo diffus supplémentaire. */
#page-play.gs-fx-reduced .gs-cell.gs-on[style*="--gs-c"]{
  box-shadow:0 0 0 2px var(--gs-c);
}
#page-play .gs-cell.gs-on[style*="--gs-c"] .gs-deg,
#page-play .gs-cell.gs-on[style*="--gs-c"] .gs-nn{color:#111}
#page-play .gs-cell.gs-held[style*="--gs-c"]{
  background:color-mix(in srgb,var(--s3) 60%,var(--gs-c) 40%);border-color:var(--gs-c);
}
/* Note HORS de la gamme (dispositions chromatique/guitare) : jouable — c'est tout l'intérêt, chromatismes
   et notes de passage — mais estompée, pour que la gamme reste lisible d'un coup d'œil. */
#page-play .gs-cell.gs-out{background:var(--s2);border-style:dashed;opacity:.55}
#page-play .gs-cell.gs-out .gs-deg{color:var(--tx3);font-weight:400}
#page-play .gs-cell.gs-out.gs-on{opacity:1}
/* "Chrom Lock" (gsLayout==='chromatique_pur', demande explicite : "même affichage que mode Chrom, mais les
   notes hors gammes ne sont pas jouables") — même estompage .gs-out que ci-dessus, .gs-locked s'AJOUTE pour
   la rendre réellement injouable (cf. gsPointerDown, qui sort tôt sur cette classe) — même mécanisme déjà en
   place côté hexagonal (cf. .gs-hex-cell.gs-locked). .gs-on jamais posée en même temps que .gs-locked
   (gsPointerDown refuse le jeu AVANT toute création de voix), donc aucune règle de priorité à gérer ici.
   Estompage encore un cran plus marqué que le simple .gs-out (opacité plus basse) : signal visuel qu'une
   case n'est pas seulement "hors gamme" mais RÉELLEMENT bloquée, cohérent avec le palier 'block' côté hex
   (cf. .gs-hex-cell.gs-locked, opacité .08 contre .55 pour le simple .gs-dim-strong). */
#page-play .gs-cell.gs-locked{opacity:.3;cursor:not-allowed}
#page-play .gs-cell.gs-locked .gs-deg{color:var(--tx3);font-weight:400}

/* Nom de note sous le degré, affiché seulement quand la case est assez grande (mesuré dans gsRender).
   Le texte OCCUPE le pad (demande explicite : « utiliser plus de surface à l'intérieur des pads ») : les
   tailles sont exprimées en cqw/cqh, donc relatives à la CASE elle-même et non à la racine. Le texte
   grandit ainsi avec les pads quand on réduit la grille, au lieu de rester un petit label perdu au centre
   d'un grand carré. clamp() borne les extrêmes : lisible sur une case minée, jamais démesuré sur une
   case de 300px. */
#page-play .gs-cell{container-type:size}
#page-play .gs-deg{font-size:clamp(.7rem,26cqh,2.1rem);line-height:1}
#page-play .gs-grid.gs-has-names .gs-cell{flex-direction:column;gap:2cqh}
#page-play .gs-grid.gs-has-names .gs-deg{font-size:clamp(.7rem,30cqh,2.4rem);line-height:1}
#page-play .gs-nn{
  font-family:var(--fm);font-weight:700;color:var(--tx3);
  font-size:clamp(.5rem,19cqh,1.3rem);
  pointer-events:none;line-height:1;
}
#page-play .gs-cell.gs-on .gs-nn{color:#111}
/* La couleur de note s'applique aussi au NOM : c'est lui qu'on lit quand on cherche une note précise. */
#page-play .gs-cell[style*="--gs-c"] .gs-nn{color:color-mix(in srgb,var(--gs-c) 75%,var(--tx3))}
/* Repli si les container queries ne sont pas supportées (Safari < 16) : tailles fixes, comme avant. */
@supports not (container-type:size){
  #page-play .gs-deg{font-size:.85rem}
  #page-play .gs-grid.gs-has-names .gs-deg{font-size:.8rem}
  #page-play .gs-nn{font-size:max(10px,.55rem)}
  #page-play .gs-chord-oct-badge{font-size:.6rem}
}

/* Groupe taille de grille : 2 menus L/C, fusionnés visuellement (demande explicite : "les boutons +-...
   sont supprimés au profit des seuls bouton 'X - Y'"). */
#page-play .gs-size-group{display:inline-flex;gap:2px;flex-shrink:0}

/* ── SURFACE : enregistrement audio ────────────────────────────────────────────────────────────────────────
   Le bouton d'enregistrement en cours pulse en rouge : c'est le seul état de l'application qui continue de
   produire un fichier en arrière-plan, il ne doit pas pouvoir être oublié. Rouge et non couleur de thème :
   le rouge « ça enregistre » est une convention universelle, et il se distingue de tous les autres états
   actifs du module, qui utilisent l'accent. */
#page-play .gs-mini-btn.gs-rec-on{
  background:#c0392b;color:#fff;border-color:#e74c3c;
  animation:gs-rec-pulse 1.4s ease-in-out infinite;
}
@keyframes gs-rec-pulse{0%,100%{box-shadow:0 0 0 0 rgba(231,76,60,.7)}50%{box-shadow:0 0 0 5px rgba(231,76,60,0)}}
#page-play .gs-rec-time{font-family:var(--fm);font-size:.6rem;font-weight:700;color:var(--tx2);min-width:34px}
#page-play .gs-rec-audio{height:28px;max-width:230px;flex:1 1 140px;min-width:0}
@media(prefers-reduced-motion:reduce){
  /* Sans animation, l'état reste porté par la couleur rouge pleine, qui suffit à le signaler. */
  #page-play .gs-mini-btn.gs-rec-on{animation:none}
}

/* ── SURFACE : menus déroulants alignés sur le style de Progressions ───────────────────────────────────────
   Reprend l'apparence du sélecteur de mode de Progressions (#inspModeSelect : fond --s2, bord --bd, rayon
   --r, police --fb), demandé explicitement. La flèche est redessinée en SVG inline plutôt que laissée au
   rendu natif : celui-ci diffère fortement d'un système à l'autre (gros triangle Windows, chevron discret
   macOS), ce qui faisait dépareiller ces menus des autres contrôles du module.
   background-image en data:URI et non un fichier : un aller-retour réseau pour une flèche de 10px serait
   absurde, et le module doit rester intégralement jouable hors ligne. */
#page-play .gs-bar .sound-select,
#page-play .gs-sel,
#page-step .gs-sel{
  -webkit-appearance:none;appearance:none;
  background-color:var(--s2);
  background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23b0b0cc' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:right 6px center;
  padding-right:20px;
  border:1px solid var(--bd);border-radius:var(--r);
  color:var(--tx);font-family:var(--fb);
  cursor:pointer;outline:none;
  transition:border-color var(--tr),background-color var(--tr);
}
#page-play .gs-bar .sound-select:hover,
#page-play .gs-sel:hover{border-color:var(--bd2);background-color:var(--s3)}
#page-play .gs-bar .sound-select:focus-visible,
#page-play .gs-sel:focus-visible{border-color:var(--c-root);box-shadow:0 0 0 2px color-mix(in srgb,var(--c-root) 35%,transparent)}
/* Les <option> héritent du fond du système sur certains navigateurs : on force un fond sombre lisible,
   sinon la liste ouverte apparaît en blanc au milieu d'une interface sombre. */
#page-play .gs-bar .sound-select option,
#page-play .gs-sel option{background:var(--s2);color:var(--tx)}
#page-play .gs-bar .sound-select optgroup{background:var(--sf);color:var(--tx3);font-style:normal;font-weight:700}
@media(max-width:600px){
  /* Flèche resserrée : à cette taille chaque pixel de largeur compte pour le libellé lui-même. */
  #page-play .gs-bar .sound-select{background-position:right 3px center;padding-right:14px}
}

/* Apprentissage MIDI armé : l'utilisateur doit savoir que le prochain potentiomètre tourné sera capturé. */
#page-play .gs-learn-on{color:var(--c-root);font-weight:700}
#page-play .gs-preset-load[disabled]{cursor:default;opacity:.9}

/* Backing/Vidéo — le titre reste tronqué sur desktop. Sur mobile, seules les lignes réellement trop
   longues reçoivent .is-scrolling (mesure faite après rendu dans app.js) et défilent automatiquement
   jusqu'à leur extrémité, avec une pause aux deux bornes pour rester lisibles. */
.ytb-preset-title-text{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@keyframes ytbPresetTitlePan{
  0%,12%{transform:translateX(0)}
  45%,62%{transform:translateX(var(--ytb-title-shift,0px))}
  100%{transform:translateX(0)}
}
@media(max-width:700px){
  .ytb-preset-title.is-scrolling .ytb-preset-title-text{
    display:inline-block;min-width:max-content;overflow:visible;text-overflow:clip;
    animation:ytbPresetTitlePan var(--ytb-title-duration,9s) ease-in-out infinite;
    will-change:transform;
  }
}
@media(prefers-reduced-motion:reduce){
  .ytb-preset-title.is-scrolling .ytb-preset-title-text{animation:none}
}

/* Sur le layout mobile, la topbar reste sticky pendant que le contenu des modules défile derrière elle.
   Le calque de texture global placé derrière toute l'application ne peut donc pas servir seul de fond à
   des barres transparentes : le module, peint plus près de l'utilisateur, devient visible au travers.
   Chaque barre reçoit ici une copie locale opaque de la texture, ancrée au viewport pour conserver le
   raccord entre topbar, rangée de tonalité et subbars. Le desktop garde son calque partagé historique. */
@media(max-width:1023px){
  .ui-bg-enabled .topbar,
  .ui-bg-enabled .subbar{
    isolation:isolate;
    background:var(--bg)!important;
  }
  .ui-bg-enabled .subbar{position:relative}
  .ui-bg-enabled .topbar::before,
  .ui-bg-enabled .subbar::before{
    content:"";
    position:absolute;
    inset:0;
    z-index:-1;
    pointer-events:none;
    background-image:linear-gradient(rgba(0,0,0,var(--ui-bg-scrim)),rgba(0,0,0,var(--ui-bg-scrim))),var(--ui-bg-image);
    background-size:cover;
    background-position:var(--ui-bg-pos,center);
    background-attachment:fixed;
    filter:blur(var(--ui-bg-blur)) saturate(calc(var(--ui-bg-saturation) * 2));
    opacity:var(--ui-bg-opacity);
  }
}

/* Surface — couche expressive (surface-expression.js, ex-0.2) : légère mise en valeur des cases tenues. */
#page-play.gs-expr .gs-cell.gs-on,
#page-play.gs-expr .gs-hex-cell.gs-on{
  filter:saturate(1.08) brightness(1.04);
}
/* Surface 0.3 — réglage en attente (appliqué au relâchement des notes, cf. _gsPendingPolyMode). */
#page-play .gs-mode-btn.gs-pending{outline:1px dashed var(--c-root);outline-offset:2px}

/* ═══ Surface 0.3 — toile de zones, pages, presets (surface-zones.js) ═══════════════════════════════════ */
#page-play .gsz-stage{flex:1;min-height:0;display:flex;flex-direction:column;position:relative}
body.gs-full-width #page-play > #gsStage{max-width:none;margin:0}
/* Plein écran = tout l'écran, quel que soit le réglage « pleine largeur » : sans ça, le plafond desktop de 1600px
   laissait des bandes vides sur les grands écrans (mesuré : tablette 3392×2400 à DPR 2 = 1696px CSS, 48px de chaque côté). */
body.gs-fullscreen #page-play > #gsStage,body.gs-fullscreen #page-play > #gsGridWrap,body.gs-fullscreen #page-play > #gsHexGridWrap{max-width:none;margin:0}
#page-play .gsz-strip{flex:none;display:flex;align-items:center;gap:6px;padding:3px 6px 0;min-height:30px;overflow:hidden}
/* Pages et chaîne se partagent la largeur à leur taille naturelle : rien n'est tronqué tant qu'il reste de la place.
   Quand elle manque, la chaîne cède trois fois plus vite que les pages, puis chacune défile. */
#page-play .gsz-pages{display:flex;gap:3px;align-items:center;flex:0 1 auto;min-width:0;max-width:60%;overflow-x:auto;scrollbar-width:none}
#page-play .gsz-pages::-webkit-scrollbar{display:none}
/* Deux lignes indépendantes : chaîne d'effets (#gsFxRow, en haut, bouton FX) et pages (#gsStrip, EN BAS de la
   surface, bouton ligne de contrôles). */
#page-play #gsStrip{padding:3px 6px}
#page-play #gsFxRow{display:flex;align-items:center;padding:3px 6px 0;min-height:30px}
#page-play #gsFxRow .gsz-chain{flex:1 1 auto;max-width:100%}
#page-play .gsz-strip .gsz-pages{flex:1 1 0;max-width:none}
/* ⋯ / + / liste hors de la zone défilante ; fondu sur les bords quand les onglets débordent */
#page-play .gsz-pbtns{display:flex;gap:3px;align-items:center;flex:none}
#page-play .gsz-tab[hidden]{display:none}
#page-play .gsz-pages.gsz-over{-webkit-mask-image:linear-gradient(90deg,transparent 0,#000 14px,#000 calc(100% - 14px),transparent 100%);mask-image:linear-gradient(90deg,transparent 0,#000 14px,#000 calc(100% - 14px),transparent 100%)}
#page-play .gsz-tab{flex:none;height:26px;min-width:26px;padding:0 9px;border-radius:7px;border:1px solid var(--bd2);background:var(--s1);color:var(--tx2);
  font-family:var(--fm);font-size:.62rem;font-weight:700;cursor:pointer;touch-action:manipulation;display:inline-flex;align-items:center;justify-content:center;gap:4px;white-space:nowrap;max-width:220px;overflow:hidden;text-overflow:ellipsis}
#page-play .gsz-tab.on{background:var(--s1);border-color:var(--c-root);color:var(--tx2)}
/* Réordonnancement des pages par appui long + glisser (2026-10-10) */
#page-play .gsz-pages.gsz-reorder{overflow-x:hidden;-webkit-mask-image:none;mask-image:none}
#page-play .gsz-pages.gsz-reorder .gsz-tab[data-p]{transition:transform .15s ease}
#page-play .gsz-tab.gsz-lift{position:relative;z-index:3;transition:none!important;box-shadow:0 4px 14px rgba(0,0,0,.45);border-color:var(--c-root);background:var(--s2,var(--s1));color:var(--tx);cursor:grabbing;transform:scale(1.06)}
#page-play .gsz-tab.on.gsz-lift{background:var(--s2,var(--s1));color:var(--tx2)}
#page-play .gsz-icon-btn{padding:0 6px}
#page-play .gsz-chain{flex:1 3 auto;min-width:0;display:flex;align-items:center;gap:4px;overflow-x:auto;scrollbar-width:none}
#page-play .gsz-chain::-webkit-scrollbar{display:none}
#page-play .gsz-ic{display:inline-flex;width:14px;height:14px;flex:none}
#page-play .gsz-ic svg,.gs-picker-pop .gsz-ic svg,.gsz-sheet .gsz-ic svg{width:100%;height:100%;display:block}
#page-play .gsz-row{flex:1;min-height:0;display:flex;flex-direction:row;position:relative}
#page-play .gsz-canvas{flex:1;min-width:0;min-height:0;position:relative;touch-action:none;-webkit-user-select:none;user-select:none}
#page-play .gsz-zone{position:absolute;box-sizing:border-box;padding:3px;min-width:0;min-height:0}
#page-play .gsz-edit .gsz-zone.gsz-selected{z-index:16}
#page-play .gsz-body{position:absolute;inset:3px;display:flex;min-width:0;min-height:0}
#page-play .gsz-body > .gs-grid-wrap{flex:1;height:auto!important;max-width:none;margin:0;min-width:0}
/* Zone tournée (jeu face à face, z.rot) : seul le CONTENU tourne ; le cadre d'édition reste dans le repère de
   l'écran. À 90/270° le corps prend les dimensions PERMUTÉES de la zone (unités de conteneur cqw/cqh). Les
   gestes passent par gsLocalPoint (surface-zones.js). Pas de transition : elle fausserait les hit-tests. */
#page-play .gsz-zone.gsz-rot{container-type:size}
#page-play .gsz-zone.gsz-rot180>.gsz-body{transform:rotate(180deg)}
#page-play .gsz-zone.gsz-rot90>.gsz-body,#page-play .gsz-zone.gsz-rot270>.gsz-body{inset:auto;left:50%;top:50%;width:calc(100cqh - 6px);height:calc(100cqw - 6px)}
#page-play .gsz-zone.gsz-rot90>.gsz-body{transform:translate(-50%,-50%) rotate(90deg)}
#page-play .gsz-zone.gsz-rot270>.gsz-body{transform:translate(-50%,-50%) rotate(270deg)}
#page-play .gsz-hbtn.on{border-color:var(--c-root);color:var(--c-root)}
/* Pastille VOL (jeu) : discrète, en haut à droite de la zone, sous les cadres d'édition */
#page-play .gsz-hbtn.gsz-vol,.gsz-sheet .gsz-hbtn.gsz-vol{width:auto;padding:0 6px;font:700 max(9px,.5rem)/1 var(--fm)}
/* Page Surface qui émet du son : l'onglet clignote (même animation que les onglets de modules) */
#page-play .gsz-tab.gsz-pulsing{animation:gszTabGlow .9s ease-in-out infinite;border-color:var(--c-root);color:var(--tx2)}
@keyframes gszTabGlow{0%,100%{border-color:color-mix(in srgb,var(--c-root) 35%,var(--bd2));box-shadow:none}50%{border-color:var(--c-root);box-shadow:0 0 8px var(--c-root)}}
@media(prefers-reduced-motion:reduce){#page-play .gsz-tab.gsz-pulsing{animation:none;box-shadow:inset 0 -2px 0 var(--c-root)}}
.gsz-volpop{position:fixed;z-index:10050;display:flex;align-items:center;gap:8px;padding:8px 10px;background:var(--s2);border:1px solid var(--bd2);border-radius:8px;box-shadow:0 6px 22px rgba(0,0,0,.45);font:700 max(10px,.55rem) var(--fm);color:var(--tx2)}
.gsz-volpop input{width:150px;accent-color:var(--c-root)}
.gsz-volpop output{min-width:3.2em;text-align:right;color:var(--c-root)}
#page-play .gsz-sheet{transform-origin:50% 50%}
.gs-picker-pop.gsz-pop-rot,.gsl-pop.gsz-pop-rot{transform-origin:50% 50%}
#page-play .gsz-missing,#page-play .gsz-empty-hint{margin:auto;color:var(--tx2);font-family:var(--fm);font-size:.7rem;text-align:center;padding:12px}
#page-play .gsz-empty-hint{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;border:1px dashed var(--bd2);border-radius:10px;pointer-events:none}
#page-play .gsz-canvas > .gs-func-free-layer{z-index:20}
/* Tiroir de bibliothèque déplacé à droite de la toile (il vivait dans #gsGridWrap). */
#page-play .gsz-row > .gs-func-free-drawer{margin:3px 3px 3px 6px}
#page-play .gsz-drawer-title{font-family:var(--fm);font-size:max(10px,.5rem);font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--tx2);margin:2px 0}
#page-play .gsz-drawer-sec{display:flex;flex-direction:column;gap:5px}
#page-play .gsz-drawer-item{display:flex;align-items:center;gap:5px;padding:7px 5px;border-radius:6px;border:1px solid var(--c-root);background:var(--s1);color:var(--tx);
  font-family:var(--fm);font-size:.6rem;font-weight:700;cursor:grab;touch-action:none;user-select:none;-webkit-user-select:none}
/* Cadre d'édition d'une zone */
#page-play .gsz-frame{container-type:size;position:absolute;inset:1px;border:1.5px dashed color-mix(in srgb,var(--c-root) 70%,transparent);border-radius:9px;pointer-events:none;z-index:15}
#page-play .gsz-frame.gsz-sel{border-style:solid;border-color:var(--c-root);box-shadow:0 0 0 2px color-mix(in srgb,var(--c-root) 25%,transparent)}
#page-play .gsz-head{position:absolute;left:6px;top:6px;display:flex;align-items:center;gap:5px;padding:3px 4px 3px 7px;border-radius:7px;background:color-mix(in srgb,var(--bg) 88%,transparent);
  border:1px solid var(--c-root);color:var(--tx);font-family:var(--fm);font-size:.6rem;font-weight:700;pointer-events:auto;cursor:move;touch-action:none;max-width:calc(100% - 12px);
  z-index:3} /* au-dessus des poignées de redimensionnement (z-index:2), sinon elles interceptent le rouage d'une zone étroite */
#page-play .gsz-colored>.gsz-frame .gsz-head{border:2px solid var(--gsz-c)}
#page-play .gsz-lbl{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* Zone étroite (fader vertical…) : la barre flottante passe en colonne et ne dépasse jamais de son cadre */
#page-play .gsz-head{max-height:calc(100% - 12px);overflow:hidden}
@container (max-width:150px){
  #page-play .gsz-head{flex-direction:column;padding:6px 3px;gap:4px}
  #page-play .gsz-head .gsz-lbl{writing-mode:vertical-rl;transform:rotate(180deg);min-height:0;flex:0 1 auto}
  #page-play .gsz-head .gsz-pan{display:none}
}
@container (max-height:60px){
  #page-play .gsz-head .gsz-lbl,#page-play .gsz-head .gsz-pan{display:none}
}
#page-play .gsz-hbtn,.gsz-sheet .gsz-hbtn{width:26px;height:24px;display:inline-flex;align-items:center;justify-content:center;border-radius:6px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);cursor:pointer;padding:0;flex:none}
#page-play .gsz-h{position:absolute;pointer-events:auto;touch-action:none;z-index:2}
#page-play .gsz-h-n,#page-play .gsz-h-s{left:18px;right:18px;height:14px;cursor:ns-resize}
#page-play .gsz-h-n{top:-7px}#page-play .gsz-h-s{bottom:-7px}
#page-play .gsz-h-e,#page-play .gsz-h-w{top:18px;bottom:18px;width:14px;cursor:ew-resize}
#page-play .gsz-h-e{right:-7px}#page-play .gsz-h-w{left:-7px}
#page-play .gsz-h-se,#page-play .gsz-h-sw,#page-play .gsz-h-ne,#page-play .gsz-h-nw{width:22px;height:22px}
#page-play .gsz-h-se{right:-7px;bottom:-7px;cursor:nwse-resize}#page-play .gsz-h-nw{left:-7px;top:-7px;cursor:nwse-resize}
#page-play .gsz-h-sw{left:-7px;bottom:-7px;cursor:nesw-resize}#page-play .gsz-h-ne{right:-7px;top:-7px;cursor:nesw-resize}
#page-play .gsz-h-se::after{content:"";position:absolute;right:8px;bottom:8px;width:9px;height:9px;border-right:2px solid var(--c-root);border-bottom:2px solid var(--c-root)}
#page-play .gsz-h-n::after,#page-play .gsz-h-s::after{content:"";position:absolute;left:50%;top:5px;width:28px;height:3px;margin-left:-14px;border-radius:2px;background:var(--c-root)}
#page-play .gsz-h-e::after,#page-play .gsz-h-w::after{content:"";position:absolute;top:50%;left:5px;height:28px;width:3px;margin-top:-14px;border-radius:2px;background:var(--c-root)}
/* Feuille de réglages d'une zone */
#page-play .gsz-sheet{position:absolute;right:8px;top:36px;z-index:40;width:min(320px,calc(100% - 16px));max-height:calc(100% - 44px);display:flex;flex-direction:column;
  background:var(--sf);border:1px solid var(--bd2);border-radius:12px;box-shadow:0 14px 40px rgba(0,0,0,.45)}
#page-play .gsz-sheet-head{display:flex;align-items:center;gap:7px;padding:8px 8px 8px 12px;border-bottom:1px solid var(--bd);font-family:var(--fd);font-size:.8rem;color:var(--tx)}
#page-play .gsz-sheet-head b{flex:1}
#page-play .gsz-sheet-head{position:relative}
#page-play .gsz-sheet-head .gsz-hdel{color:#e5484d;border-color:color-mix(in srgb,#e5484d 55%,var(--bd2))}
#page-play .gsz-delpop{position:absolute;top:calc(100% + 4px);right:36px;z-index:5;display:flex;flex-direction:column;gap:6px;padding:8px 10px;background:var(--sf);border:1px solid #e5484d;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.5);font:600 .72rem var(--fm);color:var(--tx);white-space:nowrap}
#page-play .gsz-delpop>div{display:flex;gap:6px}
#page-play .gsz-delpop .gsz-danger{background:#e5484d;border-color:#e5484d;color:#fff}
#page-play .gsz-sheet-body{overflow-y:auto;padding:8px 12px 12px;display:flex;flex-direction:column;gap:8px}
#page-play .gsz-field{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
/* Couleur de zone (2026-10-05) — cf. ZONE_COLORS (surface-zones.js) pour le principe : CADRE pour les
   instruments (le fond des cases porte les notes), CONTOUR + REPÈRE DE VALEUR pour les contrôleurs (fond
   neutre au repos = contraste maximal du libellé ; remplissage plein = état allumé, dans la couleur). */
#page-play .gsz-swatches{display:flex;flex-wrap:wrap;gap:6px}
#page-play .gsz-swatch{width:26px;height:26px;border-radius:50%;border:2px solid var(--bd2);background:var(--sw,var(--s2));cursor:pointer;padding:0;
  font-family:var(--fm);font-size:.55rem;font-weight:700;color:var(--tx2)}
#page-play .gsz-swatch.on{box-shadow:0 0 0 2px var(--sf),0 0 0 4px var(--tx)}
/* outline DÉCOLLÉ du contenu (dans les 3px de marge de la zone) et qui suit l'arrondi propre de chaque corps de
   zone — un box-shadow collé imposait son propre rayon et touchait les touches/cases. */
#page-play .gsz-zone.gsz-colored>.gsz-body:not(.gsz-ctl-host){outline:2px solid var(--gsz-c);outline-offset:1px}
#page-play .gsz-zone.gsz-colored .gsz-ctl{--c-root:var(--gsz-c);border:2px solid color-mix(in srgb,var(--gsz-c) 80%,transparent)}
#page-play .gsz-field > .gs-midi-lbl{min-width:92px}
#page-play .gsz-field input[type=range]{flex:1;min-width:90px}
#page-play .gsz-val{font-family:var(--fm);font-size:.62rem;color:var(--tx2);min-width:34px;text-align:right}
#page-play .gsz-note{font-size:.7rem;color:var(--tx2);line-height:1.4}

/* ═══ Surface 0.3 — zones Piano / Mini (surface-keys.js) ═══════════════════════════════════════════════ */
#page-play .gsk-host{position:absolute;inset:3px;flex-direction:column;border-radius:9px;overflow:hidden;background:var(--s1)}
#page-play .gsk-ctl{position:absolute;left:0;right:0;top:0;height:24px;display:flex;align-items:center;gap:6px;padding:0 8px;
  background:color-mix(in srgb,var(--s2) 80%,transparent);font-family:var(--fm);font-size:.6rem;color:var(--tx2)}
/* Barre de titre d'une zone colorée (piano, mini, grilles) : la couleur ne passe JAMAIS sous ni dans le
   texte (demande explicite : lisibilité) — fond neutre inchangé, seulement un filet en bas et un repère à
   gauche, hors des libellés. */
#page-play .gsz-colored .gsk-ctl{box-shadow:inset 0 -2px 0 color-mix(in srgb,var(--gsz-c) 70%,transparent)}
/* Mode « Titre » (Paramètres > Apparence > Couleur des zones) : pas de contour complet. Zone avec barre de
   titre → filet plein de 3px sous la barre ; zone sans barre (hexa, pads, outils) → filet de 3px posé dans la
   marge AU-DESSUS de la zone, jamais sur son contenu. Les contrôleurs gardent leur contour (c'est leur forme). */
#page-play.gsz-colmode-title .gsz-zone.gsz-colored>.gsz-body:not(.gsz-ctl-host){outline:none}
#page-play.gsz-colmode-title .gsz-colored .gsk-ctl{box-shadow:inset 0 -3px 0 var(--gsz-c)}
#page-play.gsz-colmode-title .gsz-zone.gsz-colored:not(:has(.gsk-ctl)):not(:has(.gsz-ctl-host))::before{
  content:"";position:absolute;left:8px;right:8px;top:0;height:3px;border-radius:2px;background:var(--gsz-c);pointer-events:none;z-index:3}
#page-play .gsk-oct{width:30px;height:20px;border-radius:5px;border:1px solid var(--bd2);background:var(--s1);color:var(--tx);font:700 .8rem/1 var(--fm);cursor:pointer;touch-action:none;padding:0}
#page-play .gsk-range{min-width:30px;text-align:center;font-weight:700;color:var(--tx)}
#page-play .gsk-kb{position:absolute;left:0;right:0;top:24px;bottom:0}
/* appui long au doigt : ni sélection de texte ni bulle système sur les zones de jeu et l'éditeur de passe */
#page-play .gsz-canvas,#page-play .gsk-host,.gsl-ed-grid,.gsl-ed-keys,.gsl-ed-vel{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}
#page-play .gsk-row{position:absolute;left:0;right:0}
#page-play .gsk-key{position:absolute;top:0;box-sizing:border-box;pointer-events:none;display:flex;align-items:flex-end;justify-content:center;
  transition:background-color var(--gs-on-dur,.06s)}
#page-play .gsk-w{height:100%;background:linear-gradient(#f4f4f2,#dcdcd8);border:1px solid #2a2a2a;border-top:none;border-radius:0 0 6px 6px;z-index:1}
#page-play .gsk-b{background:linear-gradient(#2b2b2e,#111113);border:1px solid #000;border-top:none;border-radius:0 0 4px 4px;z-index:3;box-shadow:0 2px 3px rgba(0,0,0,.45)}
/* Touches noires pleine hauteur : elles recouvrent le bas des blanches. Les étiquettes des BLANCHES passent AU-DESSUS
   des noires, sur une pastille claire (jamais masquées ni tronquées) ; celles des noires remontent pour ne pas
   chevaucher. */
#page-play .gsk-bfull .gsk-w,#page-play .gsk-bfull .gsk-w.gsk-echo{z-index:auto}
#page-play .gsk-bfull .gsk-w .gsk-lbl{position:relative;z-index:6;margin-bottom:6px;padding:1px 3px;border-radius:4px;background:rgba(244,244,242,.92);color:#222;white-space:nowrap}
#page-play .gsk-bfull .gsk-w .gsk-lbl .gsk-deg{color:#444}
#page-play .gsk-bfull .gsk-b .gsk-lbl{margin-bottom:46px;white-space:nowrap}
#page-play .gsk-in::after{content:"";position:absolute;left:18%;right:18%;bottom:5px;height:4px;border-radius:3px;background:var(--gsk-c,var(--c-root));opacity:.85}
#page-play .gsk-root::after{height:7px;opacity:1;box-shadow:0 0 6px var(--gsk-c,var(--c-root))}
/* Hors gamme (estompées) : même hachure que « verrouillées », en plus léger ; appuyée → hachure pleine (pas la couleur d'accent) */
#page-play .gsk-w.gsk-out{background:repeating-linear-gradient(135deg,rgba(138,138,136,.55) 0 6px,rgba(119,119,119,.55) 6px 12px),linear-gradient(#d8d8d5,#c4c4c0)}
#page-play .gsk-b.gsk-out{background:repeating-linear-gradient(135deg,rgba(28,28,30,.6) 0 5px,rgba(52,52,58,.6) 5px 10px),linear-gradient(#2b2b2e,#111113)}
#page-play .gsk-w.gsk-out.gsk-on{background:repeating-linear-gradient(135deg,#8a8a88 0 6px,#777 6px 12px)!important;opacity:.35}
#page-play .gsk-b.gsk-out.gsk-on{background:repeating-linear-gradient(135deg,#1c1c1e 0 5px,#2c2c30 5px 10px)!important}
#page-play .gsk-locked{opacity:.35}
#page-play .gsk-w.gsk-locked{background:repeating-linear-gradient(135deg,#8a8a88 0 6px,#777 6px 12px)}
#page-play .gsk-b.gsk-locked{opacity:1;background:repeating-linear-gradient(135deg,#1c1c1e 0 5px,#2c2c30 5px 10px)}
#page-play .gsk-lbl{font-family:var(--fm);font-size:max(10px,.58rem);font-weight:700;color:#333;margin-bottom:12px;pointer-events:none}
#page-play .gsk-b .gsk-lbl{color:#ccc;margin-bottom:8px;font-size:max(10px,.5rem)}
/* Touche allumée : couleur pleine, SANS filter (un filter crée une couche de composition qui, sur mobile,
   pouvait remonter la touche blanche allumée par-dessus les noires pendant un glissé). */
/* Couleur de note par-dessus un fond OPAQUE : les couleurs de notes peuvent être translucides, et une noire
   allumée laissait voir la séparation des blanches dessous. */
#page-play .gsk-w.gsk-on{background:linear-gradient(var(--gsk-c,var(--c-root)),var(--gsk-c,var(--c-root))),#f4f4f2!important;box-shadow:inset 0 0 0 2px rgba(255,255,255,.35)}
#page-play .gsk-b.gsk-on{background:linear-gradient(var(--gsk-c,var(--c-root)),var(--gsk-c,var(--c-root))),#141416!important;box-shadow:inset 0 0 0 2px rgba(255,255,255,.35),0 2px 3px rgba(0,0,0,.45)}
/* Mini (stylophone) : touches métalliques, jeu au « stylet » */
#page-play .gsk-mini .gsk-w{background:linear-gradient(#e3e6ea,#a9aeb5);border:1px solid #545a62;border-radius:3px;margin:0 1px;width:auto}
#page-play .gsk-mini .gsk-b{background:linear-gradient(#5a6068,#2c3036);border:1px solid #1c1f23;border-radius:3px}
#page-play .gsk-mini .gsk-lbl{margin-bottom:6px}

/* ═══ Surface 0.3 — zone Pads (surface-drums.js) ═════════════════════════════════════════════════════ */
#page-play .gsd-host{display:grid;grid-template-columns:repeat(var(--gsd-cols,4),1fr);grid-template-rows:repeat(var(--gsd-rows,2),1fr);gap:6px;padding:2px}
#page-play .gsd-pad{position:relative;border-radius:10px;display:flex;align-items:flex-end;justify-content:flex-start;padding:7px 9px;box-sizing:border-box;min-width:0;min-height:0;
  background:linear-gradient(160deg,hsl(var(--gsd-h) 45% 30%),hsl(var(--gsd-h) 40% 18%));border:1px solid hsl(var(--gsd-h) 55% 42%);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.08);cursor:pointer;touch-action:none;user-select:none;-webkit-user-select:none}
#page-play .gsd-lbl{font-family:var(--fm);font-size:.66rem;font-weight:700;color:hsl(var(--gsd-h) 70% 85%);pointer-events:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#page-play .gsd-pad.gsd-hit{animation:gsdHit .22s ease-out}
@keyframes gsdHit{0%{background:hsl(var(--gsd-h) 80% 62%);transform:scale(.96)}100%{transform:scale(1)}}
#page-play .gsz-edit .gsd-pad{outline:1px dashed hsl(var(--gsd-h) 60% 60%);outline-offset:-4px}

/* ═══ Surface 0.3 — looper et zone Beat (surface-looper.js) ════════════════════════════════════════════ */
:is(#page-play,#gslFloat) .gsl-host{flex-direction:column;gap:6px;padding:6px;border-radius:9px;background:var(--s1);border:1px solid var(--bd);overflow:hidden}
:is(#page-play,#gslFloat) .gsl-bar{display:flex;align-items:center;gap:5px;flex-wrap:wrap}
:is(#page-play,#gslFloat) .gsl-btn{width:40px;height:34px;border-radius:8px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;touch-action:none;padding:0}
:is(#page-play,#gslFloat) .gsl-btn svg{width:16px;height:16px}
:is(#page-play,#gslFloat) .gsl-rec{color:#e5484d}
:is(#page-play,#gslFloat) .gsl-rec.on{background:#e5484d;color:#fff;border-color:#e5484d;animation:gslPulse 1s infinite}
:is(#page-play,#gslFloat) .gsl-rec.armed{border-color:#e5484d;animation:gslPulse .5s infinite}
:is(#page-play,#gslFloat) .gsl-play.on{background:var(--c-root);border-color:var(--c-root);color:#111}
@keyframes gslPulse{50%{opacity:.55}}
:is(#page-play,#gslFloat) .gsl-status{font-family:var(--fm);font-size:.62rem;color:var(--tx2);margin-left:4px;white-space:nowrap}
:is(#page-play,#gslFloat) .gsl-prog{height:6px;border-radius:3px;background:var(--s2);overflow:hidden;flex:none}
:is(#page-play,#gslFloat) .gsl-prog-fill{height:100%;width:0;background:var(--c-root)}
:is(#page-play,#gslFloat) .gsl-prog.gsl-blip{animation:gslBlip .15s}
@keyframes gslBlip{0%{box-shadow:0 0 6px var(--c-root)}}
:is(#page-play,#gslFloat) .gsl-opts{display:flex;gap:6px;flex-wrap:wrap}
:is(#page-play,#gslFloat) .gsl-layers{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:4px}
:is(#page-play,#gslFloat) .gsl-layer{display:flex;align-items:center;gap:6px;padding:3px 4px;border-radius:7px;border-left:4px solid hsl(var(--gsl-h) 70% 55%);background:var(--s2);font-family:var(--fm);font-size:.6rem;color:var(--tx2)}
:is(#page-play,#gslFloat) .gsl-layer button{border:none;background:none;color:var(--tx);font:inherit;font-weight:700;cursor:pointer;padding:4px 6px}
:is(#page-play,#gslFloat) .gsl-layer span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
:is(#page-play,#gslFloat) .gsl-layer.muted{opacity:.45}
:is(#page-play,#gslFloat) .gsl-layer .gsl-x{color:var(--tx2);font-size:.85rem}
:is(#page-play,#gslFloat) .gsl-empty{font-size:.66rem;color:var(--tx2);line-height:1.4}
/* Zone Beat : mini-clavier en haut, pads + transport en bas */
#page-play .gsb-host{flex-direction:column;gap:6px;border-radius:12px;padding:6px;background:linear-gradient(160deg,#2b2f36,#1b1d22);border:1px solid #3a3f47}
#page-play .gsb-keys{position:relative;flex:1.2;min-height:0;display:flex}
#page-play .gsb-keys>.gsk-host{inset:0}
#page-play .gsb-low{flex:1;min-height:0;display:flex;gap:6px}
#page-play .gsb-drums{flex:1.3;min-width:0;display:grid}
#page-play .gsb-tr{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center;gap:6px}

/* ═══ Surface 0.3 — bandeau chaîne, tiroir et zone Synthé (surface-synth.js) ═══════════════════════════ */
#page-play .gsc-chip{flex:none;height:26px;display:inline-flex;align-items:center;gap:5px;padding:0 9px;border-radius:13px;border:1px solid var(--bd2);background:var(--s1);color:var(--tx2);
  font-family:var(--fm);font-size:.6rem;font-weight:700;cursor:pointer;touch-action:manipulation;white-space:nowrap}
#page-play .gsc-chip b{color:var(--tx);font-weight:700}
#page-play .gsc-instr{color:var(--tx);border-color:var(--c-root);background:color-mix(in srgb,var(--c-root) 18%,var(--s1))}
#page-play .gsc-fx.on{color:#111;background:var(--c-root);border-color:var(--c-root)}
#page-play .gsc-fx.on b{color:#111}
#page-play .gsc-edit.on{background:var(--c-root);color:#111;border-color:var(--c-root)}
#page-play .gsc-arrow{flex:none;width:10px;height:2px;background:var(--bd2);position:relative}
#page-play .gsc-arrow::after{content:"";position:absolute;right:-1px;top:-3px;border-left:5px solid var(--bd2);border-top:4px solid transparent;border-bottom:4px solid transparent}
#page-play .gss-drawer{position:absolute;left:6px;right:6px;top:34px;z-index:45;max-height:calc(100% - 44px);overflow-y:auto;border-radius:12px;
  background:color-mix(in srgb,var(--sf) 94%,transparent);border:1px solid var(--bd2);box-shadow:0 16px 40px rgba(0,0,0,.5);padding:8px 10px}
#page-play .gss-close{position:absolute;right:8px;top:8px}
#page-play .gss-panel{display:flex;flex-direction:column;gap:6px;min-width:0}
#page-play .gss-head{display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding-right:34px}
#page-play .gss-voice,#page-play .gss-osc{height:28px;padding:0 10px;border-radius:8px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.62rem;font-weight:700;cursor:pointer}
#page-play .gss-voice{border-color:var(--c-root)}
#page-play .gss-grid{display:flex;flex-wrap:wrap;gap:6px 10px}
#page-play .gss-sub{font-family:var(--fm);font-size:max(10px,.52rem);font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--tx2);margin-top:4px}
#page-play .gss-zone-host{flex-direction:column;gap:6px;padding:8px;overflow-y:auto;border-radius:10px;background:var(--s1);border:1px solid var(--bd)}
#page-play .gss-target{font-family:var(--fm);font-size:max(10px,.58rem);color:var(--tx2)}
#page-play .gss-grid .tb303-knob-wrap{width:68px;min-width:68px}
#page-play .gss-grid .tb303-knob-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:68px;display:block;text-align:center}
@media (pointer:coarse){#page-play .gss-grid .tb303-knob-svg{width:42px;height:42px}}
/* Surface 0.3 — barre de jeu et bandeau utilisables avec un 2e doigt pendant qu'un autre tient une note
   (surface-touchbar.js) : aucun geste navigateur (zoom à deux doigts, double-tap) ne doit s'y déclencher. */
#page-play #gsOctRow,#page-play .gsz-strip{touch-action:pan-x}
#page-play #gsOctRow button,#page-play .gsz-strip button{touch-action:manipulation}
:is(#page-play,#gslFloat) .gsl-btn.gsl-confirm{background:#e5484d;border-color:#e5484d;color:#fff;animation:gslPulse .5s infinite}
:is(#page-play,#gslFloat) .gsl-play.paused{border-color:var(--c-root);color:var(--c-root)}
/* Looper : vue compacte (une ligne), panneau ⋯ en surimpression, lignes de passe (volume/solo/quantif.). */
:is(#page-play,#gslFloat) .gsl-more{display:none;position:relative}
:is(#page-play,#gslFloat) .gsl-badge{position:absolute;top:-5px;right:-5px;min-width:15px;height:15px;padding:0 3px;border-radius:8px;background:var(--c-root);color:#111;font-family:var(--fm);font-size:max(10px,.5rem);font-weight:700;line-height:15px;text-align:center}
:is(#page-play,#gslFloat) .gsl-badge:empty{display:none}
:is(#page-play,#gslFloat) .gsl-compact .gsl-full-only{display:none!important}
:is(#page-play,#gslFloat) .gsl-compact .gsl-more{display:inline-flex}
:is(#page-play,#gslFloat) .gsl-compact .gsl-bar{flex-wrap:nowrap;gap:4px;min-width:0}
:is(#page-play,#gslFloat) .gsl-compact .gsl-btn{width:34px;height:30px;flex:none}
:is(#page-play,#gslFloat) .gsl-compact .gsl-status{min-width:0;overflow:hidden;text-overflow:ellipsis;font-size:max(10px,.56rem)}
:is(#page-play,#gslFloat) .gsl-host.gsl-compact{gap:4px;padding:4px;justify-content:center}
:is(#page-play,#gslFloat) .gsl-btn:disabled{opacity:.35;cursor:default}
:is(#page-play,#gslFloat) .gsl-opts .gsl-opt{min-width:0;padding:4px 9px;font-family:var(--fm);font-size:.62rem}
:is(#page-play,#gslFloat) .gsl-opts .gsl-opt:disabled{opacity:.35}
:is(#page-play,#gslFloat) .gsl-pop{position:fixed;z-index:10050;display:flex;background:var(--sf);border-color:var(--bd2);box-shadow:0 10px 30px rgba(0,0,0,.45);overflow-y:auto}
:is(#page-play,#gslFloat) .gsl-pop .gsl-layers{flex:none;overflow:visible}
:is(#page-play,#gslFloat) .gsl-layer .gsl-name{display:flex;flex-direction:column;align-items:flex-start;min-width:0;max-width:40%;text-align:left}
:is(#page-play,#gslFloat) .gsl-layer .gsl-name small{font-weight:400;font-size:max(10px,.52rem);color:var(--tx2);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
:is(#page-play,#gslFloat) .gsl-layer .gsl-vol{flex:1;min-width:40px;height:18px;accent-color:hsl(var(--gsl-h) 70% 55%);touch-action:none}
:is(#page-play,#gslFloat) .gsl-layer .gsl-s{border:1px solid var(--bd2);border-radius:5px;padding:2px 6px;font-size:max(10px,.55rem)}
:is(#page-play,#gslFloat) .gsl-layer .gsl-s.on{background:#e8b400;border-color:#e8b400;color:#111}
:is(#page-play,#gslFloat) .gsl-layer .gsl-qz{color:var(--tx2);font-size:.8rem;padding:2px 4px}
:is(#page-play,#gslFloat) .gsl-layer{flex-wrap:wrap}
:is(#page-play,#gslFloat) .gsl-roll{position:relative;flex:0 0 100%;height:30px;border-radius:4px;background:color-mix(in srgb,var(--bg) 70%,transparent);overflow:hidden;touch-action:none}
:is(#page-play,#gslFloat) .gsl-roll svg{position:absolute;inset:0;width:100%;height:100%}
:is(#page-play,#gslFloat) .gsl-roll rect{fill:var(--nc,hsl(var(--gsl-h) 70% 58%));cursor:pointer}
:is(#page-play,#gslFloat) .gsl-roll .gsl-roll-dr{fill:hsl(var(--gsl-h) 55% 70%)}
:is(#page-play,#gslFloat) .gsl-roll .gsl-roll-bar{stroke:var(--bd);stroke-width:2;vector-effect:non-scaling-stroke}
:is(#page-play,#gslFloat) .gsl-roll .sel rect{fill:#e5484d}
:is(#page-play,#gslFloat) .gsl-roll .drag rect{fill:var(--tx);opacity:.85}
/* Éditeur de passe (✎) : panneau bas d'écran, lignes de demi-tons, sélection/déplacement/lasso. */
:is(#page-play,#gslFloat) .gsl-layer .gsl-ed-btn{color:var(--tx2);padding:2px 4px;display:inline-flex}
:is(#page-play,#gslFloat) .gsl-layer .gsl-ed-btn svg{width:14px;height:14px}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed{position:fixed;left:8px;right:8px;bottom:8px;z-index:10060;max-height:min(70vh,560px);display:flex;flex-direction:column;gap:6px;padding:8px;border-radius:12px;background:var(--sf);border:1px solid var(--bd2);box-shadow:0 12px 36px rgba(0,0,0,.5)}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-head{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-family:var(--fm);font-size:.66rem;color:var(--tx2)}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-name{color:var(--tx);font-size:.72rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-info{flex:1;min-width:0}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-head button{white-space:nowrap;min-width:36px;height:32px;border-radius:8px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font:inherit;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0 8px}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-head button svg{width:15px;height:15px}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-head button:disabled{opacity:.35;cursor:default}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-body{display:flex;gap:4px;overflow:auto;min-height:0;overscroll-behavior-x:contain}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys{flex:none;width:46px;position:sticky;left:0;z-index:2;background:var(--sf);display:flex;flex-direction:column;font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx2)}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys div{flex:1;display:flex;align-items:center;justify-content:flex-end;padding-right:4px;border-radius:2px;min-height:0}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys .blk{background:var(--s2)}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys .dr{color:var(--tx);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;line-height:1;text-align:right;align-content:center;font-size:max(10px,.5rem)}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys .dr[data-dr]{cursor:grab;touch-action:none}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys .dr.drag{opacity:.45;cursor:grabbing}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys .dr.drop{background:var(--c-root);color:#111;font-weight:700}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys .dr:has(.drb){display:flex;align-items:center;gap:2px;padding-right:2px}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys .dr .drn{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;text-align:right}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys .dr .drb{flex:none;width:16px;height:min(16px,100%);padding:0;border:1px solid var(--bd2);border-radius:3px;background:var(--s2);color:var(--tx2);font:700 max(10px,.5rem)/1 var(--fm);cursor:pointer;display:flex;align-items:center;justify-content:center}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys .dr .drb.on{background:#e5484d;border-color:#e5484d;color:#fff}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys .dr .drb.s.on{background:#f5c518;border-color:#f5c518;color:#111}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys .dr.off .drn{opacity:.4;text-decoration:line-through}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-row.dr.off{opacity:.5}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-n.dr.off{opacity:.25!important}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-grid{position:relative;flex:1;min-width:0;touch-action:none;border-radius:6px;overflow:hidden;cursor:crosshair}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-grid svg{position:absolute;inset:0;width:100%;height:100%}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-row{fill:color-mix(in srgb,var(--bg) 55%,var(--sf))}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-row.blk{fill:color-mix(in srgb,var(--bg) 85%,var(--sf))}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-row.c{fill:color-mix(in srgb,var(--c-root) 10%,var(--bg))}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-row.dr{fill:var(--s2)}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-row.dr.alt{fill:var(--s3)}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-v{stroke:var(--bd);stroke-width:1;opacity:.45;vector-effect:non-scaling-stroke}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-v.beat{opacity:.8}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-v.bar{stroke:var(--bd2);stroke-width:2;opacity:1}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-n rect{fill:var(--nc,hsl(var(--gsl-h,12) 70% 58%));stroke:rgba(0,0,0,.35);stroke-width:1;vector-effect:non-scaling-stroke;cursor:grab}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-n.dr rect{fill:#9aa3ad}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-n.sel rect{stroke:var(--c-root);stroke-width:3;filter:brightness(1.15)}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-n.drag rect{opacity:.8}
/* glissés (slide) : trait incliné entre deux paliers de hauteur d'une même note */
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-gl{stroke:var(--nc,hsl(var(--gsl-h,12) 70% 58%));stroke-width:3;stroke-linecap:round;vector-effect:non-scaling-stroke}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-n.sel .gsl-ed-gl{stroke:var(--c-root)}
:is(#page-play,#gslFloat) .gsl-roll .gsl-roll-gl{stroke:hsl(var(--gsl-h) 70% 58%);stroke-width:2;vector-effect:non-scaling-stroke}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-n.sel rect.gsl-ed-grip{fill:#fff;stroke:none;opacity:.85;cursor:ew-resize}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-head button.on{background:var(--c-root);border-color:var(--c-root);color:#111}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-band{position:absolute;display:none;border:1px dashed var(--c-root);background:color-mix(in srgb,var(--c-root) 14%,transparent);pointer-events:none}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-head-line{position:absolute;top:0;bottom:0;left:0;width:2px;background:var(--tx);opacity:0;pointer-events:none}
/* #gslFloat : conteneur au niveau du body pour l'éditeur de passe et le panneau ⋯ du looper (au-dessus de la
   barre latérale desktop, que la pile d'affichage de #page-play ne peut pas dépasser). */
#gslFloat .gs-mode-btn{width:auto;min-width:0;padding:0 9px;height:28px;font-size:max(10px,.58rem);white-space:nowrap}
#gslFloat .gs-mode-btn.on{background:var(--c-root);color:#111;border-color:var(--c-root)}
#gslFloat .gs-mode-btn:disabled{opacity:.35}
#gslFloat .gsl-host{flex-direction:column;gap:6px;padding:6px;border-radius:9px;border:1px solid var(--bd)}
@media (max-width:560px){
  :is(#page-play,#gslFloat,#page-melodie) .gsl-ed{left:4px;right:4px;bottom:4px;padding:6px;max-height:78vh}
  :is(#page-play,#gslFloat,#page-melodie) .gsl-ed-name{flex:1 0 100%}
  :is(#page-play,#gslFloat,#page-melodie) .gsl-ed-info{display:none}
  :is(#page-play,#gslFloat,#page-melodie) .gsl-ed-head button{flex:1 1 auto;min-width:34px;height:36px;padding:0 4px;font-size:.6rem}
}
@media (max-height:480px){:is(#page-play,#gslFloat,#page-melodie) .gsl-ed{top:4px;bottom:4px;max-height:none}:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-hint{display:none}}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-hint{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx3)}
@media (max-width:560px){:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-hint{display:none}}
:is(#page-play,#gslFloat) .gsl-layer .gsl-od{color:#e5484d;font-size:.7rem;padding:2px 5px;border:1px solid transparent;border-radius:5px}
:is(#page-play,#gslFloat) .gsl-layer .gsl-od.on{background:#e5484d;color:#fff;animation:gslPulse 1s infinite}
:is(#page-play,#gslFloat) .gsl-layer.dubbing{box-shadow:inset 0 0 0 1px #e5484d}
:is(#page-play,#gslFloat) .gsl-roll-head{position:absolute;top:0;bottom:0;left:0;width:2px;background:var(--tx);opacity:0;pointer-events:none}
/* Éditeur de piste partagé (clip-editor.js) : colonne gauche (noms de notes + libellé de la bande de
   vélocité), colonne grille + bande de vélocité, barre d'actions de la sélection. */
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-side{flex:none;position:sticky;left:0;z-index:2;background:linear-gradient(var(--sf),var(--sf)),var(--bg);display:flex;flex-direction:column}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-side .gsl-ed-keys{position:static;width:auto!important}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys div[style]{box-shadow:inset -3px 0 0 var(--nc)}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys .root{font-weight:700;text-decoration:underline;text-underline-offset:2px}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys small{font-size:.8em;opacity:.6;margin-left:2px}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-vlbl{height:56px;flex:none;display:flex;align-items:center;justify-content:flex-end;padding-right:4px;margin-top:4px;font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3)}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-col{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-col .gsl-ed-grid{flex:none;width:100%}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-vel{position:relative;height:56px;flex:none;border-radius:6px;background:color-mix(in srgb,var(--bg) 70%,var(--sf));touch-action:none;cursor:ns-resize}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-vel svg{position:absolute;inset:0;width:100%;height:100%}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-vbar{stroke:var(--nc,hsl(var(--gsl-h,12) 70% 58%));stroke-width:5;stroke-linecap:round;vector-effect:non-scaling-stroke}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-vbar.dr{stroke:#9aa3ad}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-vbar.sel{stroke-width:8}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-vmid{stroke:var(--bd);stroke-width:1;stroke-dasharray:4 4;vector-effect:non-scaling-stroke}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-row.alt{fill:color-mix(in srgb,var(--bg) 70%,var(--sf))}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-row.out{fill:color-mix(in srgb,#e5484d 8%,var(--bg))}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-selbar{display:flex;flex-wrap:wrap;align-items:center;gap:5px;padding:4px;border-radius:8px;background:color-mix(in srgb,var(--c-root) 10%,var(--s1));border:1px solid color-mix(in srgb,var(--c-root) 35%,var(--bd))}
/* Toujours en place (grisée sans sélection) : l'afficher/masquer faisait sauter le piano-roll en hauteur. */
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-selbar:not(.on){opacity:.35;pointer-events:none;filter:grayscale(1)}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-selbar button{height:34px;min-width:38px;padding:0 8px;border-radius:8px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.62rem;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:5px;touch-action:manipulation}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-selbar button svg{width:15px;height:15px;flex:none}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-selbar button:disabled{opacity:.35;cursor:default}
:is(#page-play,#gslFloat,#page-melodie) .gsl-es-sep{width:1px;align-self:stretch;background:var(--bd)}
@media (max-width:560px){:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-selbar button span{display:none}:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-selbar button{flex:1 1 auto;min-width:34px}}
/* Intégré à une page (Séquenceur) : dans le flux, pas en surimpression. */
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed.gsl-ed-embed{position:relative;left:auto;right:auto;bottom:auto;z-index:auto;max-height:none;box-shadow:none;border-radius:10px;background:var(--s1);border-color:var(--bd)}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed.gsl-ed-embed .gsl-ed-side{background:var(--sf)}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed.gsl-ed-embed .gsl-ed-body{max-height:calc(100vh - 260px);min-height:240px}
/* Saisie non bloquante (gsAskText, surface.js) — remplace prompt() qui figeait la lecture. */
.gs-ask{position:fixed;left:50%;top:max(70px,12vh);transform:translateX(-50%);z-index:10050;display:flex;align-items:center;gap:6px;flex-wrap:wrap;
  width:min(420px,calc(100vw - 24px));padding:10px 12px;border-radius:12px;background:var(--sf);border:1px solid var(--c-root);box-shadow:0 14px 40px rgba(0,0,0,.5)}
.gs-ask label{width:100%;font-family:var(--fm);font-size:.62rem;color:var(--tx2)}
.gs-ask input{flex:1 1 100%;min-width:0;height:36px;box-sizing:border-box;border-radius:8px;border:1px solid var(--bd2);background:var(--s1);color:var(--tx);padding:0 10px;font-family:var(--fm);font-size:.8rem}
.gs-ask-btns{display:flex;gap:6px;justify-content:flex-end;width:100%}
.gs-ask-btn{height:32px;padding:0 14px;border-radius:8px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.7rem;font-weight:700;cursor:pointer}
.gs-ask-ok{background:var(--c-root);border-color:var(--c-root);color:#111}

/* Pitch wheel intégrée aux zones Piano/Mini (même moteur que celle de la grille, surface.js) */
#page-play .gsk-host.gsk-has-pw .gsk-kb{left:46px}
#page-play .gsk-host.gsk-has-pw.gsk-pw-right-host .gsk-kb{left:0;right:46px}
#page-play .gsk-pw{position:absolute;top:28px;bottom:4px;left:4px;width:38px;margin:0;border-radius:19px;z-index:4}
#page-play .gsk-pw.gsk-pw-right{left:auto;right:4px}
/* Sélecteur à nombreux groupes (sons) : colonnes équilibrées d'au moins 150px, titres jamais chevauchés. */
/* colonnes remplies l'une après l'autre : une catégorie longue continue sur la colonne suivante (moins de défilement),
   son titre ne reste jamais seul en bas de colonne */
.gs-picker-pop.gs-picker-many .gs-picker-cols{display:block;column-width:160px;column-gap:14px}
.gs-picker-pop.gs-picker-many .gs-picker-col{display:block;break-inside:auto;margin:0 0 10px;border-left:none;padding-left:0}
.gs-picker-pop.gs-picker-many .gs-picker-group-lbl{break-after:avoid;break-inside:avoid}
.gs-picker-pop.gs-picker-many .gs-picker-item{break-inside:avoid}
.gs-picker-pop.gs-picker-many .gs-picker-col+.gs-picker-col{border-left:none;padding-left:0}
.gs-picker-pop.gs-picker-many .gs-picker-group-lbl{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-bottom:1px solid var(--bd);padding-bottom:3px;margin-bottom:3px}
.gs-picker-pop.gs-picker-many .gs-picker-item{width:100%;text-align:left}
#page-play .gsk-lbl small{font-size:.8em;opacity:.65;margin-left:1px}
/* Mode édition : bord de la surface disponible */
#page-play .gsz-canvas.gsz-edit{outline:2px dashed color-mix(in srgb,var(--c-root) 55%,transparent);outline-offset:-1px;border-radius:10px;
  background-image:linear-gradient(color-mix(in srgb,var(--c-root) 7%,transparent) 1px,transparent 1px),linear-gradient(90deg,color-mix(in srgb,var(--c-root) 7%,transparent) 1px,transparent 1px);
  background-size:5% 5%}
/* Glisser depuis la bibliothèque : vignette sous le doigt + aperçu exact de l'emplacement sur la toile */
.gsz-ghost{position:fixed;z-index:10040;transform:translate(-50%,-120%);pointer-events:none;display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:9px;
  background:var(--sf);border:1px solid var(--c-root);color:var(--tx);font-family:var(--fm);font-size:.65rem;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.45)}
.gsz-ghost .gsz-ic{display:inline-flex;width:16px;height:16px}.gsz-ghost .gsz-ic svg{width:100%;height:100%}
#page-play .gsz-drop{position:absolute;z-index:30;pointer-events:none;box-sizing:border-box;border:2px solid var(--c-root);border-radius:10px;
  background:color-mix(in srgb,var(--c-root) 22%,transparent);display:flex;align-items:center;justify-content:center;transition:left .08s,top .08s,width .08s,height .08s}
#page-play .gsz-drop.gsz-drop-split{border-style:dashed;background:color-mix(in srgb,var(--c-root) 12%,transparent)}
#page-play .gsz-drop span{display:flex;align-items:center;gap:6px;font-family:var(--fm);font-size:.7rem;font-weight:700;color:var(--tx);background:color-mix(in srgb,var(--bg) 75%,transparent);padding:4px 8px;border-radius:7px}
/* Contrôleurs libres en édition : cadre commun avec les zones, l'ancienne poignée d'angle disparaît */
#page-play .gsz-edit .gs-func-free-widget .gs-func-free-resize{display:none}
#page-play .gs-func-free-widget>.gsz-wframe{inset:-2px;z-index:6}
#page-play .gs-func-free-widget>.gsz-wframe .gsz-head{top:4px;left:4px;font-size:max(10px,.55rem);padding:2px 3px 2px 6px;max-width:calc(100% - 8px)}
#page-play .gsz-edit .gs-func-free-widget{overflow:visible}
#page-play .gs-func-free-widget>.gsz-wframe .gsz-hbtn{width:22px;height:20px}
.gs-ask.gs-ask-confirm label{font-size:.75rem;color:var(--tx);line-height:1.4}
.gs-ask-del{background:#e5484d;border-color:#e5484d;color:#fff}
#page-play .gss-fxrow{display:flex;align-items:flex-start;gap:6px;overflow-x:auto;padding-bottom:4px}
#page-play .gss-fxrow>.gsc-arrow{margin-top:13px}
#page-play .gss-fx{flex:none;display:flex;flex-direction:column;align-items:center;gap:4px;padding:6px;border-radius:10px;border:1px solid var(--bd2);background:var(--s1);opacity:.7}
#page-play .gss-fx.on{opacity:1;border-color:var(--c-root)}
#page-play .gss-fx-knobs{display:flex;gap:4px}
#page-play .gss-fx .tb303-knob-wrap{width:62px;min-width:62px}

/* ═══ Surface 0.3 — panneau ARP flottant (surface-arp.js) ═══════════════════════════════════════════════ */
#page-play .gsa-panel{position:absolute;right:8px;top:36px;z-index:46;width:min(560px,calc(100% - 16px));max-height:calc(100% - 44px);overflow-y:auto;
  padding:10px 12px;border-radius:12px;background:color-mix(in srgb,var(--sf) 95%,transparent);border:1px solid var(--c-root);box-shadow:0 16px 40px rgba(0,0,0,.5);
  display:flex;flex-direction:column;gap:6px}
#page-play .gsa-panel.gsa-pinned{box-shadow:0 6px 18px rgba(0,0,0,.35)}
#page-play .gsa-head{display:flex;align-items:center;gap:5px;flex-wrap:wrap;font-family:var(--fd);font-size:.8rem;color:var(--tx)}
#page-play .gsa-head b{margin-right:auto}
#page-play .gsa-sec{font-family:var(--fm);font-size:max(10px,.52rem);font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--tx2);margin-top:2px}
#page-play .gsa-chips{display:flex;flex-wrap:wrap;gap:4px}
#page-play .gsa-chip{height:28px;padding:0 10px;border-radius:14px;border:1px solid var(--bd2);background:var(--s1);color:var(--tx2);font-family:var(--fm);font-size:.6rem;font-weight:700;cursor:pointer;touch-action:manipulation}
#page-play .gsa-chip.on{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-play .gsa-knobs{display:flex;flex-wrap:wrap;gap:6px 10px;margin-top:4px}
#page-play .gsa-knobs .tb303-knob-wrap{width:64px;min-width:64px}

#page-play .gsz-drop .gsz-ic{color:var(--c-root)}
#page-play .gsa-panel{width:min(640px,calc(100% - 16px))}
#page-play .gsa-body{display:flex;gap:12px;flex-wrap:wrap}
#page-play .gsa-col{flex:1.3 1 260px;display:flex;flex-direction:column;gap:6px;min-width:0}
#page-play .gsa-col2{flex:1 1 220px;padding-left:12px;border-left:1px solid var(--bd)}
#page-play .gsa-row{display:flex;align-items:center;gap:8px}
#page-play .gsa-lbl{font-family:var(--fm);font-size:max(10px,.55rem);font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--tx2);min-width:64px}
#page-play .gsa-pattern{position:relative;height:72px;border-radius:8px;background:var(--bg);border:1px solid var(--bd2);overflow:hidden}
#page-play .gsa-pattern.demo{opacity:.55}
#page-play .gsa-pattern i{position:absolute;height:18%;box-sizing:border-box;border:2px solid var(--bg);border-radius:4px;background:color-mix(in srgb,var(--c-root) 55%,var(--s2))}
#page-play .gsa-pattern i.on{background:var(--c-root);box-shadow:0 0 8px var(--c-root)}
#page-play .gsa-knobs .tb303-knob-wrap{width:60px;min-width:60px}
#page-play .gsa-knobs .tb303-knob-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:60px;display:block;text-align:center}

/* ═══ Surface 0.3 — barre de jeu : réglages en tête, états « contour » ════════════════════════════════ */
/* Les groupes de la barre de jeu sont séparés uniquement par de l'espace. */
#page-play .gs-setup-sep{display:none;flex:none;width:1px;align-self:stretch;margin:4px 4px}
@media (min-width:760px){#page-play .gs-setup-sep{display:block}}
/* Vibrato et EXP sont des modes de jeu actifs par défaut : un contour suffit, le plein reste réservé aux
   bascules ponctuelles (ARP, lecture…). */
/* Barre de jeu : un bouton ACTIF garde son fond et la couleur de son texte — seul le contour s'allume
   (demande utilisateur). Vaut pour tous les boutons de la rangée, dont V et EXP actifs par défaut. */
#page-play #gsOctRow .gs-mode-btn.on,#page-play #gsOctRow button.on{background:var(--s2)!important;color:var(--tx)!important;border-color:var(--c-root)!important;box-shadow:inset 0 0 0 1px var(--c-root)!important}
#page-play #gsBackingPlayBtn.str-play-pending{animation:rd-queued-pulse .5s ease-in-out infinite}

#page-play .gss-fxrow{align-items:stretch}
#page-play .gss-fxrow>.gsc-arrow{align-self:flex-start}
#page-play .gss-fx{justify-content:flex-start;min-width:84px}
#page-play .gss-fx.hidden-in-chain{border-style:dashed}
#page-play .gss-fx-top{display:flex;align-items:center;gap:4px}
#page-play .gss-eye{width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;border-radius:6px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx2);cursor:pointer;padding:0}
#page-play .gss-eye.on{color:var(--c-root);border-color:var(--c-root)}
#page-play .gss-sub-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
#page-play .gss-activeonly{height:24px;font-size:max(10px,.55rem)}
#page-play .gss-activeonly.on{background:var(--c-root);border-color:var(--c-root);color:#111}

/* Mini : les étiquettes restent au-dessus des pastilles de gamme (elles se chevauchaient) */
#page-play .gsk-mini .gsk-lbl{margin-bottom:15px}
#page-play .gsk-mini .gsk-in::after{height:3px;bottom:4px}
#page-play .gsk-lbl .gsk-deg{display:block;font-size:.85em;color:var(--gsk-c,var(--c-root));filter:brightness(.8);text-align:center;margin-bottom:1px}
#page-play .gsk-voice{margin-left:auto;height:20px;max-width:55%;padding:0 8px;border-radius:5px;border:1px solid var(--bd2);background:var(--s1);color:var(--tx2);
  font:700 max(10px,.58rem)/1 var(--fm);cursor:pointer;touch-action:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#page-play .gsk-voice.own{color:var(--tx);border-color:var(--c-root)}
#page-play .gsk-w .gsk-lbl .gsk-deg{color:#555;filter:none}
#page-play .gsk-b .gsk-lbl .gsk-deg{color:#bbb;filter:none}

/* ARP compact (popup + zone) — surface-arp.js */
#page-play .gsa-panel{width:min(480px,calc(100% - 16px));padding:8px 10px;gap:5px}
#page-play .gsa-host{display:flex;flex-direction:column;gap:5px;min-width:0}
#page-play .gsz-body.gsa-host{padding:6px 8px;overflow-y:auto;touch-action:pan-y;background:var(--s1);border:1px solid var(--bd);border-radius:10px}

#page-play .gsz-body.gsa-host .gsa-pattern{height:44px;flex:none}
#page-play .gsz-body.gsa-host .gsa-knobs .tb303-knob-wrap{width:56px;min-width:56px}
@media (max-height:520px){#page-play .gsz-body.gsa-host .gsa-pattern{display:none}}
#page-play .gsa-head{gap:4px}
#page-play .gsa-head b{margin-right:auto;font-size:.75rem}
#page-play .gsa-chip{height:28px;padding:0 9px;font-size:max(10px,.56rem)}
#page-play .gsa-sels{display:flex;gap:6px;flex-wrap:wrap}
#page-play .gsa-f{display:flex;flex-direction:column;gap:2px;min-width:0}
#page-play .gsa-f>span{font-family:var(--fm);font-size:max(10px,.48rem);font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--tx2)}
#page-play .gsa-pattern{height:34px}
#page-play .gsa-knobs{gap:0 2px;margin-top:0}
#page-play .gsa-knobs .tb303-knob-wrap{width:52px;min-width:52px}
#page-play .gsa-knobs .tb303-knob-label{max-width:52px}

/* Repérage inter-zones « même note, même octave » (surface-zones.js) : contour blanc pulsé, comme l'écho
   de la grille, distinct de la surbrillance de jeu. */
#page-play .gs-cell.gs-echo-x,#page-play .gs-hex-cell.gs-echo-x{border-color:#fff!important;box-shadow:0 0 0 2px #fff,0 0 12px 2px rgba(255,255,255,.7)!important;animation:gsEchoPulse 1.3s ease-in-out infinite}
#page-play .gs-hex-cell.gs-echo-x{filter:brightness(1.25)}
/* Piano : même note jouée sur une autre zone / un autre instrument. Le blanc seul disparaissait sur les touches
   blanches : la touche est TEINTÉE de la couleur de la zone (--gsk-c, sinon l'accent) avec un contour épais, un halo
   et une légère pulsation — nettement distincte de la touche jouée (.gsk-on, aplat plein). */
#page-play .gsk-key.gsk-echo{--gsk-ec:var(--gsk-c,var(--c-root));box-shadow:inset 0 0 0 4px var(--gsk-ec),inset 0 0 0 6px rgba(255,255,255,.85),0 0 14px 4px var(--gsk-ec);z-index:3;animation:gskEchoPulse 1.1s ease-in-out infinite}
/* couleur de note claire (Do) : gris, jamais blanc sur blanc */
#page-play .gsk-key.gsk-echo.gsk-lite{--gsk-ec:#7d828c;box-shadow:inset 0 0 0 4px var(--gsk-ec),inset 0 0 0 6px rgba(0,0,0,.45),0 0 14px 4px var(--gsk-ec)}
#page-play .gsk-w.gsk-echo{z-index:1;background:linear-gradient(color-mix(in srgb,var(--gsk-ec) 60%,transparent),color-mix(in srgb,var(--gsk-ec) 60%,transparent)),#f4f4f2!important}
#page-play .gsk-b.gsk-echo{z-index:4;background:linear-gradient(color-mix(in srgb,var(--gsk-ec) 75%,transparent),color-mix(in srgb,var(--gsk-ec) 75%,transparent)),#141416!important}
@keyframes gskEchoPulse{0%,100%{filter:brightness(1)}50%{filter:brightness(1.22)}}
@media(prefers-reduced-motion:reduce){#page-play .gs-cell.gs-echo-x,#page-play .gs-hex-cell.gs-echo-x,#page-play .gsk-key.gsk-echo{animation:none}}

/* Grilles supplémentaires (surface-grid2.js) : barre de titre compacte + cases de la grille principale */
#page-play .gsg-host{position:absolute;inset:3px;border-radius:9px;overflow:hidden;background:var(--s1)}
#page-play .gsg-host .gsg-grid{top:26px;left:4px;right:4px;bottom:4px;gap:4px}
#page-play .gsg-ctl{z-index:2}
/* Calque solo (notes d'accord + boîtes CAGED, surface-grid2.js) */
#page-play .gsg-cell{position:relative}
#page-play .gsg-cell.gsg-dim{opacity:.32}
#page-play .gsg-cell.gsg-sc{opacity:.78}
#page-play .gsg-cell.gsg-ct{opacity:1;box-shadow:inset 0 0 0 2px rgba(255,255,255,.92)}
#page-play .gsg-cell.gsg-ct-r{box-shadow:inset 0 0 0 3px #ffd24a,0 0 8px rgba(255,210,74,.55)}
#page-play .gsg-cell.gsg-off{opacity:.16;filter:saturate(.3)}
#page-play .gsg-cell.gsg-cg{--cg:#5aa9ff}
#page-play .gsg-cell.gsg-cg-C{--cg:#ff6b6b}
#page-play .gsg-cell.gsg-cg-A{--cg:#ffb84d}
#page-play .gsg-cell.gsg-cg-G{--cg:#5fd68a}
#page-play .gsg-cell.gsg-cg-E{--cg:#5aa9ff}
#page-play .gsg-cell.gsg-cg-D{--cg:#c58cff}
#page-play .gsg-cell.gsg-cg{background-image:linear-gradient(color-mix(in srgb,var(--cg) 34%,transparent),color-mix(in srgb,var(--cg) 34%,transparent))}
#page-play .gsg-cell.gsg-cg.gsg-ct{background-image:linear-gradient(color-mix(in srgb,var(--cg) 62%,transparent),color-mix(in srgb,var(--cg) 62%,transparent))}
#page-play .gsg-ctag{position:absolute;top:1px;left:3px;font-style:normal;font-weight:800;font-size:clamp(6px,34cqh,13px);line-height:1;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.7);pointer-events:none}
#page-play .gsg-note .gs-deg{font-size:clamp(6px,50cqh,20px);line-height:1;white-space:nowrap}
#page-play .gsg-note .gs-deg small{font-size:.6em;margin-left:1px}
#page-play .gsg-note .gs-deg .gsg-itv{display:block;font-size:.62em;text-align:center;opacity:.85;margin-bottom:1px}
/* Grille principale avec barre de titre (page à plusieurs instruments, cf. syncMainBars) */
#page-play .gsz-body.gsz-main-barred{padding-top:26px;box-sizing:border-box}
#page-play .gsz-main-ctl{border-radius:9px 9px 0 0}
/* Plusieurs grilles sur la page : cadre pour les distinguer */
#page-play .gsz-canvas.gsz-multigrid .gsz-t-grid>.gsz-body,#page-play .gsz-canvas.gsz-multigrid .gsz-t-grid2>.gsz-body,#page-play .gsz-canvas.gsz-multigrid .gsz-t-hex>.gsz-body{
  outline:2px solid color-mix(in srgb,var(--c-root) 45%,var(--bd2));outline-offset:-1px;border-radius:10px}

/* Hold des zones Piano / Mini / grilles supplémentaires */
#page-play .gsk-hold,#page-play .gsk-volbtn{height:20px;padding:0 8px;border-radius:5px;border:1px solid var(--bd2);background:var(--s1);color:var(--tx2);font:700 max(10px,.58rem)/1 var(--fm);cursor:pointer;touch-action:none}
#page-play .gsk-hold.on,#page-play .gsk-volbtn.on{border-color:var(--c-root);color:var(--tx);box-shadow:inset 0 0 0 1px var(--c-root)}
/* Zone Cordes (surface-strings.js) : barre de titre des claviers (.gsk-ctl, Archet/Violoning en .gsk-hold), toile dessous */
#page-play .gsst-host{position:absolute;inset:3px;border-radius:9px;overflow:hidden;background:var(--s1);touch-action:none;-webkit-touch-callout:none;user-select:none}
#page-play .gsst-cv{position:absolute;left:0;top:24px;width:100%;height:calc(100% - 24px);display:block}
#page-play .gsst-fit{color:var(--c-root);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* Barre d'accords de la zone Cordes : défile au doigt (pan-x), une pastille par accord (liseré = couleur de la fondamentale) */
#page-play .gsst-chords{position:absolute;left:0;right:0;top:24px;height:30px;display:flex;align-items:center;gap:4px;padding:0 6px;overflow-x:auto;overflow-y:hidden;
  scrollbar-width:none;touch-action:pan-x;background:color-mix(in srgb,var(--s2) 55%,transparent);border-top:1px solid var(--bd)}
#page-play .gsst-chords::-webkit-scrollbar{display:none}
#page-play .gsst-has-chords .gsst-cv{top:54px;height:calc(100% - 54px)}
/* Accords en colonne (réglage « Position des accords ») : à gauche ou à droite, défile à la verticale */
#page-play .gsst-chords-l .gsst-chords,#page-play .gsst-chords-r .gsst-chords{right:auto;left:0;top:24px;bottom:0;height:auto;width:68px;flex-direction:column;align-items:stretch;
  padding:4px;overflow-x:hidden;overflow-y:auto;touch-action:pan-y;border-top:0;border-right:1px solid var(--bd)}
#page-play .gsst-chords-r .gsst-chords{left:auto;right:0;border-right:0;border-left:1px solid var(--bd)}
#page-play .gsst-chords-l .gsst-cv{top:24px;height:calc(100% - 24px);left:68px;width:calc(100% - 68px)}
#page-play .gsst-chords-r .gsst-cv{top:24px;height:calc(100% - 24px);left:0;width:calc(100% - 68px)}
#page-play .gsst-chords-l .gsst-ch,#page-play .gsst-chords-r .gsst-ch{touch-action:pan-y;justify-content:center}
#page-play .gsst-ch{flex:0 0 auto;height:24px;padding:0 7px;border-radius:5px;border:1px solid var(--bd2);border-left:3px solid var(--gsst-c,var(--bd2));background:var(--s1);
  color:var(--tx2);font:700 max(10px,.6rem)/1 var(--fm);cursor:pointer;touch-action:pan-x;display:flex;align-items:center;gap:4px;white-space:nowrap}
#page-play .gsst-ch small{font-size:.85em;opacity:.7;font-weight:400}
#page-play .gsst-ch.on{color:var(--tx);border-color:var(--c-root);border-left-color:var(--gsst-c,var(--c-root));box-shadow:inset 0 0 0 1px var(--c-root)}
#page-play .gsst-ch-opt{border-left-width:1px;color:var(--tx2)}
/* Zone Orbital (surface-orbital.js) : barre de titre des claviers, roue SVG dessous (couleurs = mode de couleur de Surface) */
#page-play .gsor-host{position:absolute;inset:3px;border-radius:9px;overflow:hidden;background:var(--s1);touch-action:none;-webkit-touch-callout:none;user-select:none}
#page-play .gsor-wheel{position:absolute;left:0;right:0;top:24px;bottom:0}
#page-play .gsor-svg{display:block;overflow:visible}
/* Pavés de la roue = cases de la grille transposées en SVG (mêmes teintes, mêmes états, mêmes réglages d'effets) :
   fond --s3 teinté 22 % de la couleur de la note, contour 1,5 px de la note, étiquette de la couleur de la note. */
#page-play .gsor-pad{--gsor-pk:1;transform:scale(var(--gsor-pk));transform-box:fill-box;transform-origin:center;transition:transform .06s}
#page-play .gsor-cell{fill:color-mix(in srgb,var(--s3) 78%,var(--gsor-c,var(--s3)) 22%);stroke:var(--gsor-c,var(--bd));stroke-width:1.5;
  transition:fill var(--gs-on-dur,.06s),stroke var(--gs-on-dur,.06s)}
#page-play .gsor-lbl{fill:var(--gsor-c,var(--tx3));font-family:var(--fm);font-weight:700;text-anchor:middle;pointer-events:none}
#page-play .gsor-sub{fill:color-mix(in srgb,var(--gsor-c,var(--tx3)) 75%,var(--tx3));font-family:var(--fm);text-anchor:middle;pointer-events:none}
/* tonique : repère franc, comme .gs-cell.gs-root */
/* note de passage (moitié horaire d'une cellule coupée) : un peu plus discrète ; placé AVANT les états (.on, .echo…) qui la recouvrent */
#page-play .gsor-pad.gsor-passing .gsor-cell{fill:color-mix(in srgb,var(--s3) 90%,var(--gsor-c,var(--s3)) 10%);stroke:color-mix(in srgb,var(--gsor-c,var(--bd)) 68%,var(--bd) 32%);stroke-width:1.2}
#page-play :where(.gsor-fill) .gsor-pad.gsor-passing .gsor-cell{fill:color-mix(in srgb,var(--s3) 82%,var(--gsor-c,var(--s3)) 18%);stroke:color-mix(in srgb,var(--bd) 55%,var(--gsor-c,var(--bd)) 45%)}
#page-play .gsor-pad.gsor-passing .gsor-lbl{fill:color-mix(in srgb,var(--gsor-c,var(--tx3)) 75%,var(--tx3))}
#page-play .gsor-pad.gsor-root .gsor-cell{fill:color-mix(in srgb,var(--s3) 60%,var(--gsor-c,var(--c-root)) 40%);stroke-width:2.5}
/* cellule-accord : comme .gs-chord-cell (aplat de la fondamentale, contour plus épais) */
#page-play .gsor-pad.gsor-chord .gsor-cell{fill:color-mix(in srgb,var(--s3) 55%,var(--gsor-c,var(--c-root)) 45%);stroke-width:2.5}
#page-play .gsor-pad.gsor-chord .gsor-lbl{fill:color-mix(in srgb,var(--tx) 30%,var(--gsor-c,var(--tx)) 70%)}
#page-play .gsor-pad.gsor-off .gsor-cell{fill:var(--s2);stroke:var(--bd);stroke-dasharray:4 3;opacity:.4}
/* style « plein » de la grille (gsColorStyle) : pavé entier teinté, contour adouci */
#page-play .gsor-fill .gsor-cell{fill:color-mix(in srgb,var(--s3) 70%,var(--gsor-c,var(--s3)) 30%);stroke:color-mix(in srgb,var(--bd) 40%,var(--gsor-c,var(--bd)) 60%);stroke-width:1}
#page-play .gsor-fill .gsor-pad.gsor-root .gsor-cell{fill:color-mix(in srgb,var(--s3) 45%,var(--gsor-c) 55%);stroke:var(--gsor-c);stroke-width:2}
/* note JOUÉE : aplat franc + contour 3 px + halo, texte sombre, pavé enfoncé (--gs-press-scale, réglable) */
#page-play .gsor-pad.on{--gsor-pk:var(--gs-press-scale,.85);transition:transform .06s}
#page-play .gsor-pad.on .gsor-cell{fill:var(--gsor-c,var(--c-root));stroke:var(--gsor-c,var(--c-root));stroke-width:3;transition:none;
  filter:drop-shadow(0 0 2px var(--gsor-c,var(--c-root))) drop-shadow(0 0 8px var(--gsor-c,var(--c-root)))}
#page-play .gsor-pad.on .gsor-lbl,#page-play .gsor-pad.on .gsor-sub{fill:#111}
#page-play.gs-fx-reduced .gsor-pad.on .gsor-cell{filter:drop-shadow(0 0 2px var(--gsor-c,var(--c-root)))}
#page-play.gs-fx-off .gsor-pad.on .gsor-cell{filter:none}
/* ÉCHO (même hauteur jouée ailleurs) : contour blanc pulsant, comme .gs-cell.gs-echo */
#page-play .gsor-pad.echo .gsor-cell{stroke:#fff;stroke-width:4;animation:gsorEchoPulse 1.3s ease-in-out infinite}
@keyframes gsorEchoPulse{0%,100%{filter:drop-shadow(0 0 3px rgba(255,255,255,.6))}50%{filter:drop-shadow(0 0 8px rgba(255,255,255,.9))}}
#page-play.gs-fx-off .gsor-pad.echo .gsor-cell{animation:none}
/* flash d'attaque : MÊME animation et mêmes réglages que .gs-cell.gs-flashing (Paramètres > Effets) */
#page-play .gsor-pad.gsor-flashing{animation:gs-cell-flash var(--gs-flash-dur,.25s) cubic-bezier(.2,.7,.3,1)}
#page-play.gs-fx-off .gsor-pad.gsor-flashing{animation:none}
/* onde dans le pavé (palier Complets) */
#page-play .gsor-ripple{fill:none;stroke:var(--gsor-c,var(--c-root));stroke-width:3;opacity:0;transform-box:fill-box;transform-origin:center;
  animation:gsor-ripple .55s ease-out forwards;pointer-events:none}
@keyframes gsor-ripple{from{transform:scale(.06);opacity:var(--gsor-op,1)}to{transform:scale(1);opacity:0}}
#page-play.gs-rm .gsor-pad,#page-play.gs-rm .gsor-pad.on{transition:none}
@media (prefers-reduced-motion:reduce){#page-play .gsor-pad.gsor-flashing{animation:none}#page-play .gsor-ripple{display:none}}
/* Backing : notes de l'accord joué cerclées (pointillé), notes communes avec l'accord précédent en trait plein */
#page-play .gsor-pad.bk .gsor-cell{stroke:var(--tx);stroke-width:2.5;stroke-dasharray:5 3}
#page-play .gsor-pad.bk.common .gsor-cell{stroke-dasharray:none;stroke-width:3.5}
/* Paramètres > Surface > MIDI : mesure de latence (application Android) */
.gs-midi-latout{flex-basis:100%;font-family:var(--fm);font-size:max(10px,.6rem)}
.gs-midi-lattab{border-collapse:collapse;margin:4px 0}
.gs-midi-lattab th,.gs-midi-lattab td{padding:2px 8px;text-align:right;color:var(--tx2);font-weight:400;white-space:nowrap}
.gs-midi-lattab th{color:var(--tx3)}
.gs-midi-lattab td:first-child{text-align:left;color:var(--tx)}
/* potentiomètres associés : nom du paramètre en entier (les étiquettes de presets sont tronquées ailleurs) */
#gsMidiPanel .gs-preset-load{max-width:none;text-overflow:clip}
/* Paramètres > Surface > MIDI : appareils détectés (entrées : état + dernier message, sorties) */
.gs-midi-devs{display:flex;flex-direction:column;gap:2px;margin:4px 0 6px;font-family:var(--fm);font-size:max(10px,.6rem)}
.gs-midi-devrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:2px 0;color:var(--tx2)}
.gs-midi-devrow b{color:var(--tx);font-weight:600}
.gs-midi-devhead{color:var(--tx3);text-transform:uppercase;letter-spacing:.06em;margin-top:4px}
.gs-midi-dot{width:8px;height:8px;border-radius:50%;background:var(--bd2);flex:0 0 auto}
.gs-midi-dot.on{background:#34c759}
.gs-midi-dot.busy{background:#ff9f0a}
.gs-midi-state{color:var(--tx3)}
.gs-midi-act{margin-left:auto;color:var(--tx2);border-radius:4px;padding:0 4px}
.gs-midi-act.gs-midi-act-on{animation:gsMidiAct .6s ease-out}
@keyframes gsMidiAct{from{background:color-mix(in srgb,var(--c-root) 45%,transparent);color:var(--tx)}to{background:transparent}}
/* mode guide (Backing) : pavés hors de l'accord estompés, toujours jouables ; note reçue en MIDI : contour pointillé */
#page-play .gsor-pad.gsor-dim{opacity:.32;transition:opacity .2s}
#page-play .gsor-pad.gsor-ext .gsor-cell{stroke-dasharray:6 3}
/* même classe de hauteur que l'accord tenu : contour clair fixe (le .echo blanc pulsant reste pour la hauteur exacte) */
#page-play .gsor-pad.gsor-pc .gsor-cell{stroke:color-mix(in srgb,var(--gsor-c,var(--c-root)) 55%,#fff 45%);stroke-width:3;fill:color-mix(in srgb,var(--s3) 62%,var(--gsor-c,var(--c-root)) 38%)}
/* couture d'octave (Mélodique) : trait radial discret entre le dernier et le premier secteur ; plein en mode spirale */
#page-play .gsor-seam{stroke:var(--tx2);stroke-width:2.5;stroke-linecap:round;stroke-dasharray:1 5;opacity:.75;pointer-events:none}
#page-play .gsor-seam.gsor-spiral{stroke:var(--c-root);stroke-dasharray:none;opacity:.85}
/* forme de l'accord tenu : relie les centres des cellules de notes tenues */
#page-play .gsor-poly{fill:color-mix(in srgb,var(--c-root) 14%,transparent);stroke:var(--c-root);stroke-width:2;stroke-linejoin:round;stroke-linecap:round;opacity:.6;pointer-events:none}
#page-play .gsor-rim{fill:var(--tx2);font-family:var(--fm);font-weight:700;text-anchor:middle;pointer-events:none}
#page-play .gsor-center{fill:var(--s2);stroke:var(--bd2)}
#page-play .gsor-key{fill:var(--tx);font-family:var(--fm);font-weight:700;text-anchor:middle;pointer-events:none}
#page-play .gsor-key.gsor-chordname{fill:var(--c-root)}
#page-play .gsor-mapl{fill:var(--tx2);font-family:var(--fm);text-anchor:middle}
#page-play .gsor-mapl.gsor-forced{fill:var(--c-root)}
#page-play .gsor-pad.gsor-sel .gsor-cell{stroke:var(--gsor-c,var(--c-root));stroke-width:3;fill:color-mix(in srgb,var(--s3) 55%,var(--gsor-c,var(--c-root)) 45%)}
/* Coins : modificateurs / accords empruntés dans l'espace hors du cercle (quadrant moins disque), sous la roue */
#page-play .gsor-cn{--gsor-pk:1;transform:scale(var(--gsor-pk));transform-box:fill-box;transform-origin:center;transition:transform .06s}
#page-play .gsor-cnb{fill:transparent;stroke:color-mix(in srgb,var(--bd2) 55%,var(--gsor-c,var(--bd2)) 45%);stroke-width:1.5;stroke-linejoin:round}
#page-play .gsor-cn-mod .gsor-lbl{fill:var(--tx2)}
#page-play .gsor-cn-bor .gsor-lbl{fill:color-mix(in srgb,var(--tx) 30%,var(--gsor-c,var(--tx)) 70%)}
#page-play .gsor-cnl{font-family:var(--fm);pointer-events:none}
#page-play .gsor-cn.on{--gsor-pk:var(--gs-press-scale,.85)}
#page-play .gsor-cn.on .gsor-cnb{fill:transparent;stroke:#fff;stroke-width:3;transition:none}
#page-play .gsor-cn.gsor-flashing{animation:gs-cell-flash var(--gs-flash-dur,.25s) cubic-bezier(.2,.7,.3,1)}
#page-play.gs-fx-off .gsor-cn.gsor-flashing{animation:none}
#page-play.gs-rm .gsor-cn,#page-play.gs-rm .gsor-cn.on{transition:none}
@media (prefers-reduced-motion:reduce){#page-play .gsor-cn.gsor-flashing{animation:none}}
#page-play .gsk-key.gsk-held{box-shadow:inset 0 0 0 3px var(--c-root),inset 0 -6px 0 rgba(0,0,0,.25)}
#page-play .gsg-cell.gs-held{outline:3px dashed rgba(255,255,255,.85);outline-offset:-5px}

/* Fenêtre ARP déplaçable (barre de titre) */
#page-play .gsa-panel .gsa-head{cursor:grab;touch-action:none}
#page-play .gsa-panel.gsa-dragging{opacity:.92;box-shadow:0 22px 50px rgba(0,0,0,.55)}
#page-play .gsa-panel.gsa-dragging .gsa-head{cursor:grabbing}
#page-play .gsa-grip{display:inline-flex;color:var(--tx2);margin-right:2px}
#page-play .gsa-host:not(.gsa-panel) .gsa-head{cursor:default}

/* Surface 0.3 (retours 6) — paramètres de Surface utilisables au doigt : petits boutons agrandis sur écran
   tactile, menus déroulants des rangées d'accords compacts. */
#settingsModal #gsSettingsPanel .gs-chord-row-ctrl .dpk-select-btn{max-width:84px;padding:0 6px;overflow:hidden;text-overflow:ellipsis}
@media (pointer:coarse){
  #settingsModal #gsSettingsPanel .gs-mini-btn{min-height:34px;padding:6px 11px;font-size:.6rem}
  #settingsModal #gsSettingsPanel .gs-mode-btn{height:34px}
  #settingsModal #gsSettingsPanel .gs-midi-bar{gap:8px;row-gap:8px}
  #settingsModal #gsSettingsPanel .gs-preset-load,#settingsModal #gsSettingsPanel .gs-preset-del{min-height:32px}
}
/* Surface 0.3 (retours 6) — pastille du panoramique dans la barre de titre d'une zone en édition ; aperçu de
   dépôt d'un contrôleur libre. */
#page-play .gsz-pan{font-family:var(--fm);font-size:max(10px,.55rem);font-weight:700;color:var(--c-root);border:1px solid var(--c-root);border-radius:5px;padding:0 4px;margin-left:2px;white-space:nowrap;flex-shrink:0}
#page-play .gsz-drop.gsz-drop-w{border-style:dashed;z-index:31}
/* Surface 0.3 (retours 6) — BUG : les menus déroulants ouverts depuis la fenêtre Paramètres (z-index 9999)
   s'affichaient SOUS elle (1500) : « des boutons n'ouvrent pas de menu ». Menus passagers → au-dessus des
   fenêtres de l'app (Paramètres 9999, plein écran Gammes 10000), sous la palette de commandes (10500). */
.gs-picker-pop,.dpk-wrap{z-index:10200}

/* ═══ PARAMÈTRES — refonte 2026-09-24 ══════════════════════════════════════════════════════════════════
   Trois mises en page selon la place : bureau (≥900px) onglets en colonne à gauche ; tablette (600-899px)
   pastilles qui s'enroulent ; téléphone (<600px) plein écran, pastilles qui défilent. Le corps prend toute
   la hauteur restante (le pied de fenêtre reste en bas, même pour un onglet court). Grille CSS sur
   .settings-box sans changer le DOM : les règles d'aperçu du zoom (.settings-box-preview>…) restent valides. */
.settings-body{flex:1 1 auto;min-height:0}
.settings-tab{display:inline-flex;align-items:center;gap:7px}
.settings-tab-ic{width:15px;height:15px;flex:none;opacity:.75}
.settings-tab.active .settings-tab-ic{opacity:1}
.settings-section-title{margin-top:10px}
.settings-section>.settings-section-title:first-child{margin-top:0}
.settings-footer .settings-version-badge{margin-right:auto}
@media (min-width:900px){
  .settings-box{max-width:1080px;height:min(880px,92vh);display:grid;grid-template-columns:200px minmax(0,1fr);grid-template-rows:auto minmax(0,1fr) auto}
  .settings-box>.settings-header{grid-column:1 / -1;grid-row:1}
  .settings-box>.settings-header .settings-search-wrap{margin-left:16px}
  .settings-box>.settings-tabs{grid-column:1;grid-row:2 / 3;flex-direction:column;gap:2px;padding:12px 8px;border-bottom:0;border-right:1px solid var(--bd);overflow-y:auto;overflow-x:hidden}
  .settings-box>.settings-body{grid-column:2;grid-row:2;padding:16px 22px 22px}
  .settings-box>.settings-footer{grid-column:1 / -1;grid-row:3;justify-content:flex-end}
  .settings-tab{width:100%;justify-content:flex-start;padding:9px 12px;border:0;border-radius:8px;font-size:.72rem}
  .settings-tab:hover:not(.active){background:var(--s2)}
  .settings-tab.active{background:color-mix(in srgb,var(--c-root) 15%,transparent);color:var(--c-root)}
  .settings-footer button{flex:0 0 auto;min-width:130px}
}
@media (min-width:600px) and (max-width:899px){
  .settings-box{max-width:760px;height:min(1000px,92vh)}
  .settings-tabs{flex-wrap:wrap;gap:5px;padding:10px 14px;overflow:visible}
  .settings-tab{border:1px solid var(--bd);border-radius:18px;padding:6px 12px}
  .settings-tab.active{background:var(--c-root);border-color:var(--c-root);color:#111}
}
@media (max-width:599px){
  .settings-modal{padding:0}
  .settings-box{max-width:none;width:100%;height:100dvh;border:0;border-radius:0}
  /* Titre + outils sur la 1re ligne, recherche pleine largeur dessous (sans flex-wrap, les trois se
     partageaient une seule ligne et poussaient « Simple/Avancé » et ✕ hors de l'écran). */
  .settings-header{flex-wrap:wrap;row-gap:8px;padding:10px 12px;padding-top:max(10px,env(safe-area-inset-top))}
  .settings-title{white-space:nowrap}
  .settings-header .settings-search-wrap{order:3;flex:1 0 100%;margin:0;height:38px}  .settings-tabs{gap:6px;padding:10px 12px;scrollbar-width:none}
  .settings-tabs::-webkit-scrollbar{display:none}
  .settings-tab{border:1px solid var(--bd);border-radius:18px;padding:7px 12px;font-size:.7rem}
  .settings-tab.active{background:var(--c-root);border-color:var(--c-root);color:#111}
  .settings-body{padding:12px}
  .settings-footer{padding:8px 12px;padding-bottom:max(8px,env(safe-area-inset-bottom));gap:6px}
  .settings-footer .settings-version-badge{flex:1 0 100%;margin:0;text-align:center}
  .settings-seg button{min-height:34px}
}
/* Modules : en-tête (compteur, presets, remises à zéro) + une ligne compacte par module. */
.modvis-head{display:flex;flex-direction:column;gap:8px}
.modvis-hint{font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx3);line-height:1.5}
.modvis-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
.modvis-count{font-family:var(--fm);font-size:.64rem;font-weight:700;color:var(--tx);margin-right:auto}
.modvis-act{height:32px;padding:0 10px;border-radius:7px;border:1px solid var(--bd);background:transparent;color:var(--tx2);font-family:var(--fm);font-size:max(10px,.58rem);cursor:pointer}
.modvis-act:hover{border-color:var(--bd2);color:var(--tx)}
#modVisList{display:flex;flex-direction:column;gap:4px}
.modvis-item{display:block;border-radius:9px;background:var(--s1);border:1px solid var(--bd)}
.modvis-item.off .modvis-row,.modvis-item.off .modvis-zoom{opacity:.45}
.modvis-item-row{display:flex;align-items:center;gap:8px;padding:4px 8px}
.modvis-row,.modvis-row:not(.active){flex:1;min-width:0;display:flex;align-items:center;gap:8px;padding:8px 0;border:0;border-radius:0;background:none;color:var(--tx);font-family:var(--fb);font-size:.72rem;text-align:left;opacity:1}
.modvis-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.modvis-icon{width:18px;flex:none;text-align:center}
.modvis-zoomctl{display:flex;align-items:center;gap:6px;flex:0 1 260px;min-width:150px}
.modvis-zoomctl .settings-slider{flex:1;width:auto;min-width:0}
.modvis-zoomctl .modvis-zoom-val-wrap{margin-right:0}
.modvis-zoomctl.own .settings-val-input{color:var(--c-root);font-weight:700}
.modvis-switch{position:relative;flex:none;width:40px;height:24px;padding:0;border-radius:12px;border:1px solid var(--bd2);background:var(--s3);cursor:pointer}
.modvis-switch span{position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:var(--tx3);transition:left .15s,background .15s}
.modvis-switch.on{background:var(--c-root);border-color:var(--c-root)}
.modvis-switch.on span{left:19px;background:#fff}
/* aperçu du zoom : la liste elle-même ne doit pas être assombrie (seules les autres lignes le sont) */
.settings-box-preview .settings-section[data-settings-tab="modules"]>#modVisList#modVisList{opacity:1}
.settings-box-preview>.settings-search-wrap{opacity:.08}
@media (max-width:599px){
  .modvis-item-row{gap:6px;padding:3px 6px;flex-wrap:wrap}
  .modvis-zoomctl{order:5;flex:1 0 100%;padding:0 0 4px 22px}

  .modvis-count{flex:1 0 100%}
}
/* Barre de contrôle Groovebox : hauteur unique 28px pour tous les boutons, alignée sur les menus DPK
   (28px) et les subbars des autres modules (Backing/Gammes 26-28px). Avant : 24/28/33/34px mélangés
   (padding inline différent par bouton, "⬇ MIDI" sur 2 lignes). !important nécessaire : les boutons
   portent des paddings/tailles de police inline (cf. grooveInitPage). Les menus DPK sont exclus. */
#grooveCtrlBar button:not(.dpk-select-btn){height:28px!important;box-sizing:border-box;padding-top:0!important;padding-bottom:0!important;display:inline-flex;align-items:center;justify-content:center;line-height:1;white-space:nowrap;flex-shrink:0;font-size:max(10px,.55rem)!important;border-radius:4px}
#grooveCtrlBar .groove-mode-seg{height:28px;box-sizing:border-box}
#grooveCtrlBar .groove-mode-seg button{height:26px!important;border-radius:0}

/* Fonds pré-rendus (audit perf 2026-09-28, Mi 11 / Brave) : flou + saturation déjà cuits dans l'image par
   _bgBaked (app.js) → plus aucun filter à recalculer à chaque rastérisation du calque (bascule d'onglet,
   subbars mobiles). !important : les calques de texture d'interface ont plusieurs règles plus spécifiques. */
.page.module-bg-enabled.module-bg-baked::before,
body.ui-bg-baked #uiBgRotateLayer::before,
body.ui-bg-baked .topbar::before,
body.ui-bg-baked .subbar::before{filter:none!important}
/* Pas de verre dépoli temps réel. backdrop-filter re-floute tout ce qui passe derrière
   chaque carte/barre à chaque image (défilement, animations, bascule d'onglet) — premier coût GPU sur
   mobile, pire avec la protection anti-empreinte de Brave. Le fond étant déjà flouté (pré-rendu), le voile
   semi-opaque (--glass-opacity) seul donne un rendu quasi identique. */
/* Étendu à TOUTES les plateformes (Windows/WebView2 compris, demande explicite) : plus de verre dépoli temps réel. */
*,*::before,*::after{-webkit-backdrop-filter:none!important;backdrop-filter:none!important}

/* Contrôleur SURFACE (surface-link.js) — témoin de liaison, présent seulement en mode contrôleur. */
.surf-link-pill{position:relative;display:inline-flex;align-items:center;justify-content:center}
.surf-link-pill svg{display:block}
.surf-link-dot{position:absolute;top:3px;right:3px;width:6px;height:6px;border-radius:50%;background:#888}
.surf-link-pill[data-state="connected"] .surf-link-dot{background:#3ecf6e}
.surf-link-pill[data-state="idle"] .surf-link-dot{background:#e0b030}
.surf-link-pill[data-state="connecting"] .surf-link-dot,.surf-link-pill[data-state="lost"] .surf-link-dot{background:#e0b030;animation:surfLinkBlink 1s steps(2) infinite}
.surf-link-pill[data-state="error"] .surf-link-dot{background:#e55}
@keyframes surfLinkBlink{50%{opacity:.2}}

/* Page CONTRÔLE (surface-link.js, mode contrôleur uniquement) — grille de gros contrôles tactiles. */
#page-surfctl.active{display:flex;flex-direction:column;gap:8px;padding:8px;box-sizing:border-box;overflow-y:auto}
#page-surfctl .sc-head{display:flex;align-items:center;gap:8px;font-family:var(--fm);font-size:.62rem;color:var(--tx2);min-height:32px}
#page-surfctl .sc-state{flex:none;padding:5px 9px;border-radius:12px;border:1px solid var(--bd);background:var(--s2);color:var(--tx);cursor:pointer;white-space:nowrap}
#page-surfctl .sc-state::before{content:'';display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:6px;background:#888;vertical-align:1px}
#page-surfctl .sc-state[data-state="connected"]::before{background:#3ecf6e}
#page-surfctl .sc-state[data-state="idle"]::before{background:#e0b030}
#page-surfctl .sc-state[data-state="connecting"]::before,#page-surfctl .sc-state[data-state="lost"]::before{background:#e0b030;animation:surfLinkBlink 1s steps(2) infinite}
#page-surfctl .sc-state[data-state="error"]::before{background:#e55}
#page-surfctl .sc-info{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#page-surfctl .sc-quitbtn{flex:none;padding:5px 12px;border-radius:12px;border:1px solid var(--bd);background:transparent;color:var(--tx2);font-family:var(--fm);font-size:.62rem;cursor:pointer}
#page-surfctl .sc-editbtn{flex:none;padding:5px 12px;border-radius:12px;border:1px solid var(--bd);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.62rem;cursor:pointer}
#page-surfctl.sc-editing .sc-editbtn{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-surfctl .sc-grid{flex:1;display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:minmax(64px,auto);gap:6px;touch-action:none;user-select:none;-webkit-user-select:none}
#page-surfctl .sc-slot{position:relative;overflow:hidden;border-radius:10px;border:1px solid var(--bd);background:var(--s2);color:var(--tx);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;padding:4px;box-sizing:border-box;cursor:pointer;
  font-family:var(--fm);text-align:center;-webkit-tap-highlight-color:transparent}
#page-surfctl .sc-lbl{position:relative;font-size:.72rem;font-weight:700;line-height:1.15;word-break:break-word;pointer-events:none}
#page-surfctl .sc-val{position:relative;font-size:.6rem;color:var(--tx2);pointer-events:none}
#page-surfctl .sc-val:empty{display:none}
#page-surfctl .sc-slot.on{background:var(--c-root);border-color:var(--c-root)}
#page-surfctl .sc-slot.sc-colored{border:2px solid color-mix(in srgb,var(--c-root) 80%,transparent)}
#page-surfctl .sc-slot.on .sc-lbl,#page-surfctl .sc-slot.on .sc-val{color:#111}
#page-surfctl .sc-slot.sc-flash{animation:scFlash .25s ease-out}
@keyframes scFlash{from{filter:brightness(1.8)}to{filter:none}}
#page-surfctl .sc-fill{position:absolute;left:0;bottom:0;top:0;width:0;background:var(--c-root);opacity:.35;pointer-events:none}
#page-surfctl .sc-slot.sc-vert .sc-fill{top:auto;right:0;width:auto;height:0}
#page-surfctl .sc-dot{display:none;position:absolute;width:22px;height:22px;margin:-11px 0 0 -11px;border-radius:50%;background:var(--c-root);box-shadow:0 0 0 3px rgba(0,0,0,.25);pointer-events:none}
#page-surfctl .sc-slot[data-kind="xy"] .sc-dot{display:block}
#page-surfctl .sc-slot[data-kind="xy"]{justify-content:flex-start}
#page-surfctl .sc-slot[data-kind="none"] .sc-lbl{color:var(--tx3)}
#page-surfctl.sc-editing .sc-slot{border-style:dashed}
#page-surfctl .sc-add{border-style:dashed;background:transparent;color:var(--tx2)}
/* Pages de l'onglet CONTRÔLE : Surface / Navigation / Mixeur / Pad (télécommande fusionnée). */
#page-surfctl .sc-pages{display:flex;gap:4px;padding:2px;border-radius:12px;background:var(--s2);border:1px solid var(--bd)}
#page-surfctl .sc-pages button{flex:1;padding:8px 4px;border:0;border-radius:10px;background:transparent;color:var(--tx2);font-family:var(--fm);font-size:.64rem;cursor:pointer}
#page-surfctl .sc-pages button.on{background:var(--c-root);color:#111;font-weight:700}
#page-surfctl [data-panel]{display:none}
#page-surfctl[data-page="surface"] [data-panel="surface"]{display:grid}
#page-surfctl[data-page="nav"] [data-panel="nav"],#page-surfctl[data-page="mix"] [data-panel="mix"],#page-surfctl[data-page="pad"] [data-panel="pad"]{display:flex}
#page-surfctl:not([data-page="surface"]) .sc-editbtn{display:none}
#page-surfctl .sc-panel{flex:1;flex-direction:column;gap:6px;min-height:0}
#page-surfctl .sc-panel button{min-height:44px;border-radius:10px;border:1px solid var(--bd);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.66rem;font-weight:700;cursor:pointer;-webkit-tap-highlight-color:transparent;touch-action:manipulation}
#page-surfctl .sc-panel button.on{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-surfctl .sc-panel button:active{filter:brightness(1.4)}
#page-surfctl .sc-panel button.sc-off{opacity:.4}
#page-surfctl .sc-panel button.sc-pulse{animation:tabPulse .6s ease-in-out infinite}
#page-surfctl .sc-pad.sc-held{border-style:solid;border-color:var(--c-root);background:color-mix(in srgb,var(--c-root) 18%,var(--s2))}
#page-surfctl .sc-progform{display:flex;gap:4px}
#page-surfctl .sc-proginput{flex:1;min-width:0;min-height:44px;box-sizing:border-box;padding:0 10px;border-radius:10px;border:1px solid var(--bd);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:16px}
#page-surfctl .sc-progform button{flex:none;padding:0 16px}#page-surfctl .sc-sec{font-family:var(--fm);font-size:max(10px,.58rem);color:var(--tx3);text-transform:uppercase;letter-spacing:.06em;margin-top:6px}
#page-surfctl .sc-sub{text-transform:none;letter-spacing:0;color:var(--tx2);margin-left:6px}
#page-surfctl .sc-roots{display:grid;grid-template-columns:repeat(6,1fr);gap:4px;touch-action:manipulation}
/* Même signal que le sélecteur de tonalité de FretFace (syncRootRows) : les notes non choisies sont estompées, la note active reste à pleine intensité — aucune couleur d'accent ajoutée */
#page-surfctl .sc-roots button{background:var(--nc);border-color:var(--nc);color:#000;min-height:46px;opacity:.55}
#page-surfctl .sc-roots button.on{background:var(--nc);border-color:var(--nc);color:#000;opacity:1;box-shadow:none;filter:none;font-weight:400} /* annule le fond d'accent des boutons .on du panneau */
#page-surfctl .sc-prog{font-family:var(--fm);font-size:.6rem;color:var(--tx2);text-align:center}
#page-surfctl .sc-tempo{display:grid;grid-template-columns:1fr 1fr 1.4fr 1fr 1fr 1.2fr;gap:4px;align-items:stretch}
#page-surfctl .sc-bpm{display:flex;align-items:center;justify-content:center;font-family:var(--fm);font-size:.8rem;font-weight:700;color:var(--tx)}
#page-surfctl .sc-btns{display:grid;grid-template-columns:repeat(3,1fr);gap:4px}
#page-surfctl .sc-mix{flex-direction:row;flex-wrap:wrap;gap:6px;touch-action:none}
#page-surfctl .sc-mixch{flex:1 0 calc(25% - 6px);display:flex;flex-direction:column;align-items:stretch;gap:4px;min-height:190px}
#page-surfctl .sc-mixfader{position:relative;flex:1;border-radius:10px;border:1px solid var(--bd);background:var(--s2);overflow:hidden;display:flex;align-items:flex-start;justify-content:center}
#page-surfctl .sc-mixfader .sc-fill{top:auto;right:0;width:auto;height:0}
#page-surfctl .sc-mixfader .sc-val{position:relative;margin-top:6px;font-family:var(--fm);font-size:.6rem;color:var(--tx)}
#page-surfctl .sc-mixlbl{font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx2);text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#page-surfctl .sc-pad{flex:1;min-height:320px;border-radius:14px;border:1px dashed var(--bd2,var(--bd));background:var(--s2);display:flex;align-items:flex-end;justify-content:center;padding:12px;touch-action:none;user-select:none;-webkit-user-select:none}
#page-surfctl .sc-pad span{font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx3);text-align:center;pointer-events:none}

/* Mode télécommande (surface-link.js, html.surf-link-sat) : la barre du haut ne garde que la navigation — ses
   transports (métronome, groovebox, tempo, drone, backing) et ses sélecteurs de tonalité agiraient sur le
   TÉLÉPHONE lui-même, pas sur l'appareil principal (tout cela est dans CONTRÔLE › Navigation). !important :
   plusieurs de ces éléments reçoivent un display en style inline depuis app.js. */
html.surf-link-sat #globalMetroBtn,html.surf-link-sat #globalGrooveBtn,html.surf-link-sat #globalBpmInput,
html.surf-link-sat #globalDroneBtn,html.surf-link-sat #globalStrPlayBtn,html.surf-link-sat #topbarStatus,
html.surf-link-sat #topbarRootRowWrap,html.surf-link-sat #desktopRootRowWrap,html.surf-link-sat .tb3,
html.surf-link-sat #miniTunerBar,html.surf-link-sat .tab-sep:has(+#globalMetroBtn){display:none!important}
/* La règle générique .page > :not(div:first-child) (enfants en colonne flexible qui s'étirent) ne s'applique
   pas aux blocs de la page CONTRÔLE : seuls les panneaux prennent la hauteur restante. */
#page-surfctl>.sc-head,#page-surfctl>.sc-pages{flex:none;flex-direction:row;min-height:0}
#page-surfctl>.sc-grid{flex:1 0 auto;flex-direction:initial}
#page-surfctl>.sc-panel{flex:1 0 auto}
html.surf-link-sat #sidebarMixerSep,html.surf-link-sat #sidebarMixerRow,html.surf-link-sat #sidebarMixerToggle,html.surf-link-sat #sidebarMixer{display:none!important}

/* ── Sampler (sampler.js) : fenêtre des samples utilisateur ── */
#page-play .gsd-pad.gsd-empty{background:transparent;border-style:dashed;opacity:.7}
#page-melodie .sq-key.dr.empty{opacity:.45;border-style:dashed}
.smp-ov{position:fixed;inset:0;z-index:10040;background:rgba(0,0,0,.45);display:flex;align-items:flex-start;justify-content:center;padding:max(40px,6vh) 12px 12px;box-sizing:border-box;overflow-y:auto}
.smp-panel{width:min(640px,100%);background:var(--sf);color:var(--tx);border:1px solid var(--bd2);border-radius:14px;box-shadow:0 14px 40px rgba(0,0,0,.5);padding:10px 12px 12px;box-sizing:border-box;display:flex;flex-direction:column;gap:10px;
  --smp-wave:var(--c-root);--smp-dim:color-mix(in srgb,var(--sf) 70%,transparent)}
.smp-head{display:flex;align-items:center;gap:8px}
.smp-title{flex:1;font-family:var(--fm);font-size:.8rem}
.smp-x{width:32px;height:32px;border-radius:8px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);display:grid;place-items:center;cursor:pointer;padding:0}
.smp-x svg,.smp-btn svg{width:14px;height:14px}
.smp-slots{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:5px}
@media (max-width:560px){.smp-slots{grid-template-columns:repeat(4,minmax(0,1fr))}}
.smp-slot{height:46px;border-radius:8px;border:1px dashed var(--bd2);background:transparent;color:var(--tx3);font-family:var(--fm);font-size:max(10px,.58rem);display:flex;flex-direction:column;align-items:flex-start;justify-content:space-between;padding:4px 6px;cursor:pointer;min-width:0;text-align:left}
.smp-slot span{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.smp-slot small{font-size:.9em;opacity:.7}
.smp-slot.full{border-style:solid;border-color:hsl(var(--smp-h) 50% 45%);background:hsl(var(--smp-h) 40% 22%);color:hsl(var(--smp-h) 70% 88%)}
.smp-slot.sel{outline:2px solid var(--c-root);outline-offset:1px}
.smp-slot.drop{outline:2px dashed var(--c-root);outline-offset:1px;background:var(--bg3,rgba(255,255,255,.08))}
.smp-slot{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}
.smp-slot.smp-live,#page-play .gsd-pad.smp-live{border:2px solid #e5484d;background:rgba(229,72,77,.28);color:#fff;opacity:1}
.smp-ed{display:flex;flex-direction:column;gap:8px}
.smp-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.smp-btn,.smp-tg{height:32px;padding:0 12px;border-radius:8px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.68rem;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;gap:6px}
.smp-btn:disabled{opacity:.4;cursor:default}
.smp-rec svg{color:#e5484d}
.smp-rec.on{background:#e5484d;border-color:#e5484d;color:#fff}.smp-rec.on svg{color:#fff}
.smp-tg.on{background:var(--c-root);border-color:var(--c-root);color:#111}
.smp-del{margin-left:auto;color:#e5484d}
.smp-sp{flex:1}
.smp-recmode{height:32px}
.smp-status{font-family:var(--fm);font-size:.66rem;color:var(--tx2);min-height:0;display:flex;align-items:center;gap:8px}
.smp-status.on{color:#e5484d;min-height:18px}
.smp-meter{display:inline-block;width:90px;height:6px;border-radius:3px;background:var(--s2);overflow:hidden}
.smp-meter>i{display:block;height:100%;background:#3ecf8e}
.smp-lbl{font-family:var(--fm);font-size:.62rem;color:var(--tx2)}
.smp-name{flex:1;min-width:0;height:32px;box-sizing:border-box;border-radius:8px;border:1px solid var(--bd2);background:var(--s1,var(--s2));color:var(--tx);padding:0 10px;font-family:var(--fm);font-size:.75rem}
.smp-wave{height:110px;border-radius:8px;background:var(--s2);border:1px solid var(--bd);overflow:hidden}
.smp-wave{position:relative}
.smp-wave canvas{display:block;touch-action:none;cursor:ew-resize}
.smp-wplay{position:absolute;top:6px;right:6px;display:flex;align-items:center;gap:5px;height:30px;padding:0 10px;border-radius:15px;border:1px solid var(--bd2);background:var(--s1,rgba(0,0,0,.55));color:var(--tx);font:inherit;font-size:max(11px,.62rem);cursor:pointer;backdrop-filter:blur(4px)}
.smp-keys{display:flex;height:44px;gap:1px;touch-action:none}
.smp-key{flex:1;min-width:0;border:1px solid var(--bd2);border-radius:0 0 4px 4px;background:var(--tx);opacity:.85;padding:0;cursor:pointer}
.smp-key.blk{background:var(--s2);height:62%;margin:0 -3px;z-index:1;flex:.7;opacity:1}
.smp-key.root{background:var(--c-root)}
.smp-key:active{filter:brightness(.7)}
.smp-wplay svg{width:14px;height:14px}
.smp-wplay.on{background:var(--c-root);color:#000;border-color:var(--c-root)}
.smp-knobs{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:6px 14px}
.smp-k{display:flex;align-items:center;gap:6px;font-family:var(--fm);font-size:.62rem;color:var(--tx2);min-width:0}
.smp-k>span{min-width:52px}
.smp-k[data-g="env"]>span{color:var(--c-root)}
.smp-k input{flex:1;min-width:60px}
.smp-k output{min-width:52px;text-align:right;color:var(--tx)}
.smp-exp .smp-lbl{margin-right:2px}
.smp-wave{--smp-env:rgba(255,255,255,.85)}
@media (max-width:560px){.smp-knobs{grid-template-columns:1fr}.smp-btn,.smp-tg{padding:0 9px}}
.smp-empty{font-size:.72rem;color:var(--tx3);padding:14px 4px}
.smp-foot{font-size:.6rem;color:var(--tx3)}
#grooveTrackRows .groove-track-badge{touch-action:none;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}
#grooveTrackRows .groove-swap-src{opacity:.5}
#grooveTrackRows .groove-swap-dst{outline:2px dashed var(--c-root);outline-offset:-2px;border-radius:6px;background:color-mix(in srgb,var(--c-root) 10%,transparent)}
@media (max-width:1023px){#page-rhythm .rh-col-clear-row .rh-clear-btn{height:30px;font-size:.7rem}}

/* ── Surface : mixeurs, tranches, pitch wheel locale (surface-mixer.js) ───────────────────────────────── */
#page-play .gsx{position:absolute;inset:3px;z-index:1;display:flex;min-width:0;min-height:0;touch-action:none;user-select:none;-webkit-user-select:none}
#page-play .gsx-host>.gsx{inset:0}
#page-play .gs-func-ext>.gs-func-free-label,#page-play .gs-func-ext>.gs-deg,#page-play .gs-func-ext>.gs-func-seg-label{display:none}
#page-play .gs-func-ext{overflow:hidden}
#page-play .gsx-mix{gap:3px;padding:2px;align-items:stretch;overflow-x:auto;overflow-y:hidden}
#page-play .gsx-ch{flex:1 1 0;min-width:30px;max-width:90px;display:flex;flex-direction:column;align-items:stretch;gap:3px;padding:3px 2px;border-radius:5px;background:var(--s2);border:1px solid var(--bd)}
#page-play .gsx-strip .gsx-ch{max-width:none}
#page-play .gsx-ch.gsx-master{background:color-mix(in srgb,var(--c-root) 12%,var(--s2))}
#page-play .gsx-ch.gsx-off{opacity:.45}
#page-play .gsx-fader{position:relative;flex:1 1 auto;min-height:24px;border-radius:4px;background:var(--s3);border:1px solid var(--bd);overflow:hidden;cursor:ns-resize;touch-action:none}
#page-play .gsx-fill{position:absolute;left:0;right:0;bottom:0;height:var(--gsx-v,0%);background:var(--c-root);opacity:.85}
#page-play .gsx-ch[style*="--gsx-h"] .gsx-fill{background:hsl(var(--gsx-h) 70% 55%)}
#page-play .gsx-ch.gsx-muted .gsx-fill{opacity:.3}
#page-play .gsx-fader.gsx-held{border-color:var(--c-root)}
#page-play .gsx-val{position:absolute;left:0;right:0;top:2px;text-align:center;font:700 max(10px,.55rem) var(--fm);color:var(--tx);text-shadow:0 0 3px var(--s1);pointer-events:none}
#page-play .gsx-btns{display:flex;gap:2px}
#page-play .gsx-btn{flex:1 1 0;min-width:0;height:20px;padding:0;border-radius:4px;border:1px solid var(--bd);background:var(--s3);color:var(--tx2);font:700 max(10px,.55rem) var(--fm);display:flex;align-items:center;justify-content:center;cursor:pointer;touch-action:manipulation}
#page-play .gsx-btn svg{width:10px;height:10px}
#page-play .gsx-btn.gsx-play{flex:0 0 auto;height:20px}
#page-play .gsx-btn.on{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-play .gsx-btn.gsx-m.on{background:#e5484d;border-color:#e5484d;color:#fff}
#page-play .gsx-btn.gsx-s.on{background:#f5c542;border-color:#f5c542;color:#111}
#page-play .gsx-lbl{font:700 max(10px,.5rem) var(--fm);color:var(--tx);text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#page-play .gsx-empty{margin:auto;font-size:.6rem;color:var(--tx3);text-align:center;padding:4px}
#page-play .gsx-pw{border-radius:6px;background:var(--s3);border:1px solid var(--bd);overflow:hidden;cursor:grab}
#page-play .gsx-pw-mid{position:absolute;left:0;right:0;top:50%;height:1px;background:var(--bd2)}
#page-play .gsx-pw.gsx-pw-h .gsx-pw-mid{left:50%;top:0;bottom:0;right:auto;width:1px;height:auto}
#page-play .gsx-pw-thumb{position:absolute;left:3px;right:3px;top:50%;height:0;background:var(--c-root);border-radius:3px;opacity:.85}
#page-play .gsx-pw.gsx-pw-h .gsx-pw-thumb{top:3px;bottom:3px;left:50%;right:auto;width:0;height:auto}
#page-play .gsx-pw.gsx-pw-active{border-color:var(--c-root)}
#page-play .gsx-pw-clip .gsx-pw-lbl{color:var(--c-danger,#e0533d)}
#page-play .gsx-pw-lbl{position:absolute;left:0;right:0;bottom:2px;text-align:center;font:700 max(10px,.5rem) var(--fm);color:var(--tx3);pointer-events:none}
/* Zone Groovebox : le module complet, défilable dans sa zone (la toile bloque sinon tout geste natif). */
#page-play .gsz-gb-body{overflow:auto;touch-action:pan-x pan-y;-webkit-user-select:none;user-select:none;display:block}
#page-play .gsz-gb{display:flex;flex-direction:column;min-width:0}
#page-play .gsz-gb>.sticky-toolbar{position:sticky;top:0;z-index:5;background:var(--bg)}
#page-play .gsz-gb>.wrap{padding:.4rem;gap:.4rem;overflow:visible;max-width:none}
/* Tiroir de bibliothèque : jamais de texte qui déborde du tiroir (libellés longs, police agrandie sur mobile,
   cf. capture « Groovebo », bascule « Libre » et « Momentané » coupés) — tout est borné et tronqué en « … »,
   le libellé complet reste dans l'infobulle. */
#page-play .gs-func-free-drawer{box-sizing:border-box;overflow-x:hidden}
#page-play .gs-func-free-drawer>*{min-width:0;max-width:100%;box-sizing:border-box}
#page-play .gs-func-free-drawer-item,#page-play .gsz-drawer-item{min-width:0;max-width:100%;box-sizing:border-box;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
#page-play .gsz-drawer-item>span:not(.gsz-ic),#page-play .gsz-drawer-item>b{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#page-play .gsz-drawer-item .gsz-ic{flex:none}
#page-play .gs-func-free-drawer-toggle button{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:4px 1px}
#page-play .gs-func-free-drawer-item{white-space:normal;overflow-wrap:anywhere;line-height:1.15;hyphens:auto}
#page-play .gs-func-free-drawer>*,#page-play .gsz-drawer-sec>*{flex-shrink:0}
#page-play .gs-func-free-drawer{width:118px;padding:7px 6px}
#page-play .gsz-drawer-item{font-size:max(10px,.56rem);gap:4px;padding:7px 4px;letter-spacing:-.01em}
#page-play .gs-func-free-drawer-toggle button{font-size:max(10px,.48rem);letter-spacing:-.02em}
/* ── Surface : barre de jeu (surface-live.js) — profils Complet/Live/Masquée retirés le 2026-10-25 ── */
/* Réserves symétriques : E/FX à gauche, jeu au centre, plein écran à droite.
   Les commandes latérales sont ancrées au parent, hors du défilement central. */
#page-play .gs-play-bar{--gs-controls-left:calc(3px + max(6px,env(safe-area-inset-left,6px)));--gs-controls-right:9px;position:relative;display:block;flex:0 0 auto;min-height:0;overflow:visible;box-sizing:border-box;padding:0 max(92px,calc(var(--gs-controls-left) + 82px))}
body.gs-fullscreen #page-play>.gs-play-bar,body.gs-full-width #page-play>.gs-play-bar{width:100%;max-width:none;margin:0}
#page-play .gs-play-bar #gsOctRow{mask-image:none;-webkit-mask-image:none}
/* Même retrait que les cellules plein cadre : corps de zone 3px + padding de la grille. */
#page-play #gsOctRow>.gs-setup-actions{position:absolute;left:var(--gs-controls-left);top:2px;display:flex;align-items:center;gap:4px;z-index:1}
#page-play #gsOctRow #gsFsBtn{position:absolute;right:var(--gs-controls-right);top:2px;margin:0;z-index:1}
#page-play #gsOctRow .gs-fullscreen-sep{position:absolute;right:calc(var(--gs-controls-right) + 40px);top:6px;bottom:6px;margin:0}
@media(max-width:600px){#page-play .gs-play-bar{--gs-controls-right:7px;padding:0 max(68px,calc(var(--gs-controls-left) + 58px))}}
/* Boutons fusionnés (2026-10-05) : anciens contrôles gardés dans le DOM comme source d'état, jamais affichés. */
#page-play .gs-oct-row .gs-bar-merged,#page-play .gs-oct-row #gsSlideSelect+.dpk-select-btn{display:none!important}
#page-play #gsViewBtn{display:inline-flex;align-items:center;gap:2px}
#page-play #gsKeyBtn,#page-play #gsTouchBtn{white-space:nowrap}
/* Menu à colonnes Key/Touch : chaque colonne défile seule, titre fixe en tête. */
.gs-picker-pop.gs-bar-combo{display:flex;flex-direction:column;overflow:hidden}
.gs-bar-combo .gs-picker-cols{flex:1;min-height:0}
.gs-bar-combo .gs-picker-col{min-height:0}
.gs-bar-combo .gs-combo-head{font-family:var(--fm);font-size:max(10px,.52rem);font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--tx2);padding:6px 8px 4px;border-bottom:1px solid var(--bd)}
.gs-bar-combo .gs-combo-list{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;position:relative}
/* Portrait téléphone : la barre passe sur 2 rangées au lieu d'un défilement horizontal caché. */
@media (max-width:600px) and (orientation:portrait){
  #page-play .gs-oct-row{flex-wrap:wrap;overflow-x:visible;justify-content:center;row-gap:3px;mask-image:none;-webkit-mask-image:none;scroll-snap-type:none}
}
/* Attentes calées sur la mesure : flash au tempo + temps restants (remplace la pulsation fixe de .5 s). */
#page-play .ft-armed,#gslFloat .ft-armed,#page-melodie .ft-armed,.ft-armed{animation:none!important;position:relative}
.ft-armed.ft-armed-flash{animation:ftArmedFlash .28s ease-out!important}
@keyframes ftArmedFlash{0%{box-shadow:inset 0 0 0 40px color-mix(in srgb,#e5484d 55%,transparent)}100%{box-shadow:inset 0 0 0 40px transparent}}
.ft-armed[data-ft-count]::after{content:attr(data-ft-count);position:absolute;top:1px;right:1px;min-width:13px;height:13px;padding:0 2px;box-sizing:border-box;border-radius:7px;
  background:#e5484d;color:#fff;font:700 max(10px,.5rem)/13px var(--fm);text-align:center;pointer-events:none}
/* Tablettes et petits portables (audit 2026-10-02) : à 1024px la barre complète dépassait de ~200px (ARP,
   accompagnement, enregistrement, plein écran cachés). Même solution que le téléphone en portrait : retour à
   la ligne quand elle ne tient pas — sans effet si elle tient sur une ligne. Exclut les écrans de moins de
   600px de HAUT (téléphones en paysage) où une 2e rangée mangerait la grille : ils gardent le défilement. */
@media (min-width:601px) and (min-height:600px){
  #page-play .gs-oct-row{flex-wrap:wrap;overflow-x:visible;justify-content:center;row-gap:3px;mask-image:none;-webkit-mask-image:none;scroll-snap-type:none}
}
@media (max-width:600px) and (orientation:portrait){
  #page-play .gs-oct-row{gap:3px;padding:2px 3px}
  #page-play #gsOctRow .gs-mode-btn{padding:0 6px;min-width:0}
}
/* Zone Mixeur = un BLOC distinct par module : cadre, fond et titre, marge avec la zone voisine. */
#page-play :is(.gsz-t-mixer,.gsz-t-strip)>.gsz-body.gsx-host{inset:4px;display:flex;flex-direction:column;gap:3px;padding:4px;box-sizing:border-box;
  background:var(--s1);border:1px solid var(--bd2);border-radius:10px}
#page-play .gsx-host>.gsx{position:relative;inset:auto;flex:1 1 auto;min-height:0}
#page-play .gsx-block-title{flex:none;font:700 max(10px,.55rem) var(--fm);letter-spacing:.08em;text-transform:uppercase;color:var(--c-root);padding:1px 4px 2px;border-bottom:1px solid var(--bd);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#page-play .gsz-t-strip .gsx-lbl{display:none}
#page-play .gsz-t-strip .gsx-block-title{letter-spacing:0;text-align:center;padding:1px 0 2px}
/* ── Zone Groovebox compacte (surface-mixer.js) ──────────────────────────────────────────────────────── */
#page-play .gsz-body.gbc{display:flex;flex-direction:column;gap:4px;padding:5px;box-sizing:border-box;background:var(--s1);border:1px solid var(--bd2);border-radius:10px;overflow:hidden;touch-action:none;user-select:none;-webkit-user-select:none}
#page-play .gbc-top{display:flex;gap:4px;align-items:stretch;flex:none}
#page-play .gbc-slots{display:flex;gap:3px;flex:1;min-width:0}
#page-play .gbc-btn{height:24px;min-width:0;padding:0 4px;border-radius:5px;border:1px solid var(--bd);background:var(--s2);color:var(--tx3);font:700 max(10px,.58rem) var(--fm);display:flex;align-items:center;justify-content:center;cursor:pointer;touch-action:none}
#page-play .gbc-btn svg{width:11px;height:11px}
#page-play .gbc-play{width:34px;flex:none;color:var(--tx)}
#page-play .gbc-play.on{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-play .gbc-slot{flex:1 1 0}
#page-play .gbc-slot.filled{background:var(--s3);color:var(--tx);border-color:var(--bd2)}
#page-play .gbc-slot.cur{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-play .gbc-slot.pending{border-color:#e5484d;color:var(--tx)}
#page-play .gbc-rows{flex:1;min-height:0;display:flex;flex-direction:column;gap:3px}
#page-play .gbc-row{flex:1 1 0;min-height:14px;display:flex;gap:3px;align-items:stretch}
#page-play .gbc-row.muted .gbc-steps{opacity:.35}
#page-play .gbc-name{flex:none;width:58px;padding:0 3px;border-radius:5px;border:1px solid var(--bd);background:var(--s3);color:var(--tx);font:700 max(10px,.5rem) var(--fm);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;touch-action:none}
#page-play .gbc-row .gbc-btn{height:auto;width:22px;flex:none}
#page-play .gbc-row .gbc-btn.gbc-w{width:auto;min-width:26px;padding:0 3px}
/* M et S plus larges (demande : éviter le mauvais clic en live) — 22px → 36px, texte plus gros. */
#page-play .gbc-row .gbc-btn.gbc-m,#page-play .gbc-row .gbc-btn.gbc-s{width:36px;font-size:max(12px,.72rem)}
#page-play .gbc-m.on{background:#e5484d;border-color:#e5484d;color:#fff}
#page-play .gbc-s.on{background:#f5c542;border-color:#f5c542;color:#111}
#page-play .gbc-steps{flex:1;min-width:0;display:grid;grid-template-columns:repeat(var(--gbc-n),1fr);gap:2px}
#page-play .gbc-c{border-radius:3px;background:var(--s3);border:1px solid var(--bd);min-width:0;cursor:pointer}
#page-play .gbc-c.beat{background:color-mix(in srgb,var(--s3) 80%,var(--tx) 8%)}
#page-play .gbc-c.on{background:var(--c-root);border-color:var(--c-root)}
#page-play .gbc-c.cur{box-shadow:inset 0 0 0 2px var(--tx)}

/* ── Zones tactiles élargies (audit 2026-10-02) ──────────────────────────────────────────────────────────────
   Sur écran tactile, la plupart des boutons de barre font 26-28px (recommandation : 44-48px). Contrainte
   explicite : ne JAMAIS agrandir visuellement les contrôles (place perdue). On élargit donc seulement la zone
   qui réagit au doigt, par un pseudo-élément transparent : +4px en haut/bas, +2px sur les côtés (28px → 36px)
   — assez peu pour ne pas recouvrir le corps d'un bouton voisin (les écarts entre boutons font ≥ 2-4px).
   Classes vérifiées : jamais positionnées en absolu/fixe (position:relative sans effet visuel) et sans
   ::after propre ; .info-btn/.vcard-play/.vcard-3d exclus (positionnés en absolu à certains endroits). */
@media (pointer:coarse){
  :is(.tbtn,.ibtn,.dpk-select-btn,.pill,.gs-mode-btn,.sq-btn,.sq-mini-btn,.rd-select,.str-play-inline,.rh-select,.hn-mpill,.accent-btn,.rbtn-underline){position:relative}
  :is(.tbtn,.ibtn,.dpk-select-btn,.pill,.gs-mode-btn,.sq-btn,.sq-mini-btn,.rd-select,.str-play-inline,.rh-select,.hn-mpill,.accent-btn,.rbtn-underline)::after{content:'';position:absolute;inset:-4px -2px}
  /* Les zones élargies d'un bouton au bord d'une barre à défilement HORIZONTAL débordaient de 2-4px en hauteur,
     rendant certaines barres défilables verticalement de quelques pixels (vérifié : sous-barre du Cercle). Ces
     barres ne doivent défiler qu'horizontalement. */
  [style*='overflow-x:auto']:not([style*='overflow-y']){overflow-y:hidden}
  /* Page Cercle (défilement vertical) : le dernier bouton touchait le bas, sa zone élargie ajoutait 1px de défilement. */
  .cof-page{padding-bottom:6px}
}
/* Analyseur (audit 2026-10-02) : barre d'outils en une ligne défilante (style inline) — à 1024-1180px,
   ~50 à 200px de commandes (instrument, vitesse, MIDI) restaient cachées hors champ, sans barre de défilement
   visible à la souris. Au-dessus du format téléphone, elle passe à la ligne quand elle ne tient pas. */
@media (min-width:601px) and (min-height:600px){
  #azHeader>.subbar{flex-wrap:wrap!important;overflow-x:visible!important;row-gap:4px}
}
/* Contrôleurs en zone (surface-mixer.js, 2026-10-02) : le widget remplit sa zone, même dessin que le contrôleur libre. */
#page-play .gsz-ctl-host>.gsz-ctl{position:absolute!important;inset:0;width:auto;height:auto}
/* Onglets de page SURFACE : appui long = menu de page (surface-zones.js) — pas de menu/sélection natifs au toucher. */
#page-play .gsz-tab[data-p]{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}

/* ══ Séquenceur : ergonomie 2026-10-02 ══════════════════════════════════════════════════════════════════
   Page à hauteur d'écran : barre, pistes, ÉDITEUR (prend toute la place restante, défile en interne) puis
   outils + clavier toujours visibles en bas — avant, sur un portable 1280×720 le clavier à l'écran était
   entièrement sous la ligne de flottaison, et sur téléphone la grille commençait à 688px. Si le contenu ne
   tient vraiment pas (panneau Générer ouvert sur très petit écran), la page redéfile comme avant. */
#page-melodie.page>.sq-body{display:flex;flex-direction:column;overflow-y:auto;max-width:none;margin:0}
#page-melodie .sq-guide,#page-melodie .sq-tracks,#page-melodie .sq-tools{flex:none}
#page-melodie .sq-tracks{max-height:32vh;overflow-y:auto;overscroll-behavior:contain}
#page-melodie #seqEditor{flex:1 1 0;min-height:200px;display:flex;flex-direction:column}
#page-melodie #seqEditor>.gsl-ed{flex:1 1 0;min-height:0}
#page-melodie .gsl-ed.gsl-ed-embed .gsl-ed-body{flex:1 1 0;max-height:none;min-height:110px}
/* Barre de sélection : n'occupe plus une ligne grisée en permanence — elle apparaît en surimpression, en bas
   de l'éditeur, seulement quand des notes sont sélectionnées. */
#page-melodie .gsl-ed.gsl-ed-embed .gsl-ed-selbar:not(.on){display:none}
#page-melodie .gsl-ed.gsl-ed-embed .gsl-ed-selbar.on{position:absolute;left:50%;transform:translateX(-50%);bottom:10px;z-index:5;flex-wrap:nowrap;max-width:calc(100% - 16px);
  background:color-mix(in srgb,var(--c-root) 14%,var(--sf));box-shadow:0 8px 24px rgba(0,0,0,.45)}
/* Aide des gestes : derrière le bouton « ? » de l'éditeur. */
#page-melodie .gsl-ed.gsl-ed-embed:not(.gsl-ed-help-on) .gsl-ed-hint{display:none}
#page-melodie .gsl-ed.gsl-ed-embed.gsl-ed-help-on .gsl-ed-hint{display:block} /* y compris sur téléphone (masquée d'office sous 560px auparavant) */
/* Panneau d'outils replié (retoucher l'onglet actif) : seuls les onglets restent. */
#page-melodie .sq-tools.collapsed .sq-tool-tabs{margin-bottom:0}
#page-melodie .sq-tools.collapsed .sq-tool.on{background:color-mix(in srgb,var(--c-root) 25%,var(--s2));color:var(--tx)}

@media (max-width:640px){
  /* Barre : 2 lignes au lieu de 3 — le statut reste sur la ligne du transport (tronqué). */
  #page-melodie .sq-status{flex:1 1 0;min-width:0;max-width:none}
  #page-melodie .sq-transport{flex-wrap:nowrap;flex:1 1 100%}
  #page-melodie .sq-opts{flex-wrap:nowrap;gap:4px;width:100%}
  #page-melodie .sq-btn{width:36px;height:34px}
  #page-melodie .sq-opts .sq-btn{width:34px}
  #page-melodie .sq-bpm small{display:none}
  #page-melodie .sq-bpm .sq-mini-btn{min-width:26px;height:28px}
  #page-melodie .sq-opts .dpk-select-btn{min-width:0;max-width:96px}
  /* Pistes : seule la piste choisie montre tous ses réglages ; les autres tiennent sur une ligne (nom,
     aperçu, M, S) pour qu'on voie plusieurs pistes et l'éditeur en même temps. */
  #page-melodie .sq-track:not(.sel){grid-template-columns:minmax(64px,auto) 1fr auto;padding:2px 6px}
  #page-melodie .sq-track:not(.sel) .sq-tctl{grid-column:auto;flex-wrap:nowrap}
  #page-melodie .sq-track:not(.sel) .sq-tctl>:not(.sq-m):not(.sq-s){display:none}
  #page-melodie .sq-track:not(.sel) .sq-mini{height:26px}
  #page-melodie .sq-track.sel .sq-tctl{flex-wrap:nowrap}
  #page-melodie .sq-track.sel .sq-tctl .sq-voice+.dpk-select-btn,#page-melodie .sq-track.sel .sq-tctl .sq-kit+.dpk-select-btn{width:96px;min-width:96px;max-width:96px}
  #page-melodie .sq-track.sel .sq-tctl .sq-len+.dpk-select-btn{width:72px;min-width:72px;max-width:72px}
  #page-melodie .sq-track.sel .sq-tctl .sq-more+.dpk-select-btn{width:42px;min-width:42px;max-width:42px}
  #page-melodie .sq-track.sel .sq-vol{width:40px;flex:1 1 40px;min-width:30px}
  #page-melodie #seqEditor{min-height:240px}
}
/* Mobile et tablette (< seuil desktop) : sans hauteur imposée, la page suit la hauteur de son contenu et
   l'éditeur ne peut pas « prendre la place restante ». Même patron que Strum et SURFACE : page fixée sous la
   topbar (--rh-top, mesurée par _rhSyncTopOffset au démarrage et à chaque redimensionnement). */
@media(max-width:1023px){
  #page-melodie.page.active{position:fixed;left:0;right:0;bottom:0;top:var(--rh-top,96px);z-index:5;overflow:hidden}
}
/* En-tête de l'éditeur sur UNE ligne au-dessus du format téléphone : le nom de piste se tronque plutôt que de
   renvoyer un bouton seul à la ligne (tablette 820px : « ? » isolé sur une 2e ligne). */
@media (min-width:641px){
  #page-melodie .gsl-ed.gsl-ed-embed .gsl-ed-head{flex-wrap:nowrap}
  #page-melodie .gsl-ed.gsl-ed-embed .gsl-ed-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  #page-melodie .gsl-ed.gsl-ed-embed .gsl-ed-info:empty{display:none}
  #page-melodie .gsl-ed.gsl-ed-embed .gsl-ed-head button{flex:none}
}
/* Scènes du Séquenceur (melodie.js, 2026-10-02) : rangée compacte A–H + chaîne. */
#page-melodie .sq-scenes{flex:none;display:flex;align-items:center;gap:4px;overflow-x:auto;scrollbar-width:none}
#page-melodie .sq-sc-lbl{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx3);text-transform:uppercase;margin-right:2px;flex:none}
#page-melodie .sq-sc{flex:none;min-width:34px;height:30px;padding:0 6px;border-radius:7px;border:1px dashed var(--bd2);background:transparent;color:var(--tx3);font-family:var(--fm);font-size:.66rem;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;gap:2px;-webkit-touch-callout:none;user-select:none}
#page-melodie .sq-sc small{font-size:max(10px,.5rem);opacity:.75}
#page-melodie .sq-sc.filled{border-style:solid;background:var(--s2);color:var(--tx)}
#page-melodie .sq-sc.cur{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-melodie .sq-sc.pending{border-color:var(--c-root);color:var(--c-root);animation:sqScPend .5s steps(2) infinite}
@keyframes sqScPend{50%{background:color-mix(in srgb,var(--c-root) 30%,var(--s2))}}
#page-melodie .sq-sc-chain{flex:none;height:30px;padding:0 10px;margin-left:4px;border-radius:7px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx2);font-family:var(--fm);font-size:.62rem;font-weight:700;cursor:pointer;white-space:nowrap}
#page-melodie .sq-sc-chain.on{background:var(--c-root);border-color:var(--c-root);color:#111}
/* Repère de collage de l'éditeur de notes (clip-editor.js) : trait pointillé là où « Coller » posera les notes. */
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-paste{stroke:var(--c-root);stroke-width:2;stroke-dasharray:5 4;vector-effect:non-scaling-stroke;pointer-events:none}
@media (max-width:640px){
  /* Scènes sur téléphone : libellé masqué, boutons resserrés — A à H et « Chaîne » tiennent sur une ligne. */
  #page-melodie .sq-sc-lbl{display:none}
  #page-melodie .sq-scenes{gap:3px}
  #page-melodie .sq-sc{min-width:30px;padding:0 4px}
  #page-melodie .sq-sc-chain{margin-left:2px;padding:0 8px}
}

/* ══ Séquenceur : ergonomie 2026-10-04 ══════════════════════════════════════════════════════════════════
   Barre d'outils de l'éditeur allégée (Sélection/Crayon, Gamme, Vél., Grille, zoom, annuler/rétablir, menu ⋯),
   libellés sous les icônes sur tablette, menu d'une note, clavier de gauche sélectionnable, pistes en onglets
   compacts, réglages de la barre réunis derrière un bouton, panneau d'outils en surimpression sur téléphone. */
.gsl-ed .gsl-lbl{display:none}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-modes{display:inline-flex;gap:0}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-modes button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-modes button:last-child{border-top-left-radius:0;border-bottom-left-radius:0;border-left:none}
/* Gomme / Scinder actifs (menu ⋯) : le bouton ⋯ le signale */
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed.gsl-ed-alt-tool .gsl-ed-more,
#page-melodie .gsl-ed.gsl-ed-alt-tool .gsl-ed-more+.dpk-select-btn{border-color:var(--c-root);color:var(--c-root);box-shadow:inset 0 0 0 1px var(--c-root)}
/* menu ⋯ natif (panneau flottant du looper de Surface) */
:is(#page-play,#gslFloat) .gsl-ed-more{height:32px;max-width:48px;border-radius:8px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font:700 .7rem var(--fm);padding:0 4px;cursor:pointer}
#page-melodie .gsl-ed-head .gsl-ed-more+.dpk-select-btn{flex:none;width:42px;min-width:42px;max-width:42px;height:32px;justify-content:center;padding:0}
/* ordinateur : nom à côté des icônes des deux outils et de la grille */
@media (pointer:fine){
  #page-melodie .gsl-ed-embed .gsl-ed-modes .gsl-lbl,#page-melodie .gsl-ed-embed [data-ea="q"] .gsl-lbl{display:inline;margin-left:5px}
}
/* tablette tactile : libellé sous chaque icône, cibles de 44 px */
@media (pointer:coarse) and (min-width:641px) and (min-height:501px){
  #page-melodie .gsl-ed-embed .gsl-ed-head button{height:44px;min-width:46px}
  #page-melodie .gsl-ed-embed .gsl-ed-head button:has(svg){flex-direction:column;gap:2px;padding:2px 6px}
  #page-melodie .gsl-ed-embed .gsl-ed-head button .gsl-lbl{display:block;font-size:9px;font-weight:600;line-height:1}
  #page-melodie .gsl-ed-head .gsl-ed-more+.dpk-select-btn{height:44px;width:46px;min-width:46px;max-width:46px}
}
/* clavier de gauche : toucher une hauteur = l'entendre et sélectionner ses notes */
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys .pk{cursor:pointer;touch-action:none;-webkit-user-select:none;user-select:none}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys .pk:hover{background:color-mix(in srgb,var(--c-root) 14%,transparent)}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-keys .pk.pk-on{background:color-mix(in srgb,var(--c-root) 38%,transparent);color:var(--tx)}
/* menu d'une note (appui long / clic droit) — au-dessus du panneau flottant du looper (10060) */
.gsl-note-menu{position:fixed;z-index:10070;left:0;top:0;min-width:210px;max-width:min(280px,calc(100vw - 16px));max-height:calc(100vh - 16px);overflow-y:auto;display:flex;flex-direction:column;gap:4px;padding:8px;border-radius:12px;background:var(--sf);border:1px solid var(--bd2);box-shadow:0 10px 30px rgba(0,0,0,.55);font-family:var(--fm);-webkit-user-select:none;user-select:none}
.gsl-note-menu button{height:40px;padding:0 12px;border-radius:8px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font:700 .64rem var(--fm);text-align:left;cursor:pointer;display:flex;align-items:center;gap:8px;flex:none}
.gsl-note-menu button:hover{border-color:var(--c-root)}
.gsl-note-menu button.danger{color:#e5484d}
.gsl-nm-head{font-size:.64rem;font-weight:700;color:var(--tx);padding:0 2px 2px}
.gsl-nm-sub{font-size:max(10px,.55rem);color:var(--tx3);text-transform:uppercase;margin-top:4px;padding:0 2px}
.gsl-nm-vel{display:flex;align-items:center;gap:6px;font-size:.62rem;color:var(--tx2)}
.gsl-nm-vel span{flex:1;padding-left:2px}
.gsl-note-menu .gsl-nm-vel button{width:40px;justify-content:center;padding:0;font-size:.8rem}
.gsl-nm-vel b{min-width:30px;text-align:center;color:var(--tx)}
.gsl-nm-dot{width:10px;height:10px;border-radius:50%;background:hsl(var(--gsl-h,12) 70% 55%);flex:none}

/* Pistes en onglets compacts (bouton de la barre, ou d'office sur écran bas) */
#page-melodie .sq-tracks.compact{flex-direction:row;flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;max-height:none!important;gap:4px;scrollbar-width:none;padding-bottom:1px}
#page-melodie .sq-tracks.compact .sq-track{display:flex;flex:none;padding:0;border-left-width:4px}
#page-melodie .sq-tracks.compact .sq-mini,#page-melodie .sq-tracks.compact .sq-tctl{display:none}
#page-melodie .sq-tracks.compact .sq-tname{height:36px;padding:0 12px;max-width:160px}
#page-melodie .sq-tracks.compact+.sq-split{display:none}
#page-melodie .sq-tview.on{background:var(--c-root);border-color:var(--c-root);color:#111}

/* Barre : les boutons ne se compriment plus (« + piste » à 19 px sur téléphone) */
#page-melodie .sq-bar .sq-btn{flex:none}
#page-melodie .sq-bar{position:relative}
#page-melodie .sq-optbtn{display:none}
#page-melodie .sq-optgrp{display:contents}
/* une seule ligne à toute largeur : le statut (lecture / piste) se tronque, les réglages ne passent plus à la ligne */
#page-melodie .sq-bar{flex-wrap:nowrap}
#page-melodie .sq-transport{flex:1 1 auto;min-width:0;flex-wrap:nowrap}
#page-melodie .sq-status{flex:1 1 0;min-width:0;max-width:none}
#page-melodie .sq-opts{flex:none;flex-wrap:nowrap}
@media (max-width:1199px){
  #page-melodie .sq-bar{flex-wrap:nowrap}
  #page-melodie .sq-transport{flex:1 1 auto;min-width:0;flex-wrap:nowrap}
  #page-melodie .sq-status{flex:1 1 0;min-width:0;max-width:none}
  #page-melodie .sq-opts{flex:none;margin-left:0;flex-wrap:nowrap;width:auto;gap:4px}
  #page-melodie .sq-opts .sq-optbtn{display:inline-flex;width:auto;padding:0 6px 0 8px;gap:2px;font-family:var(--fm);font-size:.66rem}
  #page-melodie .sq-optbtn svg{width:12px;height:12px}
  #page-melodie .sq-bar.opts-open .sq-optbtn{background:var(--c-root);border-color:var(--c-root);color:#111}
  #page-melodie .sq-optgrp{display:none;position:absolute;right:8px;top:calc(100% + 4px);z-index:40;flex-wrap:wrap;align-items:center;gap:8px;width:250px;padding:10px;border-radius:12px;background:var(--sf);border:1px solid var(--bd2);box-shadow:0 10px 30px rgba(0,0,0,.5)}
  #page-melodie .sq-bar.opts-open .sq-optgrp{display:flex}
  #page-melodie .sq-optgrp .sq-bpm{flex:1 0 100%;justify-content:space-between;font-size:.7rem}
  #page-melodie .sq-optgrp .sq-bpm small{display:inline}
  #page-melodie .sq-optgrp .dpk-select-btn{max-width:none}
  #page-melodie .sq-optgrp #seqSrcSelect+.dpk-select-btn{flex:1 0 100%}
  #page-melodie .sq-optgrp #seqGridSelect+.dpk-select-btn{flex:1 1 auto}
  #page-melodie .sq-optgrp #seqBtnClear{margin-left:auto}
}

/* Cibles tactiles : 36 px minimum, 44 px pour le transport */
@media (pointer:coarse){
  #page-melodie .sq-mini-btn{min-width:36px;height:36px}
  #page-melodie .sq-tctl>button{min-width:36px;height:36px}
  #page-melodie .sq-opts .sq-btn{width:32px;height:34px}
  #page-melodie .sq-opts .sq-optbtn{width:auto}
  #page-melodie .sq-transport .sq-btn{width:36px;height:36px}
  #page-melodie .sq-tool{height:40px}
  #page-melodie .sq-sc{height:34px;min-width:34px}
}
@media (pointer:coarse) and (max-width:640px){
  /* téléphone : annuler/rétablir sont dans la barre de l'éditeur — le transport tient sur une ligne */
  #page-melodie .sq-transport #seqBtnUndo,#page-melodie .sq-transport #seqBtnRedo{display:none}
  #page-melodie .sq-transport .sq-btn{width:42px;height:42px}
  #page-melodie .sq-opts .sq-btn{width:36px}
  #page-melodie .sq-sc{min-width:32px}
}

/* Téléphone : barre de l'éditeur sur une ligne (zoom au pincement), panneau d'outils par-dessus la grille */
/* Tablette tactile (et téléphone, ci-dessous) : le panneau d'outils s'ouvre par-dessus la grille */
@media (pointer:coarse) and (max-width:1199px){
  #page-melodie .sq-tools{position:relative;padding:4px}
  #page-melodie .sq-tool-tabs{margin-bottom:0}
  #page-melodie .sq-tools:not(.collapsed) .sq-panel{position:absolute;left:0;right:0;bottom:calc(100% + 6px);max-height:min(60vh,440px);overflow-y:auto;overscroll-behavior:contain;background:var(--sf);border:1px solid var(--bd2);border-radius:12px;padding:8px;box-shadow:0 -10px 30px rgba(0,0,0,.5);z-index:30;margin:0}
}
@media (max-width:640px){
  #page-melodie .gsl-ed.gsl-ed-embed .gsl-ed-head{gap:4px}
  #page-melodie .gsl-ed.gsl-ed-embed .gsl-ed-head [data-ea="zin"],#page-melodie .gsl-ed.gsl-ed-embed .gsl-ed-head [data-ea="zout"],#page-melodie .gsl-ed.gsl-ed-embed .gsl-ed-head [data-ea="help"]{display:none} /* aide : dans le menu ⋯ */
  #page-melodie .gsl-ed.gsl-ed-embed .gsl-ed-head button{min-width:36px;height:40px}
  #page-melodie .gsl-ed-head .gsl-ed-more+.dpk-select-btn{height:40px;width:40px;min-width:40px;max-width:40px}
  #page-melodie .sq-tools{position:relative;padding:4px}
  #page-melodie .sq-tool-tabs{margin-bottom:0}
  #page-melodie .sq-tools:not(.collapsed) .sq-panel{position:absolute;left:0;right:0;bottom:calc(100% + 6px);max-height:40vh;overflow-y:auto;overscroll-behavior:contain;background:var(--sf);border:1px solid var(--bd2);border-radius:12px;padding:8px;box-shadow:0 -10px 30px rgba(0,0,0,.5);z-index:30;margin:0}
  #page-melodie .sq-tool{font-size:.6rem;padding:0 2px}
}

/* Message (toast) en haut sur ce module : en bas il masquait le panneau d'outils */
body:has(#page-melodie.active) .hint{bottom:auto;top:calc(var(--rh-top,92px) + 62px)}
/* Téléphone couché : scènes et onglets de pistes sur une même ligne, barres resserrées — la grille garde la
   hauteur restante (sous la barre de l'app, il ne reste qu'environ 230 px). */
@media (max-height:500px) and (orientation:landscape){
  #page-melodie.page>.sq-body{display:grid;grid-template-columns:auto minmax(0,1fr);grid-template-rows:auto auto minmax(0,1fr) auto;column-gap:8px;row-gap:5px;padding:5px 8px}
  #page-melodie .sq-body>*+*{margin-top:0}
  #page-melodie .sq-scenes{grid-column:1;grid-row:1}
  #page-melodie .sq-tracks{grid-column:2;grid-row:1;min-width:0}
  #page-melodie .sq-scenes:not(:has(.sq-sc))+.sq-guide+.sq-tracks,#page-melodie .sq-scenes[style*="none"]~.sq-tracks{grid-column:1 / -1}
  #page-melodie .sq-guide{grid-column:1 / -1;grid-row:2}
  #page-melodie .sq-split{display:none}
  #page-melodie #seqEditor{grid-column:1 / -1;grid-row:3;min-height:0}
  #page-melodie .sq-tools{grid-column:1 / -1;grid-row:4;padding:3px}
  #page-melodie .sq-sc-lbl{display:none}
  #page-melodie .sq-sc{height:32px;min-width:30px;padding:0 4px}
  #page-melodie .sq-sc-chain{height:32px;padding:0 8px}
  #page-melodie .sq-tracks.compact .sq-tname{height:32px}
  #page-melodie .sq-bar{padding:4px 8px}
  #page-melodie .sq-transport .sq-btn,#page-melodie .sq-opts .sq-btn{height:36px}
  #page-melodie .sq-tool{height:32px}
  #page-melodie .gsl-ed.gsl-ed-embed{padding:5px;gap:4px}
  #page-melodie .gsl-ed.gsl-ed-embed .gsl-ed-head button,#page-melodie .gsl-ed-head .gsl-ed-more+.dpk-select-btn{height:34px}
  #page-melodie .gsl-ed.gsl-ed-embed .gsl-ed-body{min-height:60px}
  #page-melodie .sq-tools:not(.collapsed) .sq-panel{position:absolute;left:0;right:0;bottom:calc(100% + 6px);max-height:70vh;overflow-y:auto;background:var(--sf);border:1px solid var(--bd2);border-radius:12px;padding:8px;box-shadow:0 -10px 30px rgba(0,0,0,.5);z-index:30;margin:0}
  #page-melodie .sq-tools{position:relative}
  #page-melodie .sq-tool-tabs{margin-bottom:0}
}

/* Séquenceur : panneau « Son » (réglages du synthé de la piste) */
#page-melodie .sq-snd-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:4px 12px;margin-top:6px}
#page-melodie .sq-snd-p{display:grid;grid-template-columns:1fr auto;grid-template-areas:"l v" "s s";gap:1px 6px;font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3)}
#page-melodie .sq-snd-p span{grid-area:l;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#page-melodie .sq-snd-p b{grid-area:v;color:var(--c-root)}
#page-melodie .sq-snd-p input{grid-area:s;width:100%;accent-color:var(--c-root)}
#page-melodie .sq-snd-pre{display:inline-flex;gap:2px}
/* STEP : défilement vertical du piano-roll — barre toujours visible (sinon invisible avec les barres « overlay ») */
#tb303Roll{scrollbar-width:thin;scrollbar-color:var(--c-root) var(--s3);padding-right:2px}
#tb303Roll::-webkit-scrollbar{width:10px}
#tb303Roll::-webkit-scrollbar-track{background:var(--s3);border-radius:5px}
#tb303Roll::-webkit-scrollbar-thumb{background:var(--c-root);border-radius:5px}
/* STEP : boutons de la barre d'outils (même gabarit que les boutons du Séquenceur) */
#page-step .st-btn{height:28px;padding:0 11px;border-radius:4px;border:1px solid var(--bd);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:max(10px,.6rem);font-weight:700;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:4px;flex-shrink:0;touch-action:manipulation;transition:background var(--tr),border-color var(--tr)}
#page-step .st-btn:hover{background:var(--s3);border-color:var(--bd2)}
#page-step .st-btn:active{transform:scale(.96)}
#page-step .st-btn.on{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-step .st-btn.st-sq{width:28px;padding:0}

/* Séquenceur : poignée de répartition pistes / éditeur */
#page-melodie .sq-split{flex:none;height:18px;margin:-2px 0;display:flex;align-items:center;justify-content:center;cursor:row-resize;touch-action:none;user-select:none}
#page-melodie .sq-split span{display:block;width:56px;height:5px;border-radius:3px;background:var(--bd2);transition:background var(--tr),width var(--tr)}
#page-melodie .sq-split:hover span,#page-melodie .sq-split.on span{background:var(--c-root);width:84px}
/* Séquenceur : réordonner une piste (appui long sur son étiquette) */
#page-melodie .sq-track.sq-moving{opacity:.92;box-shadow:0 8px 22px rgba(0,0,0,.55);outline:2px solid var(--c-root);position:relative}
#page-melodie .sq-tracks.sq-reordering{overflow-y:hidden;user-select:none}
#page-melodie .sq-track.sq-drop-before{box-shadow:0 -3px 0 0 var(--c-root)}
#page-melodie .sq-track.sq-drop-after{box-shadow:0 3px 0 0 var(--c-root)}
/* éditeur de notes : bouton de mode « − Note » */
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-modes{display:inline-flex;gap:2px;flex:none}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-modes button{min-width:26px;padding:0 6px}
:is(#page-play,#gslFloat,#page-melodie) .gsl-ed-modes button[data-ea=mDel].on{background:#e5484d;border-color:#e5484d;color:#fff}

/* ═══ NIVEAUX D'INTERFACE (Simple / Moyen / Avancé) — cf. dev/NIVEAUX-PLAN.md, applyUiLevel (app.js) ═══
   Masquage 100 % CSS piloté par le niveau EFFECTIF du module (.page[data-uilvl], global ou déplié via la ligne
   « + N masqués »). !important : beaucoup de blocs portent un display inline posé par leur module (style= ou
   JS), qui l'emporterait sinon. .uilvl-force = élément hors niveau affiché quand même (utilisé/sélectionné). */
/* :not(#uilvl_x):not(#uilvl_y):not(#uilvl_z) : chaque :not(#id) ajoute la spécificité d'un ID (sans rien filtrer) — nécessaire pour l'emporter sur
   les règles existantes du type "#page-structures #strSectionEditor{display:flex!important}" (2 ID). */
:is(.page,.uilvl-scope)[data-uilvl="1"] [data-lvl="2"]:not(.uilvl-force):not(#uilvl_x):not(#uilvl_y):not(#uilvl_z),
:is(.page,.uilvl-scope)[data-uilvl="1"] [data-lvl="3"]:not(.uilvl-force):not(#uilvl_x):not(#uilvl_y):not(#uilvl_z),
:is(.page,.uilvl-scope)[data-uilvl="2"] [data-lvl="3"]:not(.uilvl-force):not(#uilvl_x):not(#uilvl_y):not(#uilvl_z){display:none!important}
/* Drone desktop (flex-wrap en ligne, cf. "#page-drone > div:first-child") : sans align-content, les lignes
   s'étirent sur toute la hauteur quand des cartes sont masquées — grand vide entre la 1re carte et les colonnes. */
#page-drone .drone-wrap{align-content:flex-start}
.uilvl-seg{display:flex;align-items:center;height:28px;border:1px solid var(--bd);border-radius:7px;background:var(--pbg);overflow:hidden;flex-shrink:0}
.uilvl-btn{height:100%;min-width:22px;padding:0 6px;border:none;background:transparent;color:var(--tx3);font-family:var(--fm);font-size:.62rem;font-weight:700;cursor:pointer;transition:all var(--tr);line-height:1}
.uilvl-btn+.uilvl-btn{border-left:1px solid var(--bd)}
.uilvl-btn:hover{color:var(--tx)}
.uilvl-btn.active{background:var(--s3);color:var(--tx)}
/* Copie topbar réservée au desktop : en mobile la topbar est déjà pleine, la copie du tiroir prend le relais. */
.uilvl-reveal{width:100%;box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;margin:10px auto 6px;padding:2px 8px;font-family:var(--fm);font-size:.6rem;color:var(--tx3)}
.uilvl-reveal-btn{background:transparent;border:1px dashed var(--bd);border-radius:999px;color:var(--tx2);font-family:var(--fm);font-size:.6rem;padding:4px 12px;cursor:pointer;transition:all var(--tr)}
.uilvl-reveal-btn:hover{border-color:var(--c-root);color:var(--tx)}
.uilvl-reveal-names{color:var(--tx3)}
.uilvl-out{opacity:.75;font-style:italic}
.ob-presets-3{grid-template-columns:repeat(auto-fill,minmax(160px,1fr))}
.uilvl-more{border-style:dashed!important;color:var(--tx3)!important;cursor:pointer}
.uilvl-more:hover{color:var(--tx)!important;border-color:var(--c-root)!important}
.uilvl-seg-wide{margin:2px 10px 8px;height:30px}
.uilvl-seg-wide .uilvl-btn{flex:1}
/* Ligne posée en enfant direct de .page : la règle générique ".page > *:not(div:first-child)" (flex:1,
   overflow:hidden, colonne) lui ferait partager la hauteur avec le corps du module — on la ramène à sa
   hauteur naturelle. */
.page > .uilvl-reveal{flex:0 0 auto!important;flex-direction:row!important;overflow:visible!important;min-height:auto!important;margin:8px auto 8px!important}
/* Version compacte dans une barre d'outils (SURFACE) : pas de pleine largeur ni de marges de ligne. */
.uilvl-compact{display:contents}
.uilvl-compact .uilvl-reveal{width:auto;margin:0;padding:0;flex:0 0 auto}
.uilvl-compact .uilvl-reveal-btn{height:28px;padding:0 8px;white-space:nowrap}

/* ═══ Paramètres Surface réorganisés (surface-settings-ui.js, 2026-10-06) ═══════════════════════════════════════ */
#gsSettingsPanel .gsx-root{width:100%;max-width:860px;margin:0 auto;flex:1 1 auto}
#gsSettingsPanel .gsx-top{position:sticky;top:-12px;z-index:6;display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:-12px -4px 4px;padding:10px 4px;background:var(--sf);border-bottom:1px solid var(--bd)}
#gsSettingsPanel .gsx-pills{display:flex;gap:6px;flex-wrap:wrap}
#gsSettingsPanel .gsx-pill,#gsSettingsPanel .gsx-all{height:30px;padding:0 12px;border-radius:15px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.6rem;font-weight:700;cursor:pointer;white-space:nowrap}
#gsSettingsPanel .gsx-all{background:transparent;color:var(--tx2)}
#gsSettingsPanel .gsx-pill:hover,#gsSettingsPanel .gsx-all:hover{border-color:var(--c-root)}
#gsSettingsPanel .gsx-sec{border:1px solid var(--bd);border-radius:12px;margin:10px 0;background:var(--s1);overflow:hidden}
#gsSettingsPanel .gsx-sec.gsx-open{border-color:var(--bd2)}
#gsSettingsPanel .gsx-head{display:flex;align-items:center;gap:12px;width:100%;min-height:50px;padding:0 14px;background:transparent;border:0;color:var(--tx);cursor:pointer;text-align:left}
#gsSettingsPanel .gsx-title{flex:none;font-family:var(--fm);font-size:.76rem;font-weight:700;color:var(--c-root);text-transform:uppercase;letter-spacing:.07em}
#gsSettingsPanel .gsx-sum{margin-left:auto;min-width:0;max-width:62%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--fm);font-size:.62rem;color:var(--tx2)}
#gsSettingsPanel .gsx-open>.gsx-head .gsx-sum{opacity:.55}
#gsSettingsPanel .gsx-chev{flex:none;width:8px;height:8px;border-right:2px solid var(--tx3);border-bottom:2px solid var(--tx3);transform:rotate(-45deg);transition:transform .15s}
#gsSettingsPanel .gsx-open>.gsx-head .gsx-chev{transform:rotate(45deg)}
#gsSettingsPanel .gsx-body{display:none;padding:0 14px 14px}
#gsSettingsPanel .gsx-open>.gsx-body{display:block}
#settingsModal.settings-searching #gsSettingsPanel .gsx-sec .gsx-body{display:block}
#gsSettingsPanel .gsx-help{margin:0 0 8px;font-family:var(--fm);font-size:max(10px,.56rem);line-height:1.45;color:var(--tx3)}
#gsSettingsPanel .gsx-rows{display:flex;flex-direction:column}
#gsSettingsPanel .gsx-sub{margin-top:12px;padding-top:6px;border-top:1px dashed var(--bd2)}
#gsSettingsPanel .gsx-subhead{display:flex;align-items:center;gap:6px;padding:8px 0;cursor:pointer;list-style:none;font-family:var(--fm);font-size:.66rem;font-weight:700;color:var(--tx);text-transform:uppercase;letter-spacing:.05em}
#gsSettingsPanel .gsx-subhead::-webkit-details-marker{display:none}
#gsSettingsPanel .gsx-subhead::before{content:'▸';color:var(--tx3);font-size:.8em}
#gsSettingsPanel details[open]>.gsx-subhead::before{content:'▾'}
#gsSettingsPanel .gsx-hide,#gsSettingsPanel .gsx-sec.gsx-empty,#gsSettingsPanel .gsx-sec.gsx-nomatch{display:none!important}
#gsSettingsPanel .gsx-none{padding:24px 8px;text-align:center;font-family:var(--fm);font-size:.7rem;color:var(--tx3)}
@media(max-width:699px){#gsSettingsPanel .gsx-sum{max-width:48%}}

/* Lignes de réglage : libellé à gauche, contrôle à droite, séparées par un filet (au lieu du zébrage) */
#settingsModal #gsSettingsPanel .gsx-rows>.gs-settings-group{background:none!important;border-radius:0;margin:0;padding:6px 2px;min-height:46px;box-sizing:border-box;flex-wrap:nowrap;gap:12px;border-bottom:1px solid var(--bd)}
#settingsModal #gsSettingsPanel .gsx-rows>.gs-settings-group:last-child{border-bottom:0}
#settingsModal #gsSettingsPanel .gsx-rows>.gs-settings-group>.gs-midi-lbl{flex:0 0 34%;max-width:240px;white-space:normal;line-height:1.3;font-size:max(10px,.56rem)}
#settingsModal #gsSettingsPanel .gsx-rows>.gs-settings-group>:is(select,.dpk-select-btn,input[type=range],.gs-volume-slider){flex:1 1 0;min-width:0;width:auto}
#settingsModal #gsSettingsPanel .gsx-rows>.gs-settings-group>.gs-vol-val-wrap{flex:none}
#settingsModal #gsSettingsPanel .gsx-rows>.gs-settings-group:has(>.gs-midi-lbl+:is(button,.tbtn,.gs-mini-btn):last-child)>.gs-midi-lbl{flex:1 1 auto;max-width:none}
#settingsModal #gsSettingsPanel .gsx-rows>.gs-settings-group:has(.gs-midi-bar,#gsMidiPanel,#gsFuncLayoutPresetList){flex-wrap:wrap}
#settingsModal #gsSettingsPanel .gsx-sub .gsx-rows>.gs-settings-group{padding-left:2px}
@media(max-width:699px){#settingsModal #gsSettingsPanel .gsx-rows>.gs-settings-group>.gs-midi-lbl{flex-basis:38%}}

/* ═══ Paramètres : composants communs (2026-10-06, passe d'harmonisation) ═══════════════════════════════════════ */
.settings-btn{display:inline-flex;align-items:center;justify-content:center;min-height:30px;padding:5px 12px;border:1px solid var(--bd2);border-radius:6px;background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.62rem;line-height:1.2;cursor:pointer;white-space:nowrap;transition:border-color var(--tr),color var(--tr),background var(--tr);-webkit-tap-highlight-color:transparent}
.settings-btn:hover:not(:disabled){border-color:var(--c-root);color:var(--c-root)}
.settings-btn.accent{background:var(--c-root);border-color:var(--c-root);color:#111;font-weight:700}
.settings-btn.accent:hover:not(:disabled){color:#111;filter:brightness(1.1)}
.settings-btn:disabled{opacity:.45;cursor:default}
.settings-input{min-height:30px;box-sizing:border-box;padding:5px 10px;border:1px solid var(--bd);border-radius:6px;background:var(--bg2);color:var(--tx);font-family:var(--fm);font-size:.62rem}
.settings-input:focus{outline:none;border-color:var(--c-root)}
/* lignes de raccourcis : le .kbd-row écrasait le padding des .settings-row (texte collé au bord, rangées plus larges que les titres) */
.kbd-row{padding:4px 10px}
/* boutons d'un même choix (1× 2× 4× 6×…) : toujours sur une ligne, c'est le texte à gauche qui se resserre */
.settings-row .settings-seg{flex-wrap:nowrap;flex:none}
.settings-row>div:first-child{min-width:0}
/* gestionnaire hors ligne */
.off-side{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end}
.off-row .off-side{flex-wrap:nowrap}
.off-chip{font-family:var(--fm);font-size:max(10px,.54rem);padding:3px 8px;border-radius:10px;border:1px solid var(--bd2);white-space:nowrap}
.off-chip.off-ok{color:#7ad98a;border-color:#3f7f4b}
.off-chip.off-part{color:#e8c46a;border-color:#8a6f2c}
.off-chip.off-no{color:var(--tx3)}
@media(max-width:560px){.off-row{flex-wrap:wrap}.off-row .off-side{width:100%;justify-content:space-between}}
#settingsModal #gsSettingsPanel .gsx-rows>.gs-settings-group>.gs-vol-val-wrap{flex:0 0 78px;width:78px}

.off-info{display:flex;gap:10px;align-items:flex-start;padding:8px 2px;border-bottom:1px solid var(--bd)}
.off-ic{flex:none;min-width:34px;text-align:center}
.off-testhint{margin:8px 0 2px;padding:8px 10px;border-radius:8px;background:var(--s2);font-family:var(--fm);font-size:max(10px,.56rem);line-height:1.45;color:var(--tx2)}
.off-more{min-height:26px;padding:3px 8px;font-size:.56rem}
.off-files{margin:0 0 6px;padding:4px 10px;border-left:2px solid var(--bd2);background:var(--s1);border-radius:0 6px 6px 0}
.off-file{display:flex;gap:10px;align-items:center;padding:3px 0;font-family:var(--fm);font-size:max(10px,.54rem);color:var(--tx2)}
.off-fn{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.off-fs{flex:none;color:var(--tx3)}
@media(max-width:560px){.off-row .off-side{flex-wrap:wrap}}

/* ═══ Panneaux Effets / Son compactés (2026-10-08) — Surface (tiroir du bandeau + zone Synthé) et Séquenceur ═══
   Même potentiomètre (tb303KnobEl) partout : plus petit, libellé sur une ligne, cartes d'effets qui passent à la
   ligne au lieu de défiler, titres et notes réduits. Les cibles tactiles restent ≥ 30 px. */
#page-play .gss-panel{gap:3px}
#page-play .gss-zone-host{padding:4px;gap:3px}
#page-play .gss-drawer{padding:5px 8px}
#page-play .gss-sub{margin-top:1px;font-size:max(10px,.5rem);letter-spacing:.05em}
#page-play .gss-sub-row{gap:6px}
#page-play .gss-head{gap:4px}
#page-play .gss-voice,#page-play .gss-osc{height:24px;padding:0 8px;font-size:.58rem}
#page-play .gss-activeonly{height:20px;padding:0 7px}
#page-play .gss-fxrow{flex-wrap:wrap;overflow:visible;gap:4px;padding-bottom:0}
#page-play .gss-fxrow>.gsc-arrow{display:none}
#page-play .gss-fx{min-width:0;padding:3px 4px;gap:1px;border-radius:8px;flex:1 1 auto}
#page-play .gss-fx-top{gap:3px;width:100%;justify-content:space-between}
#page-play .gss-fx .gsc-chip{height:20px;padding:0 7px;font-size:max(10px,.52rem)}
#page-play .gss-eye{width:20px;height:20px}
#page-play .gss-fx-knobs{gap:2px}
#page-play .gss-fx select{height:22px;font-size:.56rem;max-width:100%}
#page-play .gss-grid{gap:2px 4px}
#page-play .gss-panel .tb303-knob-wrap,#page-play .gss-grid .tb303-knob-wrap,#page-play .gss-fx .tb303-knob-wrap{width:54px;min-width:54px;gap:0}
#page-play .gss-panel .tb303-knob-label,#page-play .gss-grid .tb303-knob-label{min-height:0;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:54px;font-size:max(10px,.46rem);line-height:1.1}
#page-play .gss-panel .tb303-knob-svg{width:30px;height:30px}
#page-play .gss-panel .tb303-knob-val,#page-melodie .sq-fx-knobs .tb303-knob-val{font-size:max(10px,.5rem);line-height:1;white-space:nowrap}
@media (pointer:coarse){#page-play .gss-panel .tb303-knob-svg,#page-play .gss-grid .tb303-knob-svg{width:34px;height:34px}}
/* Séquenceur : Effets (potentiomètres) et Son (curseurs) */
#page-melodie #seqFxBox{display:flex;flex-wrap:wrap;align-items:center;gap:2px 6px}
#page-melodie #seqFxBox>.sq-note{flex:1 0 100%;margin:0;font-size:max(10px,.5rem)}
#page-melodie #seqFxBox>.sq-fx-knobs{display:contents}
#page-melodie #seqFxBox>.sq-lbl{display:none}
#page-melodie #seqFxBox [data-g="shared"] select,#page-melodie #seqFxBox [data-g="shared"]+*{margin-left:6px}
#page-melodie #seqFxBox .dpk-select-btn{height:26px;max-width:110px;font-size:.58rem}
#page-melodie .sq-fx-knobs .tb303-knob-wrap{width:54px;gap:0}
#page-melodie .sq-fx-knobs .tb303-knob-label{min-height:0;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:54px;line-height:1.1}
#page-melodie .sq-fx-knobs .tb303-knob-svg{width:30px;height:30px}
@media (pointer:coarse){#page-melodie .sq-fx-knobs .tb303-knob-svg{width:34px;height:34px}}
#page-melodie .sq-snd-grid{grid-template-columns:repeat(auto-fill,minmax(112px,1fr));gap:1px 8px;margin-top:2px}
#page-melodie .sq-snd-p{gap:0 4px;font-size:max(10px,.46rem)}
#page-melodie .sq-snd-p input{height:18px;margin:0}
#page-melodie #seqSndBox .sq-row{gap:4px}
#page-melodie #seqSndBox .sq-tbtn{height:24px;padding:0 7px;font-size:.58rem}
#page-melodie #seqSndBox>.sq-note{margin:0}

/* Paramètres > Général : canaux affichés dans le mixeur global (puces à bascule) */
.mixch-list{display:flex;flex-wrap:wrap;gap:5px}
.mixch-chip{height:28px;padding:0 11px;border-radius:14px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx3);font-family:var(--fm);font-size:.62rem;font-weight:700;cursor:pointer;text-decoration:line-through;opacity:.6}
.mixch-chip.on{background:var(--c-root);border-color:var(--c-root);color:#111;text-decoration:none;opacity:1}

/* Zone Synthé / tiroir : section « Son » en curseurs compacts, comme « Son » du Séquenceur (2026-10-09) */
#page-play .gss-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(112px,1fr));gap:1px 8px}
#page-play .gss-snd-p{display:grid;grid-template-columns:1fr auto;grid-template-areas:"l v" "s s";gap:0 4px;font-family:var(--fm);font-size:max(10px,.46rem);color:var(--tx3);min-width:0}
#page-play .gss-snd-p span{grid-area:l;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#page-play .gss-snd-p b{grid-area:v;color:var(--c-root);white-space:nowrap}
#page-play .gss-snd-p input{grid-area:s;width:100%;height:18px;margin:0;accent-color:var(--c-root)}
#page-play .gss-grid>.gsz-note{grid-column:1/-1}

/* Effets : curseurs compacts, une carte = puce + œil puis ses curseurs côte à côte (2026-10-09) */
#page-play .gss-fxrow{align-items:flex-start;gap:4px}
#page-play .gss-fx{flex:0 1 auto;min-width:0;padding:3px 5px;gap:2px;align-items:stretch}
#page-play .gss-fx-top{justify-content:space-between}
#page-play .gss-fx-knobs{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(64px,1fr);gap:0 8px}
#page-play .gss-fx-knobs .gss-snd-p:only-child span{display:none}
#page-play .gss-fx-knobs .gss-snd-p:only-child{grid-template-columns:1fr;grid-template-areas:"v" "s";justify-items:end;min-width:84px}
#page-play .gss-fx select,#page-play .gss-fx .dpk-select-btn{height:20px;font-size:.54rem}

/* ═══ Effets en liste compacte (2026-10-14) — une ligne par effet : interrupteur · curseur · valeur · réglages secondaires ═══
   Deux colonnes dès 760 px de large ; tiroir OPAQUE et sans flou d'arrière-plan (backdrop-filter recalculé à chaque image
   tant que la surface dessous s'anime : coût GPU continu, et texte peu lisible par-dessus la grille). */
#page-play .gss-drawer{background:var(--sf);-webkit-backdrop-filter:none;backdrop-filter:none}
#page-play .gss-fxlist{display:grid;grid-template-columns:1fr;gap:3px 14px}
@media (min-width:760px){#page-play .gss-fxlist{grid-template-columns:1fr 1fr}}
#page-play .gss-fxr{display:flex;align-items:center;gap:6px;min-width:0;min-height:28px;flex-wrap:wrap}
#page-play .gss-fxr>.gsc-chip{width:76px;justify-content:center;height:24px;padding:0 6px;font-size:max(10px,.54rem)}
#page-play .gss-fxr-main{flex:1 1 90px;min-width:70px;height:22px;margin:0;accent-color:var(--c-root);opacity:.55}
#page-play .gss-fxr.on .gss-fxr-main{opacity:1}
#page-play .gss-fxr-val{flex:none;min-width:58px;font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3);white-space:nowrap}
#page-play .gss-fxr.on .gss-fxr-val{color:var(--c-root)}
#page-play .gss-fxx{display:flex;align-items:center;gap:4px 8px;flex:0 1 auto;min-width:0;flex-wrap:wrap}
#page-play .gss-mini{display:flex;align-items:center;gap:3px;font-family:var(--fm);font-size:max(10px,.46rem);color:var(--tx3);white-space:nowrap}
#page-play .gss-mini input{width:56px;height:18px;margin:0;accent-color:var(--c-root)}
#page-play .gss-mini b{color:var(--tx2);font-weight:700;min-width:34px}
#page-play .gss-fxsel,#page-play .gss-fxx .dpk-select-btn{height:22px;font-size:max(10px,.52rem);max-width:118px}
#page-play .gss-chaincfg{display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:4px 0;font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx2)}
#page-play .gss-chaincfg .gsc-chip{height:22px;padding:0 8px;font-size:max(10px,.52rem)}
#page-play .gss-chaincfg .gsc-chip.on,#page-play .gss-chainbtn.on{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-play .gss-chainbtn{height:22px;padding:0 8px;font-size:max(10px,.52rem)}
#page-play .gss-note{color:var(--tx3);font-size:max(10px,.46rem)}
#page-play .gsc-more{color:var(--tx2);border-style:dashed}
/* téléphone : les réglages secondaires passent sous le curseur, alignés après l'interrupteur */
/* téléphone : valeur plus étroite ; les réglages secondaires restent sur la ligne s'il y a la place, sinon passent dessous */
@media (max-width:600px){#page-play .gss-fxr{gap:4px}#page-play .gss-fxr-val{min-width:40px}#page-play .gss-fxr>.gsc-chip{width:66px}#page-play .gss-mini input{width:48px}#page-play .gss-mini b{min-width:28px}}
@media (pointer:coarse){#page-play .gss-fxr{min-height:32px}#page-play .gss-fxr>.gsc-chip{height:28px}}

/* ═══ Paramètres > Surface compactés (2026-10-09) : aide en infobulle, lignes plus basses, deux colonnes si la place le permet ═══ */
#gsSettingsPanel .gsx-help{display:none}
#gsSettingsPanel .gsx-body{container-type:inline-size;padding:0 12px 8px}
#gsSettingsPanel .gsx-head{min-height:40px}
#gsSettingsPanel .gsx-sec{margin:6px 0}
#gsSettingsPanel .gsx-top{gap:6px;padding:12px 4px 6px;margin-bottom:2px}
#gsSettingsPanel .gsx-pills{gap:4px}
#gsSettingsPanel .gsx-pill,#gsSettingsPanel .gsx-all{height:26px;padding:0 9px;font-size:.56rem}
#settingsModal #gsSettingsPanel .gsx-rows>.gs-settings-group{min-height:34px;padding:2px 2px;gap:8px}
#settingsModal #gsSettingsPanel .gsx-rows>.gs-settings-group>.gs-midi-lbl{flex:0 0 42%;max-width:190px}
@container (min-width:620px){
  #gsSettingsPanel .gsx-rows{display:grid;grid-template-columns:1fr 1fr;column-gap:20px}
  /* lignes larges (panneaux MIDI, listes, intégrés, ≥ 5 éléments) : sur toute la largeur */
  #settingsModal #gsSettingsPanel .gsx-rows>.gs-settings-group:has(.gs-midi-bar,#gsMidiPanel,#gsFuncLayoutPresetList),
  #settingsModal #gsSettingsPanel .gsx-rows>:not(.gs-settings-group),
  #settingsModal #gsSettingsPanel .gsx-rows>.gs-settings-group:last-child:nth-child(odd){grid-column:1/-1}
}

/* Séquenceur > Effets : curseurs compacts comme « Son » (2026-10-09) */
#page-melodie #seqFxBox>.sq-fx-knobs{display:grid;grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:1px 8px;flex:1 1 100%;align-items:end}
#page-melodie #seqFxBox>.sq-fx-knobs .sq-snd-p{min-width:0}
#page-melodie #seqFxBox>.sq-fx-knobs select,#page-melodie #seqFxBox>.sq-fx-knobs .dpk-select-btn{margin:0;max-width:100%}

/* Panneau Synthé / Effets (Paramètres > Surface, menu ⚙) : curseurs compacts (2026-10-09) */
#settingsModal #gsSettingsPanel .gs-params-grid,#page-play .gs-params-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(112px,1fr));gap:1px 8px;justify-content:stretch}
.gs-snd-p{display:grid;grid-template-columns:1fr auto;grid-template-areas:"l v" "s s";gap:0 4px;font-family:var(--fm);font-size:max(10px,.46rem);color:var(--tx3);min-width:0}
.gs-snd-p span{grid-area:l;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gs-snd-p b{grid-area:v;color:var(--c-root);white-space:nowrap}
.gs-snd-p input{grid-area:s;width:100%;height:18px;margin:0;accent-color:var(--c-root)}

/* Décompte du Backing (structures.js, _strCountdownStart) : le chiffre remplace ▶ sur les boutons Play ; en grand au centre si activé */
/* Pas sur un bouton déjà en attente « .ft-armed » (surface-live.js) : son badge rouge affiche déjà les temps restants, et les deux
   ::after se mélangeaient pendant la dernière mesure (badge étiré sur tout le bouton, chiffre noir, ▶ masqué). */
:is(#globalStrPlayBtn,#strPlayBtn,#gsBackingPlayBtn)[data-cd]:not(.ft-armed){color:transparent!important;position:relative}
:is(#globalStrPlayBtn,#strPlayBtn,#gsBackingPlayBtn)[data-cd]:not(.ft-armed)::after{content:attr(data-cd);position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--c-root);font-weight:800;font-size:1.05rem}
#strCountdownBig{position:fixed;inset:0;z-index:9990;display:flex;align-items:center;justify-content:center;pointer-events:none;font-family:var(--fm);font-weight:800;font-size:min(42vh,42vw);line-height:1;color:var(--c-root);text-shadow:0 0 40px rgba(0,0,0,.6),0 4px 30px rgba(0,0,0,.5);background:rgba(0,0,0,.28)}
#strCountdownBig.beat{animation:strCdPop .55s ease-out both}
@keyframes strCdPop{from{transform:scale(1.35);opacity:.2}to{transform:scale(1);opacity:1}}
:is(#globalStrPlayBtn,#strPlayBtn,#gsBackingPlayBtn)[data-cd]:not(.ft-armed).on::after{color:#111}

/* Navigation clavier dans Paramètres (demande explicite : « flèches directionnelles pour monter/descendre/changer
   la position des sélecteurs ») : contour + léger fond aux couleurs d'accent. Spécificité via #settingsModal pour
   passer au-dessus du zébrage .settings-row:nth-child. Pas de transition sous prefers-reduced-motion. */
#settingsModal .settings-kbd-active{outline:2px solid var(--c-root);outline-offset:-2px;background:color-mix(in srgb,var(--c-root) 14%,transparent);transition:outline-color var(--tr),background var(--tr)}
#settingsModal .settings-box:focus{outline:none}

/* Choix du style et de la structure dans un seul menu à deux colonnes. */
.str-template-menu{position:fixed;z-index:1200;display:grid;grid-template-columns:minmax(90px,1fr) minmax(150px,2fr);width:min(440px,calc(100vw - 16px));max-height:360px;background:var(--sf);border:1px solid var(--bd);border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,.3);overflow:hidden}
.str-template-menu[hidden]{display:none}
.str-undo-actions{display:inline-flex;align-items:center;gap:4px;flex-shrink:0}
.str-undo-actions>button{flex-shrink:0}
/* Sur mobile/tablette, ne pas replier les commandes dans un bloc étroit à droite de Natif/Play. */
@media(max-width:1023px){
  #strNativeOnlyControls:not([hidden]){display:contents!important}
  #strNativeOnlyControls[hidden]{display:none!important}
  #strToolbar{justify-content:flex-start;align-content:flex-start;row-gap:4px}
  #strToolbar>button,#strNativeOnlyControls>button,#strNativeOnlyControls>.tsep,#strNativeOnlyControls>.info-btn{flex-shrink:0}
  #strTemplateMenuBtn{max-width:calc(100% - 130px);overflow:hidden;text-overflow:ellipsis}
  #strOptBtn{margin-left:0!important}
}
.str-template-column{display:flex;flex-direction:column;min-width:0;min-height:0}
.str-template-column+.str-template-column{border-left:1px solid var(--bd)}
.str-template-heading{padding:8px 10px;font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx3);border-bottom:1px solid var(--bd)}
.str-template-list{overflow-y:auto;padding:4px;min-height:0}
.str-template-item{display:block;width:100%;border:0;border-radius:4px;background:transparent;color:var(--tx2);padding:7px 8px;text-align:left;font-family:var(--fm);font-size:max(11px,.6rem);cursor:pointer;white-space:normal}
.str-template-item:hover,.str-template-item:focus-visible{background:var(--s2);color:var(--tx);outline:1px solid var(--c-root);outline-offset:-1px}
.str-template-item[aria-pressed="true"]{background:var(--s2);color:var(--c-root)}
@media(prefers-reduced-motion:reduce){#settingsModal .settings-kbd-active{transition:none}}

/* ── Rotor (groove-rotor.js) : vue circulaire de la Groovebox, page Groovebox et zone Surface ─────────── */
.rotor{position:relative;display:flex;flex-direction:column;gap:3px;min-width:0;user-select:none;-webkit-user-select:none}
.rotor-bar{display:flex;align-items:center;justify-content:flex-start;gap:4px;flex-wrap:wrap}
/* barre de jeu + outils : sur UNE ligne quand la largeur le permet (la roue gagne la hauteur), sinon retour à la ligne */
.rotor-bars{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:4px 12px;flex-shrink:0;min-width:0}
.rotor-play-bar{display:flex;align-items:center;gap:6px;min-width:0;flex-wrap:wrap}
.rotor-play-bar:empty{display:none}
.rotor-play-bar>button{flex-shrink:0}
.rotor-play-bar>.dpk-select-btn{order:1;max-width:none;overflow:visible;text-overflow:clip}
.rotor-play-bar>[data-mirror=grooveUiModeSeg]{order:0}
/* La barre de jeu n'est nécessaire que pour le plein écran (subbar masquée) : les COPIES des contrôles de la subbar
   (4/8, kit, preset, pattern) n'y apparaissent donc que là ; GEN, CLR, GESTES, OPT, plein écran sont DÉPLACÉS. */
.rotor-bars .rotor-play-bar>[data-mirror],.rotor-bars .rotor-play-bar>.dpk-select-btn{display:none}
body.groove-rotor-fullscreen .rotor-bars .rotor-play-bar>.dpk-select-btn{display:inline-flex}
body.groove-rotor-fullscreen .rotor-bars .rotor-play-bar>.groove-mode-seg[data-mirror]{display:flex}
.rotor-play-bar>#grooveGestureBtn{order:2}
/* Hauteur unique pour TOUS les boutons des barres du Rotor (boutons, menus dpk, segments) ; plein écran tout à droite */
.rotor-bars{--rbh:32px;margin-bottom:8px}
@media(pointer:coarse){.rotor-bars{--rbh:40px}}
.rotor-bars button,.rotor-bars .dpk-select-btn{box-sizing:border-box;height:var(--rbh);min-height:var(--rbh);padding-top:0;padding-bottom:0;display:inline-flex;align-items:center;justify-content:center;line-height:1}
.rotor-bars .groove-mode-seg{height:var(--rbh);box-sizing:border-box}
.rotor-bars .groove-mode-seg button{height:calc(var(--rbh) - 2px);min-height:0}
.rotor-bars>#grooveFsBtn{margin-left:auto;width:var(--rbh);height:var(--rbh);padding:0;font-size:16px;flex-shrink:0}
/* Même bouton que le plein écran SURFACE (#gsFsBtn : .tbtn.gs-mode-btn, ↗ / ✕) — état actif identique */
#grooveFsBtn.on{background:var(--c-root);color:#111;border-color:var(--c-root)}
/* Tabs : bouton plein écran fermé (✕) en contour coloré, comme Surface (#gsOctRow button.on) */
#page-tablature #tbeFsBtn.on,.ft-module-fullscreen #tbeFsBtn.on{background:var(--s2)!important;color:var(--tx)!important;border-color:var(--c-root)!important;box-shadow:inset 0 0 0 1px var(--c-root)}
.rotor-btn{height:26px;padding:0 .5rem;border:1px solid var(--bd);background:var(--s3);color:var(--tx2);border-radius:4px;cursor:pointer;font:700 max(10px,.55rem)/1 var(--fm);white-space:nowrap}
.rotor-btn.on{background:var(--c-root);color:#111;border-color:var(--c-root)}
.rotor-main{display:flex;gap:10px;align-items:flex-start;justify-content:center;min-width:0}
.rotor-col{flex:0 1 auto;min-width:0;display:flex;flex-direction:column;align-items:center;gap:3px}
/* boutons momentanés Euclide / Tourner : en bas à gauche de la roue, tenus par un doigt pendant que l'autre dessine */
.rotor-momentary{position:absolute;left:6px;bottom:6px;z-index:4;display:flex;gap:14px}
.rotor-momentary .rotor-btn{width:50px;height:50px;padding:0;font-size:1.35rem;touch-action:none;user-select:none;-webkit-user-select:none}
.rotor-stage{position:relative;width:max(300px,calc(100vh - 290px));max-width:100%;aspect-ratio:1/1}
/* Page Groovebox en largeur (tablette) : boutons momentanés en triangle dans le coin bas-gauche de la roue (hors du
   cercle, comme en mode étroit) ; la barre d'outils (Dessin…MIDI) est déplacée en tête du panneau de gauche
   (groove-rotor.js) et GEN / CLR / GESTES / OPT / plein écran restent dans la sous-barre du module : la roue prend
   toute la hauteur. */
.groove-rotor-view .rotor:not(.rotor-narrow) .rotor-momentary{--mb:max(9.5%,34px);left:0;top:0;right:0;bottom:0;width:100%;height:100%;display:block;pointer-events:none}
.groove-rotor-view .rotor:not(.rotor-narrow) .rotor-momentary .rotor-btn{position:absolute;pointer-events:auto;width:var(--mb);height:auto;aspect-ratio:1;min-width:34px;min-height:34px;font-size:1.05rem;left:0;bottom:0}
.groove-rotor-view .rotor:not(.rotor-narrow) .rotor-momentary .rotor-btn:nth-child(2){left:calc(var(--mb) + 5px)}
.groove-rotor-view .rotor:not(.rotor-narrow) .rotor-momentary .rotor-btn:nth-child(3){bottom:calc(var(--mb) + 5px)}
.groove-rotor-view .rotor:not(.rotor-narrow) .rotor-bars{margin-bottom:0}
.rotor-panel>.rotor-bar{display:flex;flex-wrap:wrap;align-items:center;gap:4px;flex:none}
.rotor-panel>.rotor-bar .rl{display:none}
.rotor-panel>.rotor-bar button{height:38px;min-width:38px;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;padding:0 .5rem}
.rotor-panel>.rotor-bar .groove-mode-seg{height:38px;box-sizing:border-box}
.rotor-panel>.rotor-bar .groove-mode-seg button{height:36px;min-height:0}
/* Volume de la Groovebox retiré de la sous-barre (il reste dans le mixeur global) ; les éléments restent dans le DOM,
   masqués, car app.js les tient à jour (setGrooveVolume, sauvegarde, MIDI learn). */
#page-groove div:has(>#grooveVolSlider),#page-groove #grooveVolVal{display:none!important}
/* Mode étroit (téléphone en portrait) : les 3 boutons momentanés quittent la roue et forment une colonne à sa gauche
   (la colonne est élargie de la même marge à droite pour garder la roue centrée) ; barres un peu plus basses. */
.rotor-narrow .rotor-col{padding:0}
/* Les 3 boutons momentanés occupent le coin bas-gauche du carré de la roue (hors du cercle) : triangle de trois boutons
   dont la taille suit celle de la roue (9,5 %, 34px au moins) — la roue garde toute la largeur. */
.rotor-narrow .rotor-momentary{--mb:max(9.5%,34px);left:0;top:0;right:0;bottom:0;width:100%;height:100%;display:block;pointer-events:none}
.rotor-narrow .rotor-momentary .rotor-btn{position:absolute;pointer-events:auto;width:var(--mb);height:auto;aspect-ratio:1;min-width:34px;min-height:34px;font-size:1.05rem;left:0;bottom:0}
.rotor-narrow .rotor-momentary .rotor-btn:nth-child(2){left:calc(var(--mb) + 5px)}
.rotor-narrow .rotor-momentary .rotor-btn:nth-child(3){bottom:calc(var(--mb) + 5px)}
@media(pointer:coarse){.rotor-narrow .rotor-bars{--rbh:34px}}
/* Outils en icônes seules en mode étroit : Poly, Annuler, MIDI et Plein écran tiennent sur la même rangée (plus de rangée presque vide). */
.rotor-narrow .rotor-tools .rl{display:none}
/* Plein écran : sort de sa propre rangée et se pose dans le coin haut-droit de la zone de la roue (coin vide du carré). */
.rotor-narrow .rotor-bars{position:relative;gap:4px 3px}
/* Boutons du haut regroupés par fonction, sur le moins de rangées possible : mode / outils / Poly, Annuler, MIDI /
   GEN, CLR, Gestes, Opt / menus (sons, kit, préréglage). Les deux barres deviennent une seule rangée flexible. */
.rotor-narrow .rotor-bars>.rotor-play-bar,.rotor-narrow .rotor-bars>.rotor-bar{display:contents}
.rotor-narrow .rotor-bars .rotor-play-bar>.groove-mode-seg{order:0}
.rotor-narrow .rotor-bars .rotor-tools{order:1}
.rotor-narrow .rotor-bars [data-act=poly]{order:2}
.rotor-narrow .rotor-bars [data-act=undo]{order:3}
.rotor-narrow .rotor-bars [data-act=midi]{order:4}
.rotor-narrow .rotor-bars .rotor-play-bar>#grooveRandomAllBtn{order:5}
.rotor-narrow .rotor-bars .rotor-play-bar>#grooveClearBtn{order:6}
.rotor-narrow .rotor-bars .rotor-play-bar>#grooveGestureBtn{order:7}
.rotor-narrow .rotor-bars .rotor-play-bar>#grooveFileBtn{order:8}
.rotor-narrow .rotor-bars .rotor-play-bar>.dpk-select-btn{order:9}
.rotor-narrow .rotor-bars button:not(.groove-mode-seg button){padding-left:.4rem;padding-right:.4rem}
.rotor-narrow .rotor-bars #grooveFsBtn{position:absolute;right:0;top:100%;margin:4px 0 0;z-index:5}
@supports (height:100dvh){.rotor-stage{width:max(300px,calc(100dvh - 290px))}}
.rotor-side{flex:0 0 330px;display:flex;flex-direction:column;gap:6px;max-height:calc(100vh - 230px);overflow-y:auto;padding-right:2px}
.rotor-panel{flex:0 0 330px;min-width:0;min-height:0;max-height:calc(100dvh - 230px);overflow-y:auto;overscroll-behavior:contain;display:flex;flex-direction:column;gap:8px}
.rotor-panel>.rotor-side{flex:none;width:100%;max-height:none;overflow:visible}
.rotor-extras{display:flex;flex-direction:column;gap:8px;min-width:0}
.rotor-extras>.card{flex:none;margin:0!important}
.rotor-control{display:flex;align-items:center;gap:5px;min-width:0}
.rotor-control>label{flex:1;min-width:0}
.rotor-row .rotor-midi{flex:0 0 52px;min-height:26px;padding:0 3px;font-size:10px}
.rotor-row .rotor-midi.midi-assigned{border-color:var(--c);color:var(--c)}
.rotor-row .rotor-midi.midi-learning{background:var(--c);color:#111}
/* boutons MIDI des curseurs : masqués tant que ⌁ MIDI n'est pas activé (sauf assignés / en écoute) */
.rotor:not(.rotor-show-midi) .rotor-row .rotor-midi:not(.midi-assigned):not(.midi-learning){display:none}
.rotor-btn:disabled{opacity:.35;cursor:default}
.rotor-canvas:focus-visible{outline:2px solid var(--c-root);outline-offset:2px;border-radius:6px}
.rotor-row label.rotor-slider-locked::after{content:'🔒';font-size:10px;margin-left:2px;pointer-events:none}
.rotor-row label.rotor-slider-locked{opacity:.5;background:var(--s2);border-radius:4px;cursor:not-allowed}
.rotor-row label.rotor-slider-locked input{accent-color:#888;pointer-events:none}
.rotor-row label:focus-visible{outline:2px solid var(--c-root);outline-offset:2px}
.rotor-row{display:flex;flex-direction:column;gap:4px;padding:6px 8px;background:var(--s3);border:1px solid var(--bd);border-left:3px solid var(--c);border-radius:8px;font:600 max(10px,.55rem)/1.1 var(--fm);color:var(--tx2)}
.rotor-row.off{opacity:.55}
.rotor-row-h{display:flex;align-items:center;gap:6px}
.rotor-row-h i{width:9px;height:9px;border-radius:50%;background:var(--c);flex:none}
.rotor-row-h b{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rotor-row-h span,.rotor-row-x,.rotor-row-l{display:flex;align-items:center;gap:4px}
.rotor-row button{height:22px;min-width:24px;padding:0 .35rem;border:1px solid var(--bd);background:var(--s2);color:var(--tx2);border-radius:4px;cursor:pointer;font:700 max(10px,.55rem)/1 var(--fm)}
.rotor-row button.on{background:var(--c);border-color:var(--c);color:#111}
.rotor-row-v{display:flex;align-items:center;gap:6px}
.rotor-row-v input{flex:1;min-width:0;accent-color:var(--c)}
.rotor-row output{min-width:30px;text-align:right;color:var(--tx3)}
.rotor-row-l{color:var(--tx3)}
.rotor-row-l output{min-width:20px;text-align:center}
.rotor-row-l{flex:none}
.rotor-row-l.own output{color:var(--tx)}
.rotor-row-x select{flex:1;min-width:90px;background:var(--s2);color:var(--tx2);border:1px solid var(--bd);border-radius:5px;height:22px;font-size:max(10px,.55rem)}
.rotor-narrow .rotor-main{flex-direction:column;align-items:stretch}
.rotor-narrow .rotor-col{flex:none;width:100%;order:0}
.rotor-narrow .rotor-panel{order:1}
.rotor-narrow .rotor-panel-r{order:2}
.rotor-narrow .rotor-side{flex:none;max-height:none;display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr))}
.rotor-narrow .rotor-panel{flex:none;width:100%;max-height:35dvh}
.rotor-zone .rotor-main{flex:1;min-height:0;align-items:stretch}
.rotor-zone .rotor-side{max-height:none;flex:0 0 min(330px,46%)}
.rotor-zone.rotor-narrow .rotor-side{max-height:38%;overflow-y:auto}
.rotor-zone .rotor-panel{max-height:none;flex:0 0 min(330px,46%)}
.rotor-zone.rotor-narrow .rotor-panel{flex:0 0 38%;min-height:0;overflow-y:auto}
.rotor-zone.rotor-narrow .rotor-panel>.rotor-side{max-height:none;overflow:visible}
.rotor-zone .rotor-col{height:100%;flex:1 1 0}
.rotor-zone .rotor-stage{flex:1;width:100%;max-width:none;aspect-ratio:auto;min-height:0}
.rotor-zone.rotor-control-panel>.rotor-side{flex:1;width:100%;height:100%;max-height:none;overflow:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(min(220px,100%),1fr));align-content:start}
.rotor-zone.rotor-gesture-panel{--gesture-track-height:100%;--gesture-fader-height:100%}
.rotor-zone.rotor-gesture-panel>.rotor-side{display:flex;align-content:normal}
.rotor-canvas{position:absolute;inset:0;touch-action:none;cursor:pointer}
.rotor-pop{position:absolute;inset:auto 0 0 0;z-index:5;display:flex;justify-content:center;padding:6px}
.rotor-pop[hidden]{display:none}
.rotor-pop-in{position:relative;width:min(100%,360px);display:flex;flex-direction:column;gap:7px;padding:10px 12px;background:var(--s2);border:1px solid var(--bd);border-radius:10px;box-shadow:0 6px 24px rgba(0,0,0,.45);font:600 max(11px,.6rem)/1.2 var(--fm);color:var(--tx2)}
.rotor-pop-t{font-weight:800;color:var(--c);padding-right:26px}
.rotor-pop-in label{display:flex;align-items:center;gap:8px}
.rotor-pop-in label input[type=range]{flex:1;min-width:0;accent-color:var(--c-root)}
.rotor-pop-in label select{flex:1;min-width:0}
.rotor-pop-in output{min-width:34px;text-align:right;color:var(--tx3)}
.rotor-pop-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.rotor-pop-x{position:absolute;top:6px;right:6px;width:24px;height:24px;border:0;border-radius:5px;background:var(--s3);color:var(--tx3);cursor:pointer}
@media (max-width:560px){.rotor-tools button{padding:0 .35rem}}
.rotor-zone{padding:5px;box-sizing:border-box;background:var(--s1);border:1px solid var(--bd2);border-radius:10px;overflow:hidden}


.rotor-row .rotor-row-name{flex:1;min-width:0;display:flex;align-items:center;gap:6px;height:26px;padding:0 .5rem;text-align:left;background:var(--s2);border:1px solid var(--bd);border-radius:6px;color:var(--tx)}
.rotor-row .rotor-row-name:hover{border-color:var(--c)}
.rotor-row .rotor-row-name:active{background:var(--c);color:#111}
.rotor-row .rotor-row-name b{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

.groove-opt-menu{position:fixed;z-index:1000;display:flex;flex-direction:column;gap:8px;min-width:200px;padding:10px;background:var(--s2);border:1px solid var(--bd);border-radius:10px;box-shadow:0 8px 28px rgba(0,0,0,.45);font:600 max(11px,.6rem)/1.2 var(--fm);color:var(--tx2)}
.groove-opt-menu label{display:flex;flex-direction:column;gap:4px}
.groove-opt-menu label:has(input[type=checkbox]){flex-direction:row;align-items:center;gap:8px;cursor:pointer}
.groove-opt-menu label input[type=checkbox]{margin:0;flex:none}
.groove-opt-menu .dpk-select-btn{width:100%;text-align:left}
.groove-opt-menu select{height:28px}
.groove-opt-menu button{height:28px;padding:0 .6rem;border:1px solid var(--bd);background:var(--s3);color:var(--tx2);border-radius:6px;cursor:pointer;font:700 max(10px,.55rem)/1 var(--fm);text-align:left}

.rotor-row-s{display:flex;align-items:center;gap:6px}
.rotor-row-s span{flex:none;width:34px;color:var(--tx3)}
.rotor-row-s input{flex:1;min-width:0;accent-color:var(--c)}
.rotor-row-s output{min-width:34px;text-align:right;color:var(--tx3)}
/* Keep the wheel in the module viewport; track controls and bank share their own scroll area. */
#page-groove #grooveWrap.groove-rotor-view{overflow:hidden}
.groove-rotor-view>#grooveRotorCard{flex:1;min-height:0;display:flex;overflow:hidden}
.groove-rotor-view #grooveRotorHost{flex:1;min-height:0;height:100%;width:100%}
.groove-rotor-view .rotor-main{flex:1;min-height:0;height:100%;align-items:stretch}
.groove-rotor-view .rotor-panel{max-height:none;height:100%;overflow-y:auto;flex-shrink:0}
/* colonne de la roue = largeur de la roue (pas étirée) : les panneaux de pistes sont collés au cercle */
.groove-rotor-view .rotor-col{justify-content:flex-start;flex:0 1 auto;min-height:0}
.groove-rotor-view .rotor-stage{flex:none;max-width:100%;min-height:0}
.groove-rotor-view .rotor-narrow .rotor-col{flex:none;min-height:0}
.groove-rotor-view .rotor-narrow .rotor-panel{flex:1;min-height:0;height:auto;max-height:none}
/* Optical module: isolated camera workspace with FretoLogic audio/MIDI controls. */
.phrotor-toolbar{display:flex;flex-wrap:wrap;gap:6px;align-items:center;padding:0 0 8px;color:var(--tx)}
.phrotor-title{font:800 .8rem var(--fm);margin-right:4px}
.phrotor-toolbar .rotor-btn,.phrotor-toolbar .dpk-select-btn{height:28px}
#phrotorHostStatus{font-size:11px;color:var(--tx3);flex:1 1 140px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* phRotor natif : page défilante, cartes et boutons aux gabarits F-T (rotor-btn, groove-mode-seg, dpk) */
#page-phrotor .wrap{display:flex;flex-direction:column;flex:1;min-height:0;overflow-y:auto;padding:.6rem;width:100%;max-width:2400px;margin:0 auto}
.ph-root{min-width:0;padding-bottom:16px;font:13px/1.45 var(--fb)}
.ph-root .card{background:var(--s2);border:1px solid var(--bd);border-radius:10px;padding:12px;margin:0 0 10px}
.ph-root h3{font-size:15px;margin:2px 0 8px}
.ph-root h4{font-size:13px;margin:14px 0 4px}
.ph-nav{display:inline-flex;max-width:100%;overflow-x:auto;margin-bottom:10px}
.ph-nav button{height:32px;padding:0 .8rem}
.ph-work{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(280px,1fr);gap:10px;align-items:start}
.ph-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
.ph-grid .card,.ph-side .card{margin:0}
.ph-side{display:flex;flex-direction:column;gap:10px;min-width:0}
.ph-viewer{padding:0!important;overflow:hidden}
.ph-head,.ph-under{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:8px 12px;font-size:12px}
.ph-head{border-bottom:1px solid var(--bd)}
.ph-under{border-top:1px solid var(--bd);color:var(--tx3)}
.ph-badge{font:700 10px var(--fm);letter-spacing:1px;padding:2px 8px;border:1px solid var(--bd);border-radius:10px;color:var(--tx3)}
#phr-view{display:block;width:100%;background:var(--bg);touch-action:none;cursor:crosshair}
.ph-disc{position:relative;width:100%;max-width:min(100%,calc(100dvh - 230px));margin:0 auto}
.ph-fab{position:absolute;left:0;top:0;transform:translate(-50%,-50%);width:46px;height:46px;padding:0;border-radius:50%;border:1px solid var(--bd);background:var(--s2);color:var(--tx);display:flex;align-items:center;justify-content:center;cursor:grab;touch-action:none;box-shadow:0 2px 10px rgba(0,0,0,.4);z-index:2}
.ph-fab:active{cursor:grabbing}.ph-fab.on{background:var(--c-root);border-color:var(--c-root);color:#111}
@media(pointer:coarse){.ph-fab{width:56px;height:56px}}
.ph-eyebrow{margin:0 0 4px;font:700 10px var(--fm);letter-spacing:1.5px;color:var(--c-root)}
.ph-muted{margin:6px 0 0;font-size:11px;color:var(--tx3)}
.ph-root label{display:block;margin-top:10px;font-size:11px;color:var(--tx3)}
.ph-root label>select{margin-top:4px}
.ph-root .dpk-select-btn{display:flex;width:100%;max-width:none;justify-content:space-between;margin-top:4px;height:30px;overflow:hidden;text-overflow:ellipsis}
.ph-num{display:block;width:100%;margin-top:4px;height:30px;padding:0 8px;background:var(--bg);color:var(--tx);border:1px solid var(--bd);border-radius:5px;font:600 12px var(--fm)}
.ph-root input[type=range]{display:block;width:100%;margin-top:6px;accent-color:var(--c-root)}
.ph-root output{color:var(--c-root);font-size:11px}
.ph-row{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;align-items:flex-end}
.ph-row>label{flex:1 1 90px;min-width:0;margin-top:0}
.ph-row>.rotor-btn{flex:1 1 auto}
.ph-root .rotor-btn{height:auto;min-height:32px;padding:0 .7rem}
.ph-primary{background:var(--c-root)!important;color:#111!important;border-color:var(--c-root)!important}
.ph-file{position:relative;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;margin-top:0!important;color:var(--tx2)!important}
.ph-file input{display:none}
.ph-notes{display:flex;flex-wrap:wrap;gap:6px;min-height:28px}
.ph-notes span{border:1px solid var(--bd);border-radius:5px;padding:3px 8px;font:600 11px var(--fm)}
.ph-swatches{display:flex;gap:8px;flex-wrap:wrap}
.ph-root .ph-swatches .rotor-btn{width:32px;min-height:32px;padding:0;border-radius:50%}
.ph-swatches .rotor-btn.on{outline:2px solid var(--tx);outline-offset:2px}
.ph-lane{display:grid;grid-template-columns:1fr 1fr 1fr 40px;gap:6px;align-items:end;margin-top:6px}
.ph-prow{display:grid;grid-template-columns:70px 1fr 1fr 84px;gap:6px;align-items:end;margin-top:6px}
.ph-prow span{font-size:12px;padding-bottom:8px}
.ph-notice{position:sticky;bottom:0;padding:8px 0;background:var(--bg);border-top:1px solid var(--bd);color:var(--c-root);font-size:12px}
#phr-strip{width:100%;border-radius:5px}
.ph-root pre{font:11px/1.7 var(--fm);white-space:pre-wrap;max-height:400px;overflow:auto}
.ph-root dl{font-size:12px}.ph-root dt{color:var(--tx3)}.ph-root dd{margin:0 0 8px}
.ph-guide{max-width:820px}
.ph-guide li{padding:4px 0}
/* Studio stylet : disque + panneau de commandes à droite (dessous sur écran étroit), cartes en dessous */
.ph-studio .ph-stage{display:grid;grid-template-columns:280px minmax(0,1fr);align-items:start}
.ph-studio .ph-spanel{grid-column:1;grid-row:1}.ph-studio .ph-disc{grid-column:2;grid-row:1}
.ph-spanel{display:flex;flex-direction:column;gap:14px;padding:12px;border-right:1px solid var(--bd);min-width:0}
.ph-spanel section{min-width:0}
.ph-tight{margin-top:0}
.ph-seg{width:100%}.ph-seg button{flex:1;height:32px}
.ph-sdot{flex:none;width:22px;height:22px;border-radius:50%;box-shadow:0 0 0 1px var(--bd)}
.ph-scolors{display:flex;flex-direction:column;gap:3px;margin-top:6px}
#phr-instLabel b{color:var(--tx)}
.ph-scolor{display:grid;grid-template-columns:22px 58px minmax(0,1fr) auto;gap:8px;align-items:center;padding:5px 8px;border:1px solid transparent;border-radius:6px;background:none;color:var(--tx);font:12px var(--fb);text-align:left;cursor:pointer}
.ph-scolor:hover{background:var(--s3)}
.ph-scolor.dim .ph-sdot,.ph-scolor.dim .ph-sname{opacity:.4}
.ph-smini{display:flex;gap:3px}.ph-smini button{width:22px;height:22px;padding:0;border:1px solid var(--bd);border-radius:4px;background:var(--bg);color:var(--tx3);font:700 10px var(--fm);cursor:pointer}
.ph-smini button.on{background:var(--c-root);border-color:var(--c-root);color:#111}
.ph-scolor.on{border-color:var(--c-root);background:var(--s3)}
.ph-sinst{color:var(--tx3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ph-scolor.on .ph-sinst{color:var(--tx)}
.ph-spanel .rotor-btn.on{background:var(--c-root);border-color:var(--c-root);color:#111}
.ph-root [hidden]{display:none!important}
.ph-root label.ph-inline{display:inline-flex;align-items:center;gap:6px;margin:0;font-size:11px;color:var(--tx2)}
.ph-studio .ph-work{grid-template-columns:1fr}
.ph-studio .ph-side{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}
.ph-studio .ph-disc{max-width:min(100%,max(340px,calc(100dvh - 330px)))}
.ph-viewer:fullscreen{background:var(--bg);display:flex;flex-direction:column;overflow:auto}
.ph-viewer:fullscreen .ph-disc{max-width:min(100%,calc(100dvh - 90px))}
/* Panneau compact : moins de défilement. */
.ph-root .ph-file{display:inline-flex;align-items:center;justify-content:center;min-height:32px;font:600 11px var(--fm)}
.ph-spanel{gap:10px;padding:8px 10px}
.ph-spanel .ph-row{margin-top:4px;gap:4px}
.ph-spanel label{margin-top:5px}
.ph-spanel .ph-num,.ph-spanel .ph-seg button{height:26px}
.ph-root .ph-spanel .dpk-select-btn{height:26px;margin-top:2px}
.ph-root .ph-spanel .rotor-btn{min-height:28px;padding:0 .5rem}
.ph-spanel .ph-muted{margin-top:3px;font-size:10px;line-height:1.3}
.ph-spanel .ph-eyebrow{margin:0 0 2px}
.ph-scolor{padding:3px 6px}.ph-scolors{margin-top:3px;gap:1px}
/* Paysage : panneau à gauche sur 2 colonnes (les sections ne se coupent pas) pour limiter le défilement vertical. */
.ph-studio .ph-spanel{column-gap:14px}
.ph-pcol{display:contents}
.ph-studio .ph-pcol>section,.ph-studio .ph-pcol>.ph-row{break-inside:avoid;margin:0 0 10px}
@media(orientation:landscape) and (min-width:640px){
  .ph-studio .ph-stage{grid-template-columns:min(540px,62vw) minmax(0,1fr)}
  .ph-studio .ph-spanel{display:block;column-count:2;grid-column:1;grid-row:1;border-right:1px solid var(--bd);border-top:0}
  .ph-studio .ph-disc{grid-column:2;grid-row:1;max-width:min(100%,max(260px,calc(100dvh - 280px)))}
}
/* Paysage peu haut : 3 colonnes pour tenir sans défilement vertical du panneau. */
@media(orientation:landscape) and (min-width:900px) and (max-height:880px){
  .ph-studio .ph-stage{grid-template-columns:min(720px,64vw) minmax(0,1fr)}
  .ph-studio .ph-spanel{column-count:3;column-gap:12px}
}
@media(max-width:900px){.ph-studio .ph-stage{grid-template-columns:1fr}.ph-studio .ph-spanel,.ph-studio .ph-disc{grid-column:1;grid-row:auto}.ph-spanel{border-right:0;border-top:1px solid var(--bd);display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}}
@media(pointer:coarse){.ph-root .rotor-btn,.ph-num,.ph-root .dpk-select-btn{min-height:40px}.ph-root .ph-swatches .rotor-btn{width:40px;min-height:40px}}
@media(max-width:900px){.ph-work,.ph-grid{grid-template-columns:1fr}.ph-disc{max-width:min(100%,60dvh)}}
@media(max-width:760px){.phrotor-toolbar{gap:5px}.phrotor-title{width:100%;margin:0}.ph-prow{grid-template-columns:52px 1fr 1fr 76px}#page-phrotor .wrap{padding:.4rem}}
@media(max-height:520px) and (orientation:landscape){.ph-work{grid-template-columns:minmax(0,1fr) minmax(240px,1fr)}.ph-disc{max-width:min(100%,calc(100dvh - 130px))}}

#sidebarTabs .tab[data-tab="phrotor"] .tab-icon::before,#mainTabs .tab[data-tab="phrotor"] .tab-icon::before,.ft-ico[data-ico="phrotor"]{--ico:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='9' fill='none' stroke='black' stroke-width='1.7'/%3E%3Ccircle cx='12' cy='12' r='4' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M12 3v9' stroke='black' stroke-width='2'/%3E%3C/svg%3E")}

#grooveMidiOutDialog{width:min(560px,calc(100vw - 24px));max-height:85dvh;overflow:auto;border:1px solid var(--bd);border-radius:12px;background:var(--sf);color:var(--tx);padding:18px;font:13px var(--fb)}
#grooveMidiOutDialog::backdrop{background:#0009}
#grooveMidiOutDialog button,#grooveMidiOutDialog input,#grooveMidiOutDialog select{font:inherit;color:inherit;background:var(--s2);border:1px solid var(--bd);border-radius:5px;padding:6px;min-height:32px}
#grooveMidiOutDialog label{display:flex;flex-direction:column;gap:5px;margin:12px 0;min-width:0}
#grooveMidiOutDialog p{color:var(--tx2);line-height:1.5}
.groove-midi-heading{display:flex;align-items:center;justify-content:space-between;gap:12px}
.groove-midi-heading>label{flex:1}
.groove-midi-routes{display:grid;grid-template-columns:minmax(74px,1fr) 32px 56px 56px;gap:6px;align-items:center}
.groove-midi-routes>[data-track]{display:contents}
.groove-midi-routes input{width:100%;min-width:0}
#grooveMidiOutBody [data-status]{min-height:20px}
/* Groovebox fullscreen: same app chrome hiding as Surface, with transport and routing retained. */
body.groove-fullscreen .topbar,
body.groove-fullscreen .desktop-root-row,
body.groove-fullscreen .sidebar,
body.groove-fullscreen .sidebar-desktop,
body.groove-fullscreen .mobile-nav,
body.groove-fullscreen #btnTabBack{display:none!important}
body.groove-fullscreen #page-groove.page.active{position:fixed;inset:0;top:0!important;z-index:60;overflow:hidden;background:var(--bg);zoom:1!important}
body.groove-fullscreen #grooveWrap{max-width:none;margin:0;zoom:1!important}
body.groove-rotor-fullscreen #grooveToolbarWrap{display:none!important}
body.groove-fullscreen #ftBattCorner{display:block;top:auto;bottom:3px;right:12px}
/* Optional finger performance panel: two broad vertical ranges per track, sharing the Rotor controls. */
#grooveGestureCard{position:relative;min-height:0;overflow:hidden}
#grooveGestureCard:not([hidden]){display:flex;flex-direction:column}
.groove-gesture-actions{display:flex;justify-content:flex-end;padding-bottom:6px;flex-shrink:0}
.groove-gesture-actions .rotor-btn{min-height:36px;padding:4px 12px}
#grooveGestureHost{flex:1;height:100%;width:100%;min-height:0}
.rotor-gesture-panel>.rotor-side{display:flex;flex-direction:row;gap:8px;width:100%;height:100%;min-height:0;max-height:none;overflow-x:auto;overflow-y:hidden;overscroll-behavior-x:contain;flex:1;scrollbar-width:thin;padding:2px 2px 8px}
.rotor-gesture-panel .rotor-gesture-track{flex:0 0 92px;min-width:92px;height:var(--gesture-track-height,min(100%,220px));min-height:0;gap:6px;padding:6px}
.rotor-gesture-name{display:flex;align-items:center;gap:6px;min-height:18px;font-size:11px;white-space:nowrap}
.rotor-gesture-name i{width:8px;height:8px;flex:none;border-radius:50%;background:var(--c)}
.rotor-gesture-faders{display:flex;align-items:stretch;justify-content:space-between;gap:6px;flex:1;min-height:0}
.rotor-gesture-faders>label{display:flex;flex-direction:column;align-items:center;gap:5px;flex:1;min-width:32px;min-height:0;font-size:10px}
.rotor-gesture-faders output{min-width:0;width:100%;text-align:center;font-size:11px}
.rotor-gesture-faders input[type=range]{writing-mode:vertical-lr;direction:rtl;appearance:none;-webkit-appearance:none;width:32px;min-width:32px;height:100%;min-height:70px;max-height:var(--gesture-fader-height,140px);flex:1;margin:0;padding:0;background:transparent;touch-action:none;cursor:ns-resize}
.rotor-gesture-faders input::-webkit-slider-runnable-track{width:12px;height:100%;border:1px solid var(--bd);border-radius:5px;background:linear-gradient(to bottom,transparent calc(50% - 1px),var(--tx) calc(50% - 1px) calc(50% + 1px),transparent calc(50% + 1px)),linear-gradient(to top,var(--c) 0 var(--slider-fill,0%),var(--s2) var(--slider-fill,0%) 100%)}
.rotor-gesture-faders input::-webkit-slider-thumb{appearance:none;-webkit-appearance:none;width:28px;height:16px;margin-left:-8px;border:2px solid var(--sf);border-radius:5px;background:var(--c)}
.rotor-gesture-faders input::-moz-range-track{width:12px;height:100%;border:1px solid var(--bd);border-radius:5px;background:linear-gradient(to bottom,transparent calc(50% - 1px),var(--tx) calc(50% - 1px) calc(50% + 1px),transparent calc(50% + 1px)),var(--s2)}
.rotor-gesture-faders input::-moz-range-thumb{width:26px;height:14px;border:2px solid var(--sf);border-radius:5px;background:var(--c)}
.rotor-gesture-faders .rotor-slider-locked input::-webkit-slider-thumb{background:var(--tx3)}
.rotor-gesture-faders .rotor-slider-locked input::-webkit-slider-runnable-track{background:linear-gradient(to bottom,transparent calc(50% - 1px),var(--tx) calc(50% - 1px) calc(50% + 1px),transparent calc(50% + 1px)),var(--s3)}
.rotor-gesture-faders .rotor-slider-locked input::-moz-range-thumb{background:var(--tx3)}
#grooveWrap.groove-gesture-mode:not(.groove-rotor-view){height:var(--groove-gesture-workspace-height);flex:0 0 var(--groove-gesture-workspace-height);overflow:hidden}
.groove-gesture-mode:not(.groove-rotor-view)>#grooveGridCard{flex:1;min-height:0;overflow:auto}
.groove-gesture-mode:not(.groove-rotor-view)>#grooveGestureCard{flex:0 0 30%;min-height:150px;max-height:240px}
.groove-gesture-mode:not(.groove-rotor-view)>.card:not(#grooveGridCard):not(#grooveGestureCard){display:none!important}
.groove-gesture-mode.groove-rotor-view #grooveRotorHost .rotor-extras>.card:not(#grooveGestureCard){display:none!important}
.groove-gesture-mode.groove-rotor-view #grooveRotorHost .rotor-extras{flex:1;min-height:0;overflow:hidden}
.groove-gesture-mode.groove-rotor-view #grooveGestureCard{flex:1;height:100%;max-height:240px;min-height:0;margin:0}
.groove-gesture-mode.groove-rotor-view #grooveGestureCard.groove-gesture-sized{max-height:none}
.groove-gesture-mode.groove-rotor-view #grooveRotorHost:not(.rotor-narrow)>.rotor-main>.rotor-panel{flex-basis:var(--groove-gesture-panel-width,min(460px,52%))}
.groove-gesture-resize{position:absolute;padding:0;border:0;background:transparent;touch-action:none;user-select:none;z-index:3;color:var(--c-root)}
.groove-gesture-resize-top{top:0;left:calc(50% - 22px);width:44px;height:18px;cursor:ns-resize}
.groove-gesture-resize-top::after{content:"";position:absolute;top:5px;left:8px;width:28px;height:3px;border-radius:2px;background:var(--c-root)}
.groove-gesture-resize-corner{bottom:0;right:0;width:28px;height:28px;cursor:nwse-resize}
.groove-gesture-resize-corner::after{content:"";position:absolute;right:5px;bottom:5px;width:9px;height:9px;border-right:2px solid var(--c-root);border-bottom:2px solid var(--c-root)}
.groove-gesture-resize:focus-visible{outline:2px solid var(--c-root);outline-offset:-2px}
#grooveGestureCard.groove-gesture-sized{max-width:100%;min-height:190px}

/* Fullscreen toggle stays at the right edge of the transport bar. */
#grooveToolbarWrap .groove-subbar{overflow-x:hidden!important}
#grooveToolbarWrap #grooveCtrlBar{flex:1 1 0;min-width:0;width:auto;overflow-x:auto!important}
#grooveToolbarWrap #grooveFsBtn{flex:0 0 28px;width:28px;height:28px;padding:0;margin-left:auto;align-self:center;font-size:14px}
/* Mobile/tablette portrait : la barre Groovebox passe à la ligne au lieu d'être rognée (overflow-x:hidden + OPT collant
   masquaient #grooveStyle et « GESTES », inaccessibles faute de scroll). OPT/plein écran descendent sur leur propre ligne. */
@media(max-width:1023px){
  #grooveToolbarWrap .groove-subbar{overflow-x:visible!important;flex-wrap:wrap!important;row-gap:4px}
  #grooveToolbarWrap #grooveCtrlBar{flex:1 1 0;min-width:0;width:auto;overflow:visible!important;flex-wrap:wrap!important}
  #grooveToolbarWrap #grooveCtrlBar .groove-ctrl-row{flex-wrap:wrap;overflow:visible;row-gap:4px}
}
@media(max-width:600px){#grooveToolbarWrap #grooveCtrlBar{flex-basis:100%;width:100%}}

/* ═══ Paramètres > Surface : lisibilité (2026-10-14) ═══ */
/* libellé + contrôle d'une même paire ne se séparent jamais à la mise à la ligne (Instrument/Octave/Taille accords) */
.gs-midi-bar>.gs-pair{display:inline-flex;align-items:center;gap:5px;flex-wrap:nowrap;min-width:0;max-width:100%}
.gs-midi-bar>.gs-pair>.gs-midi-lbl{flex:none;white-space:nowrap}
.gs-midi-bar>.gs-pair>:is(select,.dpk-select-btn){min-width:0;max-width:200px}
.gs-midi-bar>.gs-pair>input[type=range]{width:90px;min-width:60px}
/* lignes seules (impaires, pleine largeur) : le contrôle garde une largeur raisonnable au lieu de s'étirer */
@container (min-width:620px){
  #settingsModal #gsSettingsPanel .gsx-rows>.gs-settings-group:last-child:nth-child(odd)>:is(select,.dpk-select-btn,input[type=range],.gs-volume-slider){flex:0 1 320px}
  #settingsModal #gsSettingsPanel .gsx-rows>.gs-settings-group:last-child:nth-child(odd)>.gs-midi-lbl{flex:0 0 42%;max-width:none}
}
/* « Charger… » ne doit pas être rogné à côté d'un bouton (Preset des cellules de fonction) */
#settingsModal #gsSettingsPanel .gsx-rows>.gs-settings-group>.dpk-select-btn{min-width:104px}
/* Synthé / Zone d'accords : texte plus lisible dans le panneau Paramètres */
#settingsModal #gsSettingsPanel .gs-settings-embed{font-size:max(11px,.62rem)}
#settingsModal #gsSettingsPanel .gs-settings-embed button,
#settingsModal #gsSettingsPanel .gs-settings-embed select,
#settingsModal #gsSettingsPanel .gs-settings-embed input{font-size:max(11px,.6rem)}
#settingsModal #gsSettingsPanel .gs-settings-embed .gs-snd-p{font-size:max(11px,.56rem)}
#settingsModal #gsSettingsPanel .gs-settings-embed .gs-midi-lbl{font-size:max(11px,.58rem)}

/* Groovebox : banque de patterns sur UNE ligne, mode chanson compact (sans titre répété, boutons sur peu de lignes) */
#grooveBankCard,#grooveAdvancedPatternCard{padding:8px 10px!important}
#grooveBankCard .card-title,#grooveAdvancedPatternCard .card-title{font-size:max(11px,.7rem)!important;margin:0 0 6px!important;line-height:1.2}
#grooveBankGrid{flex-wrap:nowrap!important;gap:3px!important;margin:0!important}
#grooveBankGrid>button{flex:1 1 0!important;min-width:0!important;padding-left:0!important;padding-right:0!important;text-align:center;overflow:hidden}
#grooveAdvancedPatternCard{display:flex;flex-wrap:wrap;align-items:center;gap:4px 6px}
#grooveAdvancedPatternCard>.card-title{flex:0 0 100%}
#grooveAdvancedPatternCard>div{margin:0!important;gap:4px!important}
#grooveAdvancedPatternCard>div>span{display:none}
#grooveAdvancedPatternCard>#grooveSongChainList{flex:1 1 100%}
#grooveAdvancedPatternCard>div[style*="border-top"]{border-top:none!important;padding-top:0!important}

/* GESTES en vue Rotor : faders Euclide / Rotation dans les panneaux de la roue — 4 pistes à gauche, 4 à droite,
   toutes entières ; redimensionnement vertical seul (poignée en bas du panneau). La carte GESTES n'est plus utilisée. */
.groove-rotor-view #grooveGestureCard{display:none!important}
.rotor-gesturing .rotor-extras{display:none}
.rotor-gesturing:not(.rotor-narrow) .rotor-panel{flex:0 0 auto;width:calc(4*84px + 3*8px + 18px);height:var(--rotor-gest-h,100%);position:relative;overflow:hidden;padding-bottom:16px}
.rotor-gesturing .rotor-panel>.rotor-side{display:flex;flex-direction:row;gap:8px;width:100%;height:100%;max-height:none;overflow:hidden;padding:2px;--gesture-track-height:100%;--gesture-fader-height:100%}
.rotor-gesturing .rotor-gesture-track{flex:1 1 0;min-width:0;height:100%;gap:6px;padding:6px}
.rotor-gesturing.rotor-narrow .rotor-panel{width:100%;height:var(--rotor-gest-h,220px);position:relative;overflow:hidden;padding-bottom:16px}
.rotor-gest-resize{position:absolute;left:calc(50% - 26px);bottom:0;width:52px;height:16px;padding:0;border:0;background:transparent;cursor:ns-resize;touch-action:none;z-index:3}
.rotor-gest-resize::after{content:"";position:absolute;left:8px;right:8px;top:6px;height:3px;border-radius:2px;background:var(--c-root)}

/* Zone Rotor de Surface : pastille du kit de batterie (en haut à gauche) */
.rotor-zone-kit{position:absolute;top:4px;left:4px;z-index:5;height:28px;max-width:60%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;opacity:.85;touch-action:manipulation}
.rotor-zone-kit:hover,.rotor-zone-kit:focus-visible{opacity:1}
.smp-btn.smp-del.armed{background:var(--bad,#e05656);border-color:var(--bad,#e05656);color:#fff}
/* Vider / Confirmer ? : même emplacement et même largeur dans les deux états (le bouton ne bouge pas) */
.smp-btn.smp-del{box-sizing:border-box;width:11.5ch;min-width:11.5ch;justify-content:center;text-align:center;white-space:nowrap;flex:0 0 auto}

/* Groovebox en vue Rotor (tablette) : la sous-barre tient sur UNE rangée (les deux lignes du module s'écoulent ensemble,
   elle ne passe à la ligne que si la place manque) et la carte de la roue perd son remplissage : la roue gagne en hauteur. */
@media(min-width:1024px){
  #page-groove:has(#grooveWrap.groove-rotor-view) #grooveCtrlBar>.groove-ctrl-row{display:contents}
  #page-groove:has(#grooveWrap.groove-rotor-view) #grooveCtrlBar{flex-wrap:wrap!important;row-gap:4px}
}
#grooveWrap.groove-rotor-view>#grooveRotorCard{padding:4px!important}

/* Bouton plein écran de la Groovebox : coin supérieur droit du carré de la roue (vue Rotor, largeur) */
.rotor:not(.rotor-narrow)>#grooveFsBtn{position:absolute;top:0;right:0;z-index:6;width:36px;height:36px;padding:0;margin:0;font-size:16px;flex:none}
body.groove-rotor-fullscreen .rotor:not(.rotor-narrow)>.rotor-bars{padding-right:44px}

/* Plein écran du Rotor : tous les boutons (play-bar + outils + fermer) sur UNE seule ligne, outils en icônes seules */
body.groove-rotor-fullscreen .rotor-bars{flex-wrap:nowrap;align-items:center;gap:4px 8px}
body.groove-rotor-fullscreen .rotor-bars>.rotor-play-bar,body.groove-rotor-fullscreen .rotor-bars>.rotor-bar{flex-wrap:nowrap;flex:0 0 auto}
body.groove-rotor-fullscreen .rotor-bars .rotor-tools .rl{display:none}

/* Mode Gestes (Rotor, largeur) : pistes de faders resserrées (2 faders de 24 px par piste, moins d'espacement) pour que
   les deux panneaux prennent ~25 % de largeur de moins et que la roue gagne la place libérée. */
.rotor-gesturing:not(.rotor-narrow) .rotor-panel{width:calc(4*62px + 3*5px + 12px)}
.rotor-gesturing:not(.rotor-narrow) .rotor-panel>.rotor-side{gap:5px;padding:2px 1px}
.rotor-gesturing:not(.rotor-narrow) .rotor-gesture-track{padding:5px 3px;gap:4px}
.rotor-gesturing:not(.rotor-narrow) .rotor-gesture-faders{gap:2px}
.rotor-gesturing:not(.rotor-narrow) .rotor-gesture-faders>label{min-width:0;gap:3px}
.rotor-gesturing:not(.rotor-narrow) .rotor-gesture-faders input[type=range]{width:24px;min-width:24px}
.rotor-gesturing:not(.rotor-narrow) .rotor-gesture-faders input::-webkit-slider-thumb{width:22px;margin-left:-5px}
.rotor-gesturing:not(.rotor-narrow) .rotor-gesture-name{font-size:10px;gap:4px;overflow:hidden}
.rotor-gesturing:not(.rotor-narrow) .rotor-gesture-name b{overflow:hidden;text-overflow:ellipsis}

/* La largeur des panneaux Gestes était imposée par flex-basis (460 px / 52 %, règle #grooveRotorHost plus haut) : elle
   l'emporte sur width, d'où l'absence d'effet du resserrement. On la fixe ici, avec la même spécificité d'id. */
.groove-gesture-mode.groove-rotor-view #grooveRotorHost.rotor-gesturing:not(.rotor-narrow)>.rotor-main>.rotor-panel{flex:0 0 calc(4*62px + 3*5px + 12px)!important;width:calc(4*62px + 3*5px + 12px)!important;max-width:calc(4*62px + 3*5px + 12px)!important}

/* Mode Gestes : la barre d'outils en tête du panneau prend sa place, les faders se partagent le reste (plus de
   débordement en bas de page) */
.rotor-gesturing .rotor-panel{display:flex;flex-direction:column}
.rotor-gesturing .rotor-panel>.rotor-bar{flex:0 0 auto}
.rotor-gesturing .rotor-panel>.rotor-side{height:auto;flex:1 1 0;min-height:0}

/* ARP compact (zone + fenêtre) : potards 30px, libellés sur une ligne, sélecteurs et puces serrés */
#page-play .gsa-host{gap:3px}
#page-play .gsz-body.gsa-host{padding:4px 6px}
#page-play .gsa-head{gap:3px}
#page-play .gsa-chip{height:24px;padding:0 8px;border-radius:12px}
#page-play .gsa-sels{gap:4px}
#page-play .gsa-sels .sound-select,#page-play .gsa-sels .dpk-select-btn{min-height:0;height:26px;padding-top:0;padding-bottom:0}
#page-play .gsa-pattern,#page-play .gsz-body.gsa-host .gsa-pattern{height:26px}
#page-play .gsa-knobs{gap:0 1px;margin-top:0;justify-content:flex-start}
#page-play .gsa-knobs .tb303-knob-wrap,#page-play .gsz-body.gsa-host .gsa-knobs .tb303-knob-wrap{width:46px;min-width:46px;gap:0}
#page-play .gsa-knobs .tb303-knob-label{min-height:0;display:block;max-width:46px;font-size:max(9px,.46rem);line-height:1.1}
#page-play .gsa-knobs .tb303-knob-svg{width:30px;height:30px}
#page-play .gsa-knobs .tb303-knob-val{font-size:max(9px,.48rem);line-height:1}
@media (pointer:coarse){#page-play .gsa-knobs .tb303-knob-svg{width:34px;height:34px}}


/* Réglages Son : sous-onglets + égaliseur paramétrique visuel (2026-10-15).
   Valeurs à largeur FIXE : une étiquette qui change de largeur (« -3 dB » → « -12.0 dB », « 999 Hz » → « 1.0 kHz »)
   rétrécissait le curseur sous le doigt, qui « vibrait » en tactile. */
.snd-tabs{display:flex;gap:6px;flex-wrap:wrap;margin:2px 0 4px}
.snd-tab{height:30px;padding:0 12px;border-radius:15px;border:1px solid var(--bd2,var(--bd));background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.6rem;font-weight:700;cursor:pointer;white-space:nowrap}
.snd-tab.active{border-color:var(--c-root);color:var(--c-root)}
.snd-pane{display:none;flex-direction:column;gap:10px}
.snd-pane.active{display:flex}
.settings-section[data-settings-tab="sound"] .settings-val{min-width:60px;text-align:right;font-variant-numeric:tabular-nums}
.settings-section[data-settings-tab="sound"] .settings-slider{touch-action:pan-y}
.peq-box{display:flex;flex-direction:column;gap:8px;padding:8px;background:var(--s2);border:1px solid var(--bd);border-radius:6px}
.peq-canvas{width:100%;height:190px;display:block;border-radius:4px;background:var(--bg,transparent);touch-action:none;cursor:crosshair}
.peq-hint{font-size:.58rem;color:var(--tx3,var(--tx2))}
.peq-rows{display:flex;flex-direction:column;gap:6px}
.peq-row{display:grid;grid-template-columns:6.4em minmax(0,1fr) 6.6em 2.6em;align-items:center;gap:.7em;font-size:.62rem}
.peq-row .settings-slider{width:100%;min-width:0;touch-action:pan-y}
.peq-lbl{font-family:var(--fm);font-size:1em;color:var(--tx2);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.peq-val{font-family:var(--fm);font-size:1em;color:var(--tx2);text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap;min-width:0;overflow:hidden}
.peq-selname{font-family:var(--fm);font-size:.6rem;font-weight:700;color:var(--c-root);text-transform:uppercase;letter-spacing:.06em;padding-top:4px;border-top:1px solid var(--bd)}
.peq-rst{width:100%;min-width:0;height:28px;border-radius:6px;border:1px solid var(--bd);background:transparent;color:var(--tx2);font-size:.8rem;cursor:pointer;line-height:1;padding:0}
.peq-rst:hover,.peq-btn:hover{border-color:var(--c-root);color:var(--c-root)}
.peq-actions{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap}
.peq-btn{background:transparent;border:1px solid var(--bd);color:var(--tx2);font-family:var(--fm);font-size:.6rem;padding:5px 10px;border-radius:4px;cursor:pointer}

/* Égaliseur par module : même grille que le général (curseur pleine largeur, valeur à largeur fixe). */
.peq-row3{grid-template-columns:8.6em minmax(0,1fr) 6.6em 2.6em}
.peq-lbl small{color:var(--tx3,var(--tx2));font-size:.9em}
#setEqBands{gap:10px}

/* Boosts graves / aigus : shelfs réglés par potentiomètres ronds (tb303KnobEl) */
.peq-shelves{display:grid;grid-template-columns:1fr 1fr;gap:8px}
@media (max-width:520px){.peq-shelves{grid-template-columns:1fr}}
.peq-shelf{border:1px solid var(--bd);border-radius:6px;padding:6px 8px;display:flex;flex-direction:column;gap:6px}
.peq-shelf.sel{border-color:var(--c-root)}
.peq-shelf-h{display:flex;align-items:center;gap:8px;font-family:var(--fm);font-size:.62rem;color:var(--tx2)}
.peq-shelf-h b{color:var(--c-root);text-transform:uppercase;letter-spacing:.05em}
.peq-shelf-h i{flex:1;font-style:normal;color:var(--tx3,var(--tx2))}
.peq-shelf-h .peq-rst{width:2.6em;flex:none}
.peq-knobs{display:flex;justify-content:space-around;gap:6px}
.peq-knobs .tb303-knob-wrap{display:flex;flex-direction:column;align-items:center;gap:2px;width:64px}
.peq-knobs .tb303-knob-label{font-family:var(--fm);font-size:max(10px,.5rem);color:var(--tx3,var(--tx2));white-space:nowrap}
.peq-knobs .tb303-knob-val{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--c-root);font-weight:700;white-space:nowrap}
.peq-knobs .tb303-knob-track{fill:none;stroke:var(--s3);stroke-width:4;stroke-linecap:round}
.peq-knobs .tb303-knob-fill{fill:none;stroke:var(--c-root);stroke-width:4;stroke-linecap:round}
.peq-knobs .tb303-knob-dot{fill:var(--tx)}
.peq-knobs .tb303-knob-svg{touch-action:none;cursor:ns-resize}

/* Gamme par accord (app.js : csfRender / csRenderLine) */
.cs-bar{display:flex;flex-direction:column;gap:5px;min-width:0;padding:6px;border:1px solid var(--bd);border-radius:var(--r);background:var(--s2)}
.cs-bar:empty{display:none}
.cs-row{display:flex;align-items:center;gap:4px;min-width:0}
.cs-nav{flex-shrink:0;width:24px;height:24px;border-radius:50%;border:1px solid var(--bd);background:var(--pbg);color:var(--tx2);font-family:var(--fm);font-size:.7rem;line-height:1;cursor:pointer;padding:0}
.cs-nav:hover{border-color:var(--c-root);color:var(--c-root)}
.cs-chips{display:flex;gap:4px;overflow-x:auto;flex:1;min-width:0;scrollbar-width:thin}
.cs-chip{flex-shrink:0;display:inline-flex;flex-direction:column;align-items:flex-start;gap:1px;padding:3px 7px;border-radius:6px;border:1px solid var(--bd);background:var(--s2);color:var(--tx2);font-family:var(--fm);font-size:max(10px,.52rem);line-height:1.2;cursor:pointer;text-align:left}
.cs-chip b{font-family:var(--fd);font-size:.74rem}
.cs-chip.alt span{color:var(--c-root)}
.cs-chip.on{border-color:var(--c-root);background:rgba(255,107,0,.1)}
.cs-bar .cs-chip:not(.on){background:var(--sf)}
.cs-empty{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx3)}
.cs-info{font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx2);line-height:1.4}
.cs-info:empty{display:none}
.cs-info b{color:var(--tx)}
.cs-alts{color:var(--tx3)}
.cs-legend{display:inline-flex;align-items:center;gap:4px;margin-left:8px;color:var(--tx3);white-space:nowrap}
.cs-legend i{display:inline-block;width:10px;height:10px;border-radius:50%;margin-left:4px}
.cs-lg-chord{border:2px solid var(--tx)}
.cs-lg-changed{border:2px dashed #ff6b00}
.cs-lg-avoid{background:var(--tx3);opacity:.35}
.cs-line{display:flex;flex-wrap:wrap;align-items:center;gap:4px}
.cs-line:empty{display:none}
.cs-line-lbl{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx3);margin-right:2px}
.cs-line-lbl small{font-size:inherit;opacity:.8}

/* Actions de fichier communes : même accès, hors des réglages audio et du contenu éditable. */
.ft-file-btn{width:auto;flex-shrink:0;margin-left:auto;white-space:nowrap}  /* apparence (hauteur, fond, police) : définition unique plus bas (« Bouton OPT : une seule apparence ») */
.subbar .ft-file-btn,.triads-subbar-scroll>.ft-file-btn{position:sticky;right:0;z-index:2}
.ft-file-legacy{display:none!important}
.mini-zoom-controls{display:flex;align-items:center;justify-content:flex-end;gap:4px;padding:2px 5px;font-family:var(--fm);font-size:10px;color:var(--tx2);flex-shrink:0}
.mini-zoom-controls button{width:26px;height:24px;padding:0;border:0;border-radius:0;background:transparent;color:var(--tx);cursor:pointer;touch-action:manipulation}
.mini-zoom-controls button:disabled{opacity:.35;cursor:default}
.mini-zoom-controls output{min-width:42px;text-align:center;font-variant-numeric:tabular-nums}
#csFollowBar .cs-chip{padding:calc(3px * var(--cs-zoom,1)) calc(7px * var(--cs-zoom,1));font-size:calc(max(10px,.52rem) * var(--cs-zoom,1))}
#csFollowBar .cs-chip b{font-size:calc(.74rem * var(--cs-zoom,1))}
@media (max-width:640px){
  /* Ne jamais laisser le groupe d'ajout de pistes recouvrir Annuler/Rétablir. */
  #page-melodie .sq-bar{flex-wrap:wrap}
  #page-melodie .sq-transport{flex:1 0 100%;width:100%}
  #page-melodie .sq-opts{flex:1 0 100%;width:100%;justify-content:flex-end}
}

/* ═══ Tablature (tab-editor.js) — 2026-10-16 : barre fixe, partition défilante, dock de saisie en bas. Les ids
   (#page-tablature > …) priment sur la règle desktop générique « .page > * » (flex:1, plafond 1600px). ═══ */
#page-tablature{overflow:hidden}
#page-tablature.active{display:flex;flex-direction:column}
#page-tablature > .tbe-bar{flex:none;display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;gap:6px 12px;padding:6px 8px;background:var(--sf);border-bottom:1px solid var(--bd);max-width:none}
#page-tablature > .tbe-head{flex:none;display:flex;flex-direction:row;flex-wrap:wrap;align-items:baseline;gap:4px 12px;padding:6px 12px 0;max-width:none;margin:0;overflow:visible}
#page-tablature > .tbe-scroll{flex:1;min-height:120px;overflow:auto;display:block;max-width:none;margin:0;padding:2px 8px 10px;box-sizing:border-box;-webkit-overflow-scrolling:touch}
#page-tablature > .tbe-dock{flex:0 1 auto;min-height:0;display:flex;flex-direction:column;gap:5px;max-width:none;margin:0;padding:6px 8px calc(6px + env(safe-area-inset-bottom));background:var(--sf);border-top:1px solid var(--bd);max-height:52vh;overflow-x:hidden;overflow-y:auto}
#page-tablature .tbe-grp{display:flex;align-items:center;gap:5px;flex-wrap:wrap}
#page-tablature .tbe-right{margin-left:auto}
#page-tablature .tbe-tempo{font-family:var(--fm);font-size:.62rem;color:var(--tx2)}
#page-tablature .tbe-tempo b{color:var(--tx);min-width:28px;text-align:center;font-size:.74rem}
#page-tablature .tbe-tempo small{font-size:max(10px,.5rem);margin-right:2px}
#page-tablature .tbe-btn,#page-tablature .tbe-mini{width:32px;height:30px;border-radius:8px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);display:inline-flex;align-items:center;justify-content:center;cursor:pointer;padding:0;touch-action:manipulation;font-family:var(--fm);font-weight:700}
#page-tablature .tbe-btn:disabled{opacity:.35;cursor:default}
#page-tablature .tbe-play.on,#page-tablature .tbe-btn.on{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-tablature .tbe-tg,#page-tablature .tbe-fxb,#page-tablature .tbe-fb,#page-tablature .tbe-db,#page-tablature .tbe-sel{height:30px;min-width:30px;padding:0 8px;border-radius:7px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.66rem;font-weight:700;cursor:pointer;touch-action:manipulation;white-space:nowrap;display:inline-flex;align-items:center;justify-content:center;gap:4px}
#page-tablature .tbe-tg.on,#page-tablature .tbe-fxb.on,#page-tablature .tbe-fb.on,#page-tablature .tbe-db.on{background:color-mix(in srgb,var(--c-root) 22%,var(--s2));border-color:var(--c-root);color:var(--tx)}
#page-tablature .tbe-fb{min-width:34px;padding:0 4px;font-size:.74rem}
#page-tablature .tbe-db{min-width:34px;padding:0 6px}
#page-tablature .tbe-row{display:flex;flex-wrap:wrap;align-items:center;gap:4px}
#page-tablature .tbe-sep{width:1px;height:20px;background:var(--bd);margin:0 3px}
#page-tablature .tbe-pos{margin-left:auto;font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx3);white-space:nowrap}
#page-tablature .tbe-fill{font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx3);margin-left:6px;display:inline-flex;align-items:center;gap:6px}
#page-tablature .tbe-fill.tbe-short{color:#d9a23a}
#page-tablature .tbe-fill.tbe-over{color:#e5484d}
#page-tablature .tbe-fill button{height:24px;padding:0 8px;border-radius:6px;border:1px solid currentColor;background:transparent;color:inherit;font:inherit;font-weight:700;cursor:pointer}
#page-tablature .tbe-title{background:none;border:none;padding:0;color:var(--tx);cursor:pointer;font-family:var(--fd);font-size:1rem;text-align:left}
#page-tablature .tbe-title span{color:var(--tx2);font-weight:400;font-size:.85rem}
#page-tablature .tbe-save-state{display:inline-flex;flex:none;align-items:center;gap:4px;padding:3px 6px;border:1px solid var(--bd);border-radius:5px;color:var(--tx3);font-family:var(--fm);font-size:max(10px,.55rem);white-space:nowrap}
#page-tablature .tbe-save-state{background:none;cursor:pointer}
#page-tablature .tbe-save-state:hover{border-color:var(--bd2)}
#page-tablature .tbe-save-state[data-state="saved"]{cursor:default}
#page-tablature .tbe-save-state[data-state="saved"]{color:var(--c-root);border-color:color-mix(in srgb,var(--c-root) 40%,var(--bd));background:color-mix(in srgb,var(--c-root) 9%,transparent)}
@media(max-width:700px){#page-tablature .tbe-save-state>span{display:none}#page-tablature .tbe-save-state{padding:3px}}
#page-tablature .tbe-meta{font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx3)}
#page-tablature .tbe-zoom{margin-left:auto;display:inline-flex;align-items:center;gap:4px;font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx3)}
#page-tablature .tbe-zoom button{height:26px;min-width:32px;border-radius:6px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.66rem;font-weight:700;cursor:pointer}
#page-tablature .tbe-zoom b{min-width:40px;text-align:center;font-weight:400}
#page-tablature .tbe-neckwrap{display:flex;align-items:stretch;gap:8px;flex:1 1 156px;min-height:156px}
#page-tablature .tbe-neck{overflow-x:auto;flex:1;min-width:0;-webkit-overflow-scrolling:touch;cursor:pointer}
#page-tablature .tbe-check{display:inline-flex;align-items:center;gap:5px;font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx2);white-space:nowrap;cursor:pointer}
#page-tablature .tbe-input{height:30px;min-width:0;flex:0 1 220px;padding:0 9px;border-radius:7px;border:1px solid var(--bd2);background:var(--bg);color:var(--tx);font-family:var(--fm);font-size:.72rem}
#page-tablature .tbe-sel{padding:0 4px}
/* Partition (SVG). Les mêmes classes sont redéfinies en noir sur blanc pour l'export (TBE_PRINT_CSS). */
#page-tablature .tbe-score{user-select:none;-webkit-user-select:none;touch-action:pan-x pan-y}
.tbe-svg{display:block;overflow:visible}
.tbe-svg text{font-family:var(--fb)}
.tbe-svg .tbe-str{stroke:var(--tx3);stroke-width:.8;opacity:.75}
.tbe-svg .tbe-bar{stroke:var(--tx2);stroke-width:1}
.tbe-svg .tbe-barh,.tbe-svg .tbe-rdot{fill:var(--tx2)}
.tbe-svg .tbe-lbl{font-family:var(--fm);font-size:10px;fill:var(--tx3)}
.tbe-svg .tbe-mnum{font-family:var(--fm);font-size:9px;fill:var(--tx3)}
.tbe-svg .tbe-mnum.tbe-short{fill:#d9a23a;font-weight:700}
.tbe-svg .tbe-mnum.tbe-over{fill:#e5484d;font-weight:700}
.tbe-svg .tbe-mk{font-size:11px;font-weight:700;fill:var(--c-root)}
.tbe-svg .tbe-rep{font-family:var(--fm);font-size:10px;font-weight:700;fill:var(--tx2)}
.tbe-svg .tbe-ts{font-size:13px;font-weight:700;fill:var(--tx)}
.tbe-svg .tbe-nbg{fill:var(--bg)}
.tbe-svg .tbe-fret{font-family:var(--fm);font-size:11.5px;font-weight:500;fill:var(--tx)}
.tbe-svg .tbe-dim{fill:var(--tx3)}
.tbe-svg .tbe-tiedash{stroke:var(--tx3);stroke-width:1.2;stroke-linecap:round}
/* Note en cours de lecture : contour blanc, la pastille garde sa couleur (couleur de note ou fond). */
.tbe-svg .tbe-nt.on .tbe-nbg{stroke:#fff!important;stroke-width:2.4!important;opacity:1!important}
/* Couleurs de notes : léger contour pour détacher les pastilles claires (Do, Mi) du fond */
.tbe-svg .tbe-col .tbe-nbg{stroke:rgba(0,0,0,.35);stroke-width:.6}
.tbe-svg .tbe-selr{stroke:var(--c-root);stroke-width:1;stroke-dasharray:3 2}
#page-tablature .tbe-zoom .tbe-colbtn{padding:0 8px}
#page-tablature .tbe-zoom .tbe-colbtn.on{background:color-mix(in srgb,var(--c-root) 22%,var(--s2));border-color:var(--c-root)}
#page-tablature .tbe-voice{max-width:190px}
/* Colonne de droite du manche : largeur fixe, les boutons A−/A+ restent alignés à droite quel que soit le zoom. */
/* Fenêtre du manche de hauteur FIXE (comme la zone de la partition) : le zoom agrandit le contenu, qui défile
   dans la fenêtre ; le dock ne change jamais de taille, donc rien ne bouge autour. */
#page-tablature .tbe-neckwrap>.tbe-neck{flex:1 1 0;min-width:0;min-height:0;overflow:auto;overscroll-behavior:contain}
#page-tablature .tbe-dockzoom{margin-left:8px;flex:none}
#page-tablature .tbe-score{cursor:cell}
.tbe-svg .tbe-ch{font-size:11.5px;font-weight:700;fill:var(--tx)}
.tbe-svg .tbe-nname{font-family:var(--fm);font-size:9.5px;font-weight:700;fill:var(--tx2)}
.tbe-svg .tbe-nnbg{stroke:rgba(0,0,0,.3);stroke-width:.6}
/* Voix 2 : pastille à contour pointillé, hampes plus discrètes dans la rangée du dessous */
.tbe-svg .tbe-v2 .tbe-nbg{stroke:var(--tx2);stroke-width:1;stroke-dasharray:2 1.5}
.tbe-svg .tbe-stem2{stroke:var(--tx3)}
.tbe-svg .tbe-v2whole{fill:none;stroke:var(--tx3);stroke-width:1.2}
#page-tablature .tbe-v2btn.on{background:color-mix(in srgb,var(--c-root) 30%,var(--s2));border-color:var(--c-root)}
/* Bouton « TAB » des cartes de voicing (Accords) : sous le bouton lecture */
.vcard-tab{top:52px;right:5px;width:auto;height:20px;border-radius:10px;padding:0 6px;font-size:max(10px,.5rem);font-family:var(--fm);font-weight:700}
.tbe-svg .tbe-tx,.tbe-svg .tbe-fx{font-size:10px;font-style:italic;fill:var(--tx2)}
.tbe-svg .tbe-fxl{stroke:var(--tx3);stroke-width:.8;stroke-dasharray:3 2;fill:none}
.tbe-svg .tbe-arc,.tbe-svg .tbe-bend,.tbe-svg .tbe-vib{stroke:var(--tx2);fill:none;stroke-width:1}
.tbe-svg .tbe-dash{stroke-dasharray:2 2}
.tbe-svg .tbe-arcl{font-size:7.5px;fill:var(--tx2)}
.tbe-svg .tbe-slide{stroke:var(--tx2);stroke-width:1.2}
.tbe-svg .tbe-bendh{fill:var(--tx2)}
.tbe-svg .tbe-bendl{font-size:8.5px;font-weight:700;fill:var(--tx2)}
.tbe-svg .tbe-stem{stroke:var(--tx2);stroke-width:1}
.tbe-svg .tbe-beam,.tbe-svg .tbe-dot,.tbe-svg .tbe-rest{fill:var(--tx2)}
.tbe-svg .tbe-restq,.tbe-svg .tbe-flag{stroke:var(--tx2);fill:none;stroke-width:1.3}
.tbe-svg .tbe-tup{stroke:var(--tx3);fill:none;stroke-width:.8}
.tbe-svg .tbe-tupl{font-size:8.5px;font-style:italic;fill:var(--tx3)}
.tbe-svg .tbe-selbox{fill:color-mix(in srgb,var(--c-root) 14%,transparent)}
.tbe-svg .tbe-curcol{fill:color-mix(in srgb,var(--c-root) 9%,transparent);stroke:color-mix(in srgb,var(--c-root) 35%,transparent);stroke-width:1}
.tbe-svg .tbe-curcell{fill:none;stroke:var(--c-root);stroke-width:1.8}
.tbe-svg .tbe-ph{stroke:var(--c-root);stroke-width:2;opacity:.9}
/* Manche de saisie */
.tbe-necksvg{display:block}
.tbe-necksvg .tbe-nfret{stroke:var(--bd2);stroke-width:1}
.tbe-necksvg .tbe-nut{stroke:var(--tx2);stroke-width:3}
.tbe-necksvg .tbe-nstr{stroke:var(--tx3);stroke-width:1.3}
.tbe-necksvg .tbe-nstr.on{stroke:var(--c-root);stroke-width:2.2}
.tbe-necksvg .tbe-nlbl,.tbe-necksvg .tbe-nnum{font-family:var(--fm);font-size:12px;fill:var(--tx3)}
.tbe-necksvg .tbe-ninlay{fill:var(--s3)}
.tbe-necksvg .tbe-nsc{fill:color-mix(in srgb,var(--tx2) 35%,transparent)}
.tbe-necksvg .tbe-nsc.root{fill:color-mix(in srgb,var(--c-root) 60%,transparent)}
.tbe-necksvg .tbe-nnote{fill:var(--c-root)}
.tbe-necksvg .tbe-nnotel{font-family:var(--fm);font-size:12px;font-weight:700;fill:#111}
.tbe-touch-help{position:fixed;z-index:10300;box-sizing:border-box;width:max-content;max-width:min(340px,calc(100vw - 24px));padding:10px 12px;border:1px solid var(--bd2);border-radius:8px;background:var(--s2);color:var(--tx);box-shadow:0 6px 20px var(--sh);font-family:var(--fb);font-size:max(12px,.72rem);line-height:1.45;pointer-events:none;overflow-wrap:break-word}
#page-tablature button{-webkit-touch-callout:none}
/* Fenêtres */
.tbe-modal{position:fixed;inset:0;z-index:8500;background:var(--ov);display:flex;align-items:center;justify-content:center;padding:12px}
.tbe-dlg{background:var(--sf);border:1px solid var(--bd);border-radius:var(--rl);box-shadow:0 10px 40px var(--sh);width:min(440px,100%);max-height:calc(100vh - 24px);display:flex;flex-direction:column;color:var(--tx)}
.tbe-dlg.wide{box-sizing:border-box;flex:0 0 auto;width:min(760px,calc(100vw - 24px));max-width:calc(100vw - 24px)}
.tbe-dlg-h{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid var(--bd);font-family:var(--fd)}
.tbe-dlg-b{padding:12px 14px;overflow-y:auto;display:flex;flex-direction:column;gap:8px}
.tbe-x{background:none;border:none;color:var(--tx2);font-size:1rem;cursor:pointer;padding:4px 6px}
.tbe-dlg .tbe-tg,.tbe-dlg .tbe-pbtn,.tbe-seg button{height:32px;padding:0 12px;border-radius:7px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:.68rem;font-weight:700;cursor:pointer}
.tbe-dlg .tbe-pbtn{background:var(--c-root);border-color:var(--c-root);color:#111}
.tbe-dlg .tbe-danger{color:#e5484d}
.tbe-dlg .tbe-danger.confirm{background:#e5484d;border-color:#e5484d;color:#fff}
.tbe-dlg-actions{display:flex;flex-wrap:wrap;gap:6px;justify-content:flex-end}
.tbe-seg{display:flex;flex-wrap:wrap;gap:4px}
.tbe-seg button.on{background:color-mix(in srgb,var(--c-root) 22%,var(--s2));border-color:var(--c-root)}
.tbe-library-modal{--tbe-lib-top:clamp(12px,var(--tbe-lib-anchor-bottom),max(12px,calc(100dvh - 240px)));align-items:flex-start;justify-content:center;padding-top:var(--tbe-lib-top)}
.tbe-library-modal .tbe-library-dlg{max-height:calc(100dvh - var(--tbe-lib-top) - 12px)}
.tbe-library-dlg .tbe-dlg-h{gap:8px;flex-wrap:wrap}
.tbe-library-dlg .tbe-dlg-h>.tbe-seg{margin-left:auto;flex-wrap:nowrap}
.tbe-library-dlg .tbe-dlg-h>.tbe-seg button{padding:0 9px}
@media(max-width:480px){
  .tbe-library-dlg .tbe-dlg-h>.tbe-seg{order:3;flex:1 0 100%;margin-left:0}
  .tbe-library-dlg .tbe-dlg-h>.tbe-seg button{flex:1;min-width:0;padding:0 6px}
  .tbe-library-dlg .tbe-dlg-h>.tbe-x{margin-left:auto}
}
/* Export : use the dialog width for format choices, settings and actions. */
.tbe-dlg.wide.tbe-export-dlg{width:min(960px,calc(100vw - 24px))}
.tbe-export-dlg .tbe-dlg-b{gap:14px;padding:14px 16px}
.tbe-export-dlg #tbeExpSeg{gap:6px}
.tbe-export-dlg #tbeExpSeg button{flex:1 0 auto;padding:0 8px;white-space:nowrap}
.tbe-export-dlg .tbe-export-fields{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:14px}
.tbe-export-dlg .tbe-export-fields>label{display:flex;flex-direction:column;gap:6px;min-width:0;font-family:var(--fm);font-size:max(11px,.6rem);color:var(--tx2)}
.tbe-export-dlg .tbe-export-fields .dpk-select-btn{box-sizing:border-box;width:100%;max-width:none;min-width:0;min-height:36px;height:auto;justify-content:space-between;padding:8px 10px;white-space:normal;text-align:left}
.tbe-export-dlg .tbe-export-checks,.tbe-export-dlg #tbeExpImg{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 16px}
.tbe-export-dlg .tbe-check{min-height:32px;line-height:1.4}
.tbe-export-dlg #tbeExpImg>.tbe-dlg-actions,.tbe-export-dlg #tbeExpPdfNote{grid-column:1/-1}
.tbe-export-dlg .tbe-dlg-actions{gap:10px;margin-top:12px}
.tbe-export-dlg .tbe-dlg-actions>button{flex:1 1 0;min-width:0;min-height:38px;height:auto;padding:9px 12px;white-space:normal}
.tbe-export-dlg #tbeVidNote{margin-top:12px}
/* Sélecteur à 3 positions (même aspect que Manche / Édition / Les deux de la zone de saisie) */
.tbe-seg3{display:inline-flex;gap:2px;padding:2px;border:1px solid var(--bd);border-radius:8px}
.tbe-dlg .tbe-seg3 .tbe-tg{height:28px;padding:0 12px;border-radius:6px;font-size:.62rem}
.tbe-dlg .tbe-seg3 .tbe-tg.on{background:var(--c-root);border-color:var(--c-root);color:#111}
.tbe-export-dlg .tbe-export-bg{grid-column:1/-1;display:flex;align-items:center;flex-wrap:wrap;gap:6px 12px;font-family:var(--fm);font-size:max(11px,.6rem);color:var(--tx2)}
.tbe-export-dlg .tbe-export-other{border-top:1px solid var(--bd);padding-top:12px}
.tbe-export-dlg .tbe-export-other .tbe-h4{margin:0}
@media(max-width:850px){.tbe-export-dlg .tbe-export-fields{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:540px){
  .tbe-export-dlg .tbe-dlg-b{padding:12px;gap:12px}
  .tbe-export-dlg #tbeExpSeg{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}
  .tbe-export-dlg #tbeExpSeg button{min-width:0;min-height:36px;height:auto;padding:8px;white-space:normal}
  .tbe-export-dlg .tbe-export-fields>label:nth-child(-n+2){grid-column:1/-1}
  .tbe-export-dlg .tbe-export-checks,.tbe-export-dlg #tbeExpImg{grid-template-columns:minmax(0,1fr)}
}
.tbe-ta{width:100%;min-height:150px;box-sizing:border-box;resize:vertical;border-radius:8px;border:1px solid var(--bd2);background:var(--bg);color:var(--tx);font-family:var(--fm);font-size:.7rem;padding:8px;white-space:pre;overflow:auto}
.tbe-dlg .tbe-mono{min-height:240px}
.tbe-note{margin:0;font-size:.72rem;color:var(--tx3);line-height:1.4}
.tbe-h4{margin:6px 0 0;font-family:var(--fd);font-size:.8rem;color:var(--tx2)}
.tbe-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px}
.tbe-form label{display:flex;flex-direction:column;gap:4px;font-family:var(--fm);font-size:max(10px,.56rem);color:var(--tx3)}
.tbe-form label.tbe-form-full{grid-column:1/-1}
.tbe-form label.tbe-check{flex-direction:row;align-items:center;color:var(--tx2)}
.tbe-form input[type=text],.tbe-form input[type=number],.tbe-form select{height:32px;border-radius:7px;border:1px solid var(--bd2);background:var(--bg);color:var(--tx);font-family:var(--fb);font-size:.8rem;padding:0 8px;min-width:0}
.tbe-inline{display:flex;align-items:center;gap:6px}
.tbe-inline select{flex:1}
.tbe-check{display:inline-flex;align-items:center;gap:6px;font-size:.74rem;color:var(--tx2);cursor:pointer}
.tbe-lib{display:flex;flex-direction:column;gap:5px}
.tbe-lib-section{display:flex;flex-direction:column;gap:5px}
.tbe-lib-section+.tbe-lib-section{border-top:1px solid var(--bd2);margin-top:10px;padding-top:10px}
.tbe-lib-group{display:flex;align-items:center;gap:8px;margin:4px 0 6px;font-family:var(--fd);font-size:.85rem;color:var(--tx)}
.tbe-lib-group span,.tbe-lib-tab-count{font-family:var(--fm);font-size:max(10px,.58rem);font-weight:400;color:var(--tx2)}
.tbe-lib-tab-count{display:inline-block;min-width:1.5ch;padding:1px 4px;border-radius:4px;background:var(--bg);text-align:center}
.tbe-library-dlg .tbe-dlg-h,.tbe-library-dlg .tbe-lib-controls{flex-shrink:0}
.tbe-library-dlg .tbe-dlg-b{min-height:0}
.tbe-lib-controls{padding:10px 14px 8px;display:flex;flex-direction:column;gap:8px;border-bottom:1px solid var(--bd)}
.tbe-lib-controls .tbe-lib-search{width:100%;min-width:0;box-sizing:border-box;height:34px;padding:0 10px;border:1px solid var(--bd2);border-radius:7px;background:var(--bg);color:var(--tx);font-family:var(--fm);font-size:max(12px,.72rem)}
.tbe-lib-search::placeholder{color:var(--tx2);opacity:1}
.tbe-lib-search:focus-visible{outline:2px solid var(--c-root);outline-offset:1px}
.tbe-lib-filters{display:flex;align-items:center;flex-wrap:wrap;gap:6px}
.tbe-lib-filters .tbe-tg{height:28px;padding:0 8px}
.tbe-lib-filters .tbe-tg[aria-pressed="true"]{border-color:var(--c-root);color:var(--c-root)}
.tbe-lib-count,.tbe-lib-number{color:var(--tx2);font-family:var(--fm);font-size:max(11px,.6rem)}
.tbe-lib-number{display:inline-block;min-width:3ch;white-space:nowrap;margin-right:10px;padding-right:10px;border-right:1px solid var(--bd);color:var(--tx3);font-weight:400;font-variant-numeric:tabular-nums}
.tbe-library-dlg .tbe-lib-color{flex:0 0 30px;width:30px;height:32px;padding:5px;border:1px solid var(--bd);border-radius:6px;background:transparent;cursor:pointer}
.tbe-lib-color span{display:block;width:16px;height:16px;box-sizing:border-box;border-radius:50%;background:var(--tbe-lib-color,transparent);border:1px solid var(--tx3)}
.tbe-lib-color:focus-visible{outline:2px solid var(--c-root);outline-offset:2px}
@media(max-width:600px){.tbe-library-dlg .tbe-li>.tbe-lib-color{position:absolute;top:4px;right:0}.tbe-library-dlg .tbe-li{position:relative}.tbe-library-dlg .tbe-li>.tbe-li-open{padding-right:36px}}
.tbe-li{display:flex;align-items:center;gap:5px;padding:4px;border-radius:9px;border:1px solid var(--bd)}
.tbe-li.on{border-color:var(--c-root)}
.tbe-li-open{flex:1;min-width:0;display:flex;flex-direction:column;align-items:flex-start;gap:2px;background:none;border:none;color:var(--tx);cursor:pointer;padding:4px 6px;text-align:left}
.tbe-li-open small{font-family:var(--fm);font-size:max(10px,.55rem);color:var(--tx3)}
@media(max-width:600px){
  .tbe-library-dlg .tbe-li{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}
  .tbe-library-dlg .tbe-li-open{grid-column:1/-1;min-width:0;width:100%;box-sizing:border-box}
  .tbe-library-dlg .tbe-li>.tbe-tg{width:100%;min-width:0;min-height:34px;height:auto;padding:7px 8px;white-space:normal}
}
.tbe-keys-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;align-items:start}
.tbe-keys{width:100%;table-layout:fixed;border-collapse:collapse;font-size:.74rem}
.tbe-keys th{text-align:left;font-family:var(--fm);white-space:normal;overflow-wrap:break-word;width:32%;padding:4px 12px 4px 0;color:var(--tx);vertical-align:top}
.tbe-keys td{width:68%;padding:4px 0;color:var(--tx2);vertical-align:top;overflow-wrap:break-word}
/* Écran étroit : chaque rangée du dock tient sur une ligne et défile horizontalement (le pavé et les
   techniques restent visibles sans faire défiler le dock). */
@media (max-width:700px){
  #page-tablature .tbe-right{margin-left:0}
  #page-tablature .tbe-row{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch}
  #page-tablature .tbe-row::-webkit-scrollbar{display:none}
  #page-tablature .tbe-row>*{flex:none}
  #page-tablature .tbe-pos{margin-left:8px}
  #page-tablature .tbe-fb{min-width:32px}
  #page-tablature .tbe-input{flex:1 0 140px}
}

/* Mode lecture (élève) : pas de saisie, bandeau des diagrammes d'accords de la tab. */
#page-tablature.tbe-ro .tbe-nav-tools,
#page-tablature.tbe-ro [data-tempo],
#page-tablature.tbe-ro #tbeSwingSel,
#page-tablature.tbe-ro #tbeSwingSel+.dpk-select-btn,
#page-tablature.tbe-ro #tbeVoiceSel+.dpk-select-btn,
#page-tablature.tbe-ro #tbeUndoBtn,
#page-tablature.tbe-ro #tbeRedoBtn{opacity:.35;pointer-events:none;filter:grayscale(1)}
#page-tablature.tbe-ro .tbe-title{cursor:default;pointer-events:none}
#page-tablature > .tbe-chords{flex:none;display:flex;flex-direction:row;gap:8px;padding:6px 10px;overflow-x:auto;overflow-y:hidden;max-width:none;margin:0;border-bottom:1px solid var(--bd);-webkit-overflow-scrolling:touch}
#page-tablature > .tbe-chords[hidden]{display:none}
#page-tablature .tbe-chd{flex:none;padding:3px 6px 2px;border:1px solid var(--bd);border-radius:9px;background:var(--s2);transition:border-color .12s,background .12s}
#page-tablature .tbe-chd.on{border-color:var(--c-root);box-shadow:0 0 0 1px var(--c-root);background:var(--sf)}
#page-tablature .tbe-chd svg{display:block}
#page-tablature .tbe-cbn{font-size:12px;font-weight:700;fill:var(--tx)}
#page-tablature .tbe-chd.on .tbe-cbn{fill:var(--c-root)}
#page-tablature .tbe-cbl{stroke:var(--tx3);stroke-width:.8}
#page-tablature .tbe-cbnut{fill:var(--tx2)}
#page-tablature .tbe-cbd{fill:var(--tx)}
#page-tablature .tbe-chd.on .tbe-cbd{fill:var(--c-root)}
#page-tablature .tbe-cbo{fill:none;stroke:var(--tx2);stroke-width:1}
#page-tablature .tbe-cbx{font-size:9px;fill:var(--tx2)}
#page-tablature .tbe-cbfr{font-size:8.5px;fill:var(--tx3)}
/* Barres repliées : transport seul */
#page-tablature.tbe-min > .tbe-bar > .tbe-grp:not(:first-child),#page-tablature.tbe-min > .tbe-head,#page-tablature.tbe-min > .tbe-dock{display:none}
#page-tablature.tbe-min > .tbe-bar{padding:3px 8px}
/* Mobile : groupes de la barre sur une seule ligne défilante (au lieu de 5 lignes empilées), dock plus bas. */
@media (max-width:700px){
  #page-tablature > .tbe-bar{gap:4px 8px;padding:4px 8px}
  #page-tablature > .tbe-bar > .tbe-grp:not(:first-child){flex-wrap:nowrap;overflow-x:auto;width:100%;margin-left:0;scrollbar-width:none;-webkit-overflow-scrolling:touch}
  #page-tablature > .tbe-bar > .tbe-grp:not(:first-child)::-webkit-scrollbar{display:none}
  #page-tablature > .tbe-bar > .tbe-grp>*{flex:none}
  #page-tablature > .tbe-head{padding-top:2px}
}
@media (max-width:700px){#page-tablature .tbe-neckwrap{flex-basis:150px;min-height:150px}}
.ph-spanel .ph-seg button{flex:1 1 0;min-width:0;padding:0 .2rem;overflow:hidden;text-overflow:ellipsis;font-size:10px}
/* Poignée de redimensionnement partition / zone de saisie */
#page-tablature > .tbe-split{flex:none;display:flex;flex-direction:row;align-items:center;justify-content:center;height:14px;max-width:none;margin:0;width:100%;overflow:visible;cursor:row-resize;touch-action:none;background:var(--sf);border-top:1px solid var(--bd)}
#page-tablature > .tbe-split span{width:48px;height:4px;border-radius:2px;background:var(--bd2);transition:background .12s}
#page-tablature > .tbe-split:hover span,#page-tablature > .tbe-split.on span{background:var(--c-root)}
/* Le panneau d'édition suit les réglages de transparence du fond des modules. */
#page-tablature.module-bg-enabled > .tbe-dock{
  background:color-mix(in srgb,var(--sf) clamp(0%,calc(var(--glass-opacity,84%) - 25%),100%),transparent);
  
}
#page-tablature.module-bg-enabled > .tbe-split{background:transparent}
#page-tablature.tbe-min > .tbe-split{display:none}
@media (pointer:coarse){#page-tablature > .tbe-split{height:22px}#page-tablature > .tbe-split span{width:64px;height:5px}}
#page-tablature .tbe-head .tbe-pos{margin-left:auto;margin-right:10px}
#page-tablature .tbe-nav .tbe-dockzoom{margin-left:auto}
#page-tablature .tbe-nav .tbe-check{margin-left:2px}
/* En-tête : une seule ligne de hauteur stable (le texte de position ne doit jamais la faire passer sur deux lignes,
   sinon la partition saute pendant une sélection à la souris). */
#page-tablature > .tbe-head{flex-wrap:nowrap;align-items:center;min-height:30px}
#page-tablature > .tbe-head .tbe-title{flex:0 1 auto;min-width:0;max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#page-tablature > .tbe-head .tbe-meta,#page-tablature > .tbe-head .tbe-pos{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#page-tablature > .tbe-head .tbe-zoom{flex:none;margin-left:0}
@media (max-width:700px){#page-tablature > .tbe-head .tbe-meta{display:none}}
/* Rangée de navigation compacte : tout (déplacement, édition, écoute, manche, gamme, zoom) sur une seule ligne. */
#page-tablature .tbe-nav{gap:3px;flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none}
#page-tablature .tbe-nav::-webkit-scrollbar{display:none}
#page-tablature .tbe-nav>*{flex:none}
#page-tablature .tbe-nav .tbe-tg{padding:0 6px}
#page-tablature .tbe-nav .tbe-btn{width:30px}
#page-tablature .tbe-nav .tbe-sep{margin:0 1px}

/* Fader de vitesse du moteur : curseur vertical long, dans le coin bas-droit du disque */
.ph-fader{position:absolute;right:2%;bottom:5%;width:54px;height:min(56%,290px);min-height:170px;display:flex;flex-direction:column;align-items:center;gap:4px;padding:6px 4px 5px;border:1px solid var(--bd);border-radius:10px;background:color-mix(in srgb,var(--s2) 90%,transparent);box-shadow:0 2px 10px rgba(0,0,0,.4);z-index:2}
.ph-fcap{display:none}
.ph-fval{font:700 9px/1.15 var(--fm);color:var(--c-root);text-align:center;white-space:pre-line;min-height:21px}
.ph-ftrack{position:relative;flex:1;min-height:60px;width:36px}
.ph-ftrack::before{content:'';position:absolute;top:8px;bottom:8px;left:50%;width:4px;margin-left:-2px;border-radius:2px;background:var(--bg);border:1px solid var(--bd)}
.ph-ftrack::after{content:'';position:absolute;top:50%;left:3px;right:3px;height:2px;margin-top:-1px;background:var(--c-root);opacity:.75;pointer-events:none}
.ph-root .ph-fader input[type=range]{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;writing-mode:vertical-lr;direction:rtl;-webkit-appearance:none;appearance:none;background:transparent;touch-action:none;cursor:grab}
.ph-fader input[type=range]::-webkit-slider-runnable-track{width:100%;height:100%;background:transparent}
.ph-fader input[type=range]::-moz-range-track{background:transparent}
.ph-fader input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:30px;height:16px;border-radius:3px;background:linear-gradient(180deg,#8d8a82,#e8e4da 45%,#8d8a82);border:1px solid #3a3a3a;box-shadow:0 2px 5px rgba(0,0,0,.5)}
.ph-fader input[type=range]::-moz-range-thumb{width:30px;height:16px;border-radius:3px;background:linear-gradient(180deg,#8d8a82,#e8e4da 45%,#8d8a82);border:1px solid #3a3a3a}
.ph-fpre{display:flex;gap:3px}
.ph-fpre button{width:22px;height:18px;padding:0;border:1px solid var(--bd);border-radius:4px;background:var(--bg);color:var(--tx2);font:700 9px var(--fm);cursor:pointer}
.ph-fpre button:hover{border-color:var(--c-root);color:var(--c-root)}
@media(pointer:coarse){.ph-fader{width:62px}.ph-fpre button{width:26px;height:26px}.ph-ftrack{width:44px}.ph-fader input[type=range]::-webkit-slider-thumb{width:38px;height:22px}}
.ph-instrow{display:flex;gap:6px;align-items:flex-end}.ph-instrow>label{flex:1;min-width:0}
.ph-root .ph-miniplay{flex:none;width:30px;min-height:26px;height:26px;padding:0;margin-bottom:0;font-size:11px}
#page-tablature .tbe-nav .tbe-dockzoom b{min-width:0;font-size:max(10px,.5rem)}
#page-tablature .tbe-nav .tbe-dockzoom{margin-left:auto;gap:2px}
#page-tablature .tbe-nav .tbe-tg,#page-tablature .tbe-nav .tbe-check{letter-spacing:-.2px}
#page-tablature .tbe-nav .tbe-sep{margin:0}
#page-tablature .tbe-nav .tbe-tg{font-size:.62rem}
/* Souris + clavier : les 4 flèches de déplacement sont redondantes (clic sur la partition, touches ←→↑↓) : masquées pour
   gagner la place du manche, de la gamme et du zoom sur la même ligne. Elles restent sur écran tactile. */
@media (hover:hover) and (pointer:fine){#page-tablature .tbe-nav [data-act="left"],#page-tablature .tbe-nav [data-act="right"],#page-tablature .tbe-nav [data-act="up"],#page-tablature .tbe-nav [data-act="down"],#page-tablature .tbe-nav .tbe-sep:first-of-type{display:none}}
/* Nom de section édité sur place */
#page-tablature > .tbe-scroll{position:relative}
#page-tablature .tbe-mkin{position:absolute;z-index:5;box-sizing:border-box;padding:0 6px;border:1px solid var(--c-root);border-radius:4px;background:var(--bg);color:var(--tx);font-family:var(--fb);font-weight:700;outline:none;box-shadow:0 2px 8px var(--sh)}

/* Studio : marge autour du disque pour les boutons flottants posés sur l'anneau (centré sur le rotor) */
.ph-studio .ph-disc{width:calc(100% - 20px);margin:10px auto}
@media(min-width:900px){
  .ph-studio .ph-disc{width:calc(100% - 90px);margin:10px 78px 10px 12px}
  .ph-studio .ph-fader{right:-68px;bottom:7%;height:min(58%,300px)}
}
/* Grand écran : commandes de part et d'autre du disque (colonne gauche : instruments, rotation, écriture, tessiture ; colonne droite : outils, samples, dessins) */
@media(orientation:landscape) and (min-width:1500px){
  .ph-studio .ph-stage{grid-template-columns:minmax(250px,1fr) auto minmax(250px,1fr);align-items:start}
  .ph-studio .ph-spanel{display:contents}
  .ph-studio .ph-pcol{display:flex;flex-direction:column;gap:10px;padding:10px;min-width:0;grid-row:1}
  .ph-studio .ph-pcol{display:block;column-width:250px;column-gap:14px;width:100%;max-width:none}
  .ph-studio .ph-pcol:first-child{grid-column:1;justify-self:end}
  .ph-studio .ph-pcol:last-child{grid-column:3;justify-self:start}
  .ph-studio .ph-pcol>section,.ph-studio .ph-pcol>.ph-row{margin:0 0 12px;break-inside:avoid}
  .ph-studio .ph-disc{grid-column:2;grid-row:1;margin:10px 78px 10px 12px;width:min(calc(100vw - 790px),max(260px,calc(100dvh - 300px)))}
}
/* Sélecteur bascule stylo / gomme : deux positions, pictos stylo à gauche et gomme à droite */
.ph-fab.ph-switch{width:96px;height:44px;border-radius:22px;padding:0;display:block;cursor:pointer}
.ph-swk{position:absolute;left:3px;top:3px;width:38px;height:38px;border-radius:50%;background:var(--c-root);transition:transform .14s ease;transform:translateX(50px)}
.ph-fab.ph-switch.erase .ph-swk{transform:translateX(0)}
.ph-swi{position:absolute;top:0;width:48px;height:44px;display:flex;align-items:center;justify-content:center;color:var(--tx2);pointer-events:none}
.ph-swi-er{left:0}.ph-swi-pen{right:0}
.ph-swi svg{width:24px;height:24px}
.ph-fab.ph-switch:not(.erase) .ph-swi-pen,.ph-fab.ph-switch.erase .ph-swi-er{color:#111}
.ph-fab.ph-switch:focus-visible{outline:2px solid var(--c-root);outline-offset:2px}
@media(pointer:coarse){.ph-fab.ph-switch{width:112px;height:52px;border-radius:26px}.ph-swk{width:46px;height:46px;transform:translateX(58px)}.ph-fab.ph-switch.erase .ph-swk{transform:translateX(0)}.ph-swi{width:56px;height:52px}.ph-swi svg{width:28px;height:28px}}
.ph-smini button[data-layer=clear]:hover{border-color:#d9534f;color:#d9534f}

/* Bureau : les onglets phRotor (Lecture / Calibration & réglages / Diagnostic / Guide) partagent la ligne du titre et des sorties audio/MIDI */
@media(min-width:1200px){
  #page-phrotor .wrap{flex-direction:row;flex-wrap:wrap;align-items:flex-start;align-content:flex-start}
  #page-phrotor .ph-root{display:contents}
  #page-phrotor .phrotor-toolbar{order:1;flex:1 1 0;min-width:0;padding-bottom:8px}
  #page-phrotor .ph-nav{order:2;flex:0 0 auto;margin:0 0 8px 12px;align-self:flex-start}
  #page-phrotor .ph-root>.ph-tab,#page-phrotor .ph-root>.ph-notice{order:3;flex:0 0 100%;min-width:0}
}

/* Studio : pas de filet ni de fond rectangulaire entre le disque et les commandes */
.ph-studio .ph-spanel,.ph-studio .ph-pcol{border:0!important}
.ph-studio #phr-view{background:transparent}
.ph-refrow{display:flex;align-items:center;gap:5px;margin-top:6px}.ph-reflab{flex:1;min-width:0;font-size:11px;color:var(--tx3)}.ph-reflab b{color:var(--c-root)}

/* Mixeur de jeu : appui long sur E, indépendant du tiroir de son. */
#page-play .gss-drawer.gss-balance{left:auto;top:auto;bottom:12px;right:12px;width:280px;max-width:calc(100% - 24px);height:auto;max-height:70%;padding:12px;z-index:46;}
#page-play .gss-balance>.gsz-hbtn{float:right;}
#page-play .gss-balance .gss-snd-p{margin:10px 0;}

/* Modules en plein écran : les menus restent dans la même couche que le module. */
body.ft-module-fullscreen-on{overflow:hidden!important}
.ft-module-fullscreen{position:fixed!important;inset:0!important;background:var(--bg)!important;width:100vw!important;height:100dvh!important;max-width:none!important;max-height:none!important;margin:0!important;box-sizing:border-box;z-index:10000;overflow:auto}
.ph-disc>.ph-fullscreen{position:absolute;right:8px;top:8px;z-index:4;width:36px;height:36px;padding:0;border-radius:8px}
.ph-fab,.ph-fab:active{cursor:pointer}
#page-tablature>.tbe-dock.tbe-dock-hidden{height:0!important;max-height:0!important;min-height:0;padding:0;border:0;overflow:hidden;flex:0 0 0}
#page-tablature .tbe-score.tbe-selecting{touch-action:none}
#page-tablature>.tbe-bar{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px}
#page-tablature>.tbe-bar>.tbe-grp{grid-column:1/-1;display:flex;flex-wrap:wrap;width:100%;min-width:0;overflow-x:visible;gap:5px}
#page-tablature>.tbe-bar>.tbe-grp:first-child{grid-column:1;grid-row:1}
#page-tablature .tbe-fs-controls{grid-column:2;grid-row:1;display:flex;gap:5px}
#page-tablature>.tbe-bar>.tbe-grp:not(:first-child){flex-wrap:wrap;overflow:visible}
#page-tablature>.tbe-bar>.tbe-grp:not(:first-child)>:is(button,select){flex:1 0 auto;min-width:max-content;max-width:100%}
#page-tablature .tbe-row:not(.tbe-nav)>:is(button,input){flex:1 1 auto}
#page-tablature .tbe-ph{will-change:transform;pointer-events:none}

.ph-root.ft-module-fullscreen{padding:8px;overflow:auto}
.ph-root.ft-module-fullscreen .ph-disc{max-width:min(100%,calc(100dvh - 70px))}

#page-tablature .tbe-row{flex-wrap:wrap;overflow:visible}
#page-tablature .tbe-nav>:is(button,input){flex:0 0 auto}
#page-tablature .tbe-nav{flex-wrap:nowrap;overflow:hidden}
#page-tablature .tbe-nav-tools{display:flex;flex:1 1 0;align-items:center;gap:3px;min-width:0;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-webkit-overflow-scrolling:touch}
#page-tablature .tbe-nav-tools::-webkit-scrollbar{display:none}
#page-tablature .tbe-nav-tools>*{flex:none}
#page-tablature .tbe-dock-fixed{display:flex;flex:none;align-items:center;gap:4px;margin-left:auto}
#page-tablature .tbe-dock-fixed .tbe-chordrow{display:flex;flex:0 0 auto;align-items:center;flex-wrap:nowrap;gap:3px;min-width:0;overflow:visible}
#page-tablature .tbe-dock-fixed .tbe-chordrow .tbe-input{flex:0 0 110px;width:110px;min-width:80px;height:28px;padding:0 6px;font-size:.66rem}
#page-tablature .tbe-dock-fixed .tbe-chordrow .tbe-tg{flex:0 0 auto;padding:0 6px}
#page-tablature .tbe-dock-sep{flex:none;width:1px;height:22px;background:var(--bd2);margin:0 3px}
#page-tablature .tbe-nav .tbe-dockzoom{margin-left:0}
#page-tablature .tbe-edit-tools{display:flex;flex:none;align-items:center;gap:3px}
#page-tablature .tbe-dock-mode{display:flex;flex:none;align-items:center;gap:2px;padding:2px;border:1px solid var(--bd);border-radius:8px}
#page-tablature .tbe-dock-mode .tbe-tg{height:26px}
#page-tablature .tbe-mode-label-short{display:none}
#page-tablature .tbe-edit-categories{display:flex;flex:0 1 auto;flex-wrap:wrap;align-content:flex-start;align-items:flex-start;column-gap:12px;row-gap:4px;min-width:0}
#page-tablature .tbe-edit-categories>.tbe-row{flex:0 0 auto;flex-wrap:nowrap;align-items:center;width:max-content;max-width:100%;min-width:0;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-webkit-overflow-scrolling:touch}
#page-tablature .tbe-edit-categories>.tbe-row::-webkit-scrollbar{display:none}
#page-tablature .tbe-edit-categories>.tbe-row>*{flex:0 0 auto}
#page-tablature .tbe-edit-categories>.tbe-row.tbe-cat-separator{position:relative}
#page-tablature .tbe-edit-categories>.tbe-row.tbe-cat-separator::after{content:"";position:absolute;top:5px;right:0;bottom:5px;width:1px;background:var(--bd2);pointer-events:none;z-index:1}
#page-tablature .tbe-pad-fx-sep{flex:none;width:1px;height:20px;background:var(--bd2);margin:0 4px}
#page-tablature.tbe-edit-hidden .tbe-edit-tools,
#page-tablature.tbe-edit-hidden .tbe-edit-categories{display:none}
#page-tablature.tbe-edit-hidden .tbe-dock-fixed .tbe-chordrow{visibility:hidden;pointer-events:none}
#page-tablature .tbe-row:not(.tbe-nav){flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-webkit-overflow-scrolling:touch}
#page-tablature .tbe-row:not(.tbe-nav)::-webkit-scrollbar{display:none}
#page-tablature .tbe-row:not(.tbe-nav)>:is(button,input){flex:0 0 auto;width:auto}
#page-tablature .tbe-pad .tbe-fb{width:34px;min-width:34px}
#page-tablature .tbe-chordrow .tbe-input{flex:0 0 220px;width:220px}
/* Barre Tabs fluide : les groupes avancés remplissent la première ligne, puis se replient au besoin. */
#page-tablature>.tbe-bar{position:relative;display:flex;flex-direction:row;align-items:flex-start;gap:0;overflow:hidden}
#page-tablature .tbe-toolbar-row{display:flex;flex:0 1 auto;align-items:center;align-content:flex-start;flex-wrap:wrap;gap:5px;width:100%;min-width:0;box-sizing:border-box;padding-right:78px;overflow:visible}
#page-tablature .tbe-toolbar-row>.tbe-grp{display:flex;flex:0 1 auto;flex-wrap:wrap;align-items:center;gap:5px;width:auto;min-width:0;max-width:100%;overflow:visible}
#page-tablature .tbe-toolbar-row>.tbe-grp>*{flex:0 0 auto;width:auto;min-width:0;max-width:100%}
#page-tablature .tbe-toolbar-primary>.tbe-tempo-advanced,
#page-tablature .tbe-toolbar-primary>.tbe-toolbar-actions{display:contents}
#page-tablature .tbe-toolbar-primary>.tbe-tempo-advanced>*,
#page-tablature .tbe-toolbar-primary>.tbe-toolbar-actions>*{flex:0 0 auto;max-width:100%}
#page-tablature .tbe-toolbar-row>.tbe-grp .tbe-btn,
#page-tablature .tbe-toolbar-row>.tbe-grp .tbe-mini{width:32px;min-width:32px}
#page-tablature .tbe-toolbar-row>.tbe-grp .tbe-tg,
#page-tablature .tbe-toolbar-row>.tbe-grp .tbe-sel{flex:none}
#page-tablature .tbe-toolbar-primary{gap:5px}
#page-tablature .tbe-toolbar-primary .tbe-tempo b{min-width:28px}
#page-tablature .tbe-toolbar-actions{margin-left:0}
#page-tablature #tbeRoSeg{flex:0 0 auto;height:28px}
#page-tablature #tbeMinBtn{position:absolute!important;top:6px;right:40px;z-index:2;flex:0 0 32px;width:32px;min-width:32px} /* !important : la règle tactile (position:relative pour la zone de clic) le remettait dans le flux, donc hors de sa place */
#page-tablature #tbeFsBtn{position:absolute!important;top:6px;right:8px;z-index:2;width:28px;height:28px;padding:0;font-size:.8rem}
#page-tablature.tbe-min .tbe-toolbar-primary>:not(.tbe-toolbar-transport):not(.tbe-tempo):not(#tbeMinBtn):not(#tbeFsBtn){display:none}
@media(max-width:700px){
  #page-tablature .tbe-nav{overflow-x:auto;overflow-y:hidden}
  #page-tablature .tbe-nav-tools{flex:0 0 max-content}
  #page-tablature .tbe-dock-fixed{margin-left:0}
  #page-tablature .tbe-dock-fixed .tbe-chordrow{flex:0 0 225px}
  #page-tablature .tbe-dock-fixed .tbe-chordrow .tbe-input{flex-basis:100px;min-width:75px}
  #page-tablature>.tbe-bar{gap:4px}
  #page-tablature .tbe-toolbar-row{gap:4px;padding-right:76px}
  #page-tablature #tbeMinBtn{top:4px;right:38px}
  #page-tablature #tbeFsBtn{top:4px;right:6px}
  .tbe-keys-grid{grid-template-columns:1fr;gap:0}
}
@media(max-width:400px){
  #page-tablature .tbe-dock-fixed{gap:2px}
  #page-tablature .tbe-dock-mode{gap:0;padding:1px}
  #page-tablature .tbe-dock-mode .tbe-tg{height:24px;min-width:0;padding:0 2px;font-size:.52rem}
  #page-tablature .tbe-mode-label-long{display:none}
  #page-tablature .tbe-mode-label-short{display:inline}
  #page-tablature .tbe-nav .tbe-dockzoom{gap:1px}
  #page-tablature .tbe-dockzoom button{min-width:24px;padding:0}
  #page-tablature .tbe-nav .tbe-dockzoom b{min-width:24px;font-size:9px}
}

/* Tabs : boutons et menus au standard des autres modules (.tbtn.gs-mode-btn : 28 px, .58rem, actif = fond tonique). */
#page-tablature :is(.tbe-btn,.tbe-mini,.tbe-tg,.tbe-sel,.dpk-select-btn){height:28px;border-radius:4px;border:1px solid var(--bd);background:var(--s2);color:var(--tx);font-size:max(10px,.58rem);transition:all var(--tr)}
#page-tablature :is(.tbe-btn,.tbe-mini,.tbe-tg,.dpk-select-btn):hover{background:var(--s3);border-color:var(--bd2)}
#page-tablature .tbe-tg,#page-tablature .dpk-select-btn{padding:0 9px}
#page-tablature .tbe-btn,#page-tablature .tbe-mini{width:30px}
#page-tablature :is(.tbe-btn,.tbe-tg).on,#page-tablature .tbe-play.on{background:var(--c-root);color:#111;border-color:var(--c-root)}
#page-tablature .tbe-bar .dpk-select-btn{width:auto;min-width:0;white-space:nowrap}

/* Tabs mobile : l'éditeur ne se réduit jamais à zéro (la partition cède la place) et ses boutons passent à la ligne
   au lieu de défiler horizontalement. */
@media(max-width:700px){
  #page-tablature > .tbe-bar{flex:none}
  #page-tablature > .tbe-scroll{flex:1 1 90px;min-height:60px}
  #page-tablature > .tbe-dock{flex:0 1 auto;min-height:min(120px,30vh);max-height:none}
  #page-tablature > .tbe-dock.tbe-dock-hidden{min-height:0}
  #page-tablature .tbe-nav,#page-tablature .tbe-nav-tools{flex-wrap:wrap;overflow:visible;flex:1 1 100%;max-width:100%}
  #page-tablature .tbe-dock-fixed{flex-wrap:wrap;margin-left:0}
  #page-tablature .tbe-dock-fixed .tbe-chordrow{flex:1 1 100%;flex-wrap:wrap}
  #page-tablature .tbe-dock-fixed .tbe-chordrow .tbe-input{flex:0 0 76px;width:76px;min-width:0}
  #page-tablature .tbe-edit-tools{flex:1 1 100%;flex-wrap:wrap;row-gap:4px}
  #page-tablature .tbe-edit-tools .tbe-sep{display:none}
  #page-tablature .tbe-edit-categories{flex:1 1 100%}
  #page-tablature .tbe-edit-categories>.tbe-row,#page-tablature .tbe-row:not(.tbe-nav){flex-wrap:wrap;width:auto;max-width:100%;overflow:visible}
  #page-tablature .tbe-edit-categories>.tbe-row.tbe-cat-separator::after{display:none}
}

/* Tabs : menu flottant (Fichier, clic droit), mesure incomplète, suivi de lecture. */
.tbe-pop{position:fixed;z-index:10100;min-width:210px;width:max-content;max-width:min(400px,calc(100vw - 8px));overflow-x:hidden;max-height:calc(100dvh - 8px);overflow-y:auto;padding:4px;background:var(--sf);border:1px solid var(--bd2);border-radius:8px;box-shadow:0 10px 30px var(--sh);display:flex;flex-direction:column}
.tbe-pop button{display:flex;align-items:center;justify-content:space-between;gap:16px;width:100%;height:30px;padding:0 10px;border:0;border-radius:5px;background:transparent;color:var(--tx);font-family:var(--fm);font-size:max(10px,.62rem);text-align:left;cursor:pointer;white-space:nowrap}
.tbe-pop button:hover:not(:disabled){background:var(--s3)}
.tbe-pop button:disabled{opacity:.35;cursor:default}
.tbe-pop small{color:var(--tx3);font-size:.9em;flex:none;white-space:nowrap}
.tbe-pop.tbe-pop-wide{width:max-content;max-width:min(360px,calc(100vw - 16px))}
.tbe-pop.tbe-pop-wide button{height:auto;min-height:30px;padding:6px 10px;white-space:normal;text-align:left;line-height:1.25}
.tbe-pop.tbe-pop-wide button small{flex:none;white-space:nowrap}
.tbe-pop-sep{height:1px;margin:4px 6px;background:var(--bd)}
.tbe-svg .tbe-badm{pointer-events:none;stroke-width:1;stroke-dasharray:4 3;fill:color-mix(in srgb,var(--c-danger,#e0533d) 8%,transparent);stroke:color-mix(in srgb,var(--c-danger,#e0533d) 60%,transparent)}
.tbe-svg .tbe-badm.tbe-over{fill:color-mix(in srgb,var(--c-danger,#e0533d) 14%,transparent)}

/* Page MIDI de Surface : sections titrées (Appareils MIDI / Sortie / Entrée / Potentiomètres et pédales / Latence) */
.gs-midi-sec{padding:6px 0;border-top:1px solid var(--bd2)}
.gs-midi-sec:first-child{border-top:0;padding-top:0}
.gs-midi-sechead{font-family:var(--fm);font-size:max(10px,.52rem);color:var(--tx3);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px}
.gs-midi-devtab{width:100%;border-collapse:collapse;table-layout:fixed;font-family:var(--fm);font-size:max(10px,.6rem)}
.gs-midi-devtab th{text-align:left;color:var(--tx3);font-weight:400;text-transform:uppercase;letter-spacing:.06em;padding:0 6px 3px 0;border-bottom:1px solid var(--bd2)}
.gs-midi-devtab td{vertical-align:top;padding:1px 8px 1px 0;width:50%}
.gs-midi-devtab td+td,.gs-midi-devtab th+th{padding-left:8px;border-left:1px solid var(--bd2)}
.gs-midi-devpick{cursor:pointer}
.gs-midi-devpick:hover b{text-decoration:underline}
.gs-midi-latdet{margin-top:6px;font-family:var(--fm);font-size:max(10px,.6rem);color:var(--tx2)}
.gs-midi-latdet summary{cursor:pointer;color:var(--tx3);margin-bottom:4px}
/* Toucher → sortie MIDI USB (injecté par native-touch.js de l'appli Android, sans style propre : il héritait de la
   taille de base du dialogue). Même échelle que la section Latence. */
#ftNativeTouchStatus,#ftNativeTouchMode~details{font-family:var(--fm);font-size:max(10px,.6rem);color:var(--tx2)}
#ftNativeTouchMode~details summary{cursor:pointer;color:var(--tx3);font-size:inherit;margin-bottom:4px}
#ftNativeTouchMode~details p,#ftNativeTouchMode~details pre{font-size:inherit;margin:4px 0}

/* MIDI learn depuis l'écran (bouton « MIDI » du bandeau d'effets) */
#page-play .gsc-midi.on{background:#42a5f5;color:#111;border-color:#42a5f5}
#page-play.gs-midilearn #gsStage [data-lk]{outline:1px dashed #42a5f5;outline-offset:1px;cursor:crosshair;position:relative}
#page-play.gs-midilearn #gsStage [data-lk] input{pointer-events:none}
#page-play.gs-midilearn #gsStage [data-lk].lk-set{outline-style:solid}
#page-play.gs-midilearn #gsStage [data-lk].lk-set::after{content:attr(data-cc);position:absolute;right:2px;top:-7px;font:600 max(9px,.45rem) var(--fm);background:#42a5f5;color:#111;border-radius:3px;padding:0 3px;pointer-events:none}
#page-play.gs-midilearn #gsStage [data-lk].lk-armed{outline:2px solid #90caf9;animation:gsLearnPulse 1s ease-in-out infinite}
@keyframes gsLearnPulse{50%{outline-color:transparent}}
/* Page MIDI : appareils compacts — nom + point d'état sur une ligne, dernier message dessous (seulement s'il existe) */
.gs-midi-devtab .gs-midi-devrow{gap:2px 6px}
.gs-midi-devtab .gs-midi-act{margin-left:0;flex-basis:100%;padding:0 0 0 14px;color:var(--tx3);min-height:0}
.gs-midi-devtab .gs-midi-act:empty{display:none}
.gs-midi-devtab b{overflow-wrap:anywhere}
.gs-settings-midi{display:block}

/* Étiquette de version cliquable (pied des Paramètres) et fenêtre « Versions » */
.settings-footer button.settings-version-badge{background:none;border:0;min-width:0;width:auto;padding:0 4px;cursor:pointer;color:var(--tx3);text-decoration:underline dotted;text-underline-offset:3px}
.settings-footer button.settings-version-badge:hover{color:var(--tx)}
.ft-ver-modal{position:fixed;inset:0;z-index:100000;background:rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center;padding:16px}
.ft-ver-box{background:var(--s1,#1e1e1e);color:var(--tx,#eee);border:1px solid var(--bd2,#444);border-radius:12px;padding:14px 16px;width:min(460px,100%);font-family:var(--fm)}
.ft-ver-title{display:flex;justify-content:space-between;align-items:center;font-weight:700;font-size:.8rem;margin-bottom:10px}
.ft-ver-x{background:none;border:0;color:var(--tx3);cursor:pointer;font-size:.9rem}
.ft-ver-grid{display:grid;grid-template-columns:auto 1fr 1fr;gap:8px 14px;font-size:max(11px,.62rem);align-items:baseline}
.ft-ver-h{color:var(--tx3);text-transform:uppercase;letter-spacing:.06em;font-size:max(10px,.52rem)}
.ft-ver-l{color:var(--tx2)}
.ft-ver-dim{color:var(--tx3)}
.ft-ver-note{margin-top:10px;font-size:max(10px,.56rem);color:var(--tx2);white-space:pre-wrap}
.ft-ver-btns{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}
.ft-ver-btn{flex:1 1 140px;background:var(--s2,#2a2a2a);color:var(--tx,#eee);border:1px solid var(--bd2,#444);border-radius:8px;padding:8px 10px;font-family:var(--fm);font-size:max(10px,.6rem);cursor:pointer}
.ft-ver-btn.primary{background:var(--c-root);color:#111;border-color:var(--c-root);font-weight:700}
.ft-ver-btn:disabled{opacity:.45;cursor:default}

/* Moniteur MIDI (Paramètres > MIDI) */
#midiMonitor{font-family:var(--fm);font-size:max(10px,.58rem);color:var(--tx2)}
#midiMonitor .mm-bar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:6px}
#midiMonitor .mm-btn{background:var(--s2);color:var(--tx);border:1px solid var(--bd2);border-radius:6px;padding:3px 9px;font:inherit;cursor:pointer}
#midiMonitor .mm-btn.on{background:var(--c-root);color:#111;border-color:var(--c-root)}
#midiMonitor .mm-chk{color:var(--tx3);display:flex;gap:4px;align-items:center}
#midiMonitor .mm-devs{display:flex;flex-direction:column;gap:2px;margin-bottom:6px}
#midiMonitor .mm-dev{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
#midiMonitor .mm-dev b{color:var(--tx);font-weight:600}
#midiMonitor .mm-dot{width:8px;height:8px;border-radius:50%;background:var(--bd2);flex:0 0 auto}
#midiMonitor .mm-dot.on{background:#34c759}
#midiMonitor .mm-dim{color:var(--tx3)}
#midiMonitor .mm-log{max-height:260px;overflow:auto;border:1px solid var(--bd);border-radius:8px;padding:4px 6px;background:var(--s1)}
#midiMonitor .mm-row{display:grid;grid-template-columns:auto minmax(0,1.1fr) minmax(0,2fr) auto;gap:2px 10px;padding:1px 0;border-bottom:1px solid transparent;white-space:nowrap}
#midiMonitor .mm-row>span{overflow:hidden;text-overflow:ellipsis}
#midiMonitor .mm-t,#midiMonitor .mm-c,#midiMonitor .mm-p{color:var(--tx3)}
#midiMonitor .mm-x b{color:var(--c-root)}
#midiMonitor .mm-x i{color:var(--tx3);font-style:normal}
#midiMonitor .mm-cc .mm-x{color:#42a5f5}
#midiMonitor .mm-note .mm-x{color:var(--tx)}
#midiMonitor .mm-pc .mm-x{color:#ffb74d}
@media(max-width:600px){#midiMonitor .mm-row{grid-template-columns:auto minmax(0,1fr)}#midiMonitor .mm-p,#midiMonitor .mm-c{display:none}}
.ft-ver-tools{display:flex;gap:6px;align-items:center}
.ft-ver-x:disabled{opacity:.4;cursor:default}
/* ↻ et ✕ de la fenêtre « Versions » : même boîte, même corps de caractère */
.ft-ver-x{width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;font-size:1rem;line-height:1;border-radius:6px;font-family:inherit}
.ft-ver-x:hover{background:var(--s2,#2a2a2a);color:var(--tx,#eee)}

/* Page MIDI de Surface : associations (CC / notes) — boutons assez grands pour le toucher */
#gsMidiPanel .gs-preset{border-radius:8px}
#gsMidiPanel .gs-preset-load,#gsMidiPanel .gs-preset-del{min-height:40px;padding:0 12px;font-size:max(12px,.7rem)}
#gsMidiPanel .gs-preset-del{min-width:44px}
#gsMidiPanel .gs-midi-bar{gap:8px}
#settingsModal #gsSettingsPanel #gsMidiPanel .gs-preset-load,#settingsModal #gsSettingsPanel #gsMidiPanel .gs-preset-del{min-height:40px;padding:0 12px;font-size:max(12px,.7rem)}
#settingsModal #gsSettingsPanel #gsMidiPanel .gs-preset-del{min-width:44px}

.fav-card .fav-icon.fav-card-menu{position:absolute;top:8px;right:8px;z-index:2;min-height:22px;height:22px;min-width:26px;padding:0 6px;font-size:.85rem;line-height:1;background:transparent;border-color:transparent;color:var(--tx2)}
.fav-card .fav-icon.fav-card-menu:hover,.fav-card .fav-icon.fav-card-menu:focus-visible{background:var(--s3);border-color:var(--bd2);color:var(--tx)}

.fav-card[data-fav-color]{box-shadow:inset 4px 0 0 var(--fav-tag)}
.fav-card-ghost[data-fav-color]{box-shadow:inset 4px 0 0 var(--fav-tag),0 14px 36px #000b}
.fav-tag{color:var(--tx2)}
.fav-tag-colors{display:flex;gap:8px;flex-wrap:wrap;margin:6px 0 12px}
.fav-tag-dot{width:28px;height:28px;min-height:28px;padding:0;border-radius:50%;border:2px solid transparent;cursor:pointer;font:inherit;line-height:1}
.fav-tag-dot.none{background:var(--s2);color:var(--tx2);border-color:var(--bd)}
.fav-tag-dot[aria-pressed="true"]{border-color:var(--tx);box-shadow:inset 0 0 0 2px var(--sf)}
.fav-tag-list,.fav-tag-suggest{margin:6px 0}
.fav-tag-x{border:none;background:transparent;color:inherit;cursor:pointer;padding:0 0 0 4px;font:inherit}
.fav-tag-suggestion{cursor:pointer;border:1px solid var(--bd);font:inherit;color:var(--tx2)}

.fav-color-dot{position:absolute;top:11px;right:40px;z-index:2;width:14px;height:14px;min-height:0;padding:0;border-radius:50%;border:1.5px solid var(--tx3);background:transparent;cursor:pointer}
.fav-color-dot::before{content:"";position:absolute;inset:-7px}
.fav-color-dot:hover,.fav-color-dot:focus-visible{border-color:var(--tx)}
.fav-color-pop{position:fixed;z-index:10001;display:flex;gap:6px;padding:8px;background:var(--sf);border:1px solid var(--bd2);border-radius:var(--rl);box-shadow:0 10px 30px #0008}
.fav-color-pop .fav-tag-dot{width:24px;height:24px;min-height:24px}

/* Surface en plein écran : jamais de toast (demande explicite) — il recouvrait la surface de jeu. */
body.gs-fullscreen .hint{display:none !important}

/* (doublon .ft-file-btn retiré : couvert par la définition unique) */

#grooveToolbarWrap #grooveFileBtn{position:relative;flex:none;margin-left:0}  /* relative (pas static) : sinon son ::after tactile (inset:-4px) s'ancre sur un ancêtre et couvre la barre */
#strOptBtn.ft-file-btn{margin-left:auto!important}

/* Taille du mini-canvas : commande sur la même ligne, toujours à droite du contenu défilant. */
.mini-canvas-row{position:relative;display:flex;align-items:center;min-width:0}
.mini-canvas-row>.mini-canvas-frame{flex:1 1 0;min-width:0}
.mini-canvas-row>.mini-zoom-controls,.cs-row>.mini-zoom-controls{position:absolute;right:8px;top:50%;transform:translateY(-50%);z-index:2;margin:0;padding:0;text-shadow:0 1px 2px #000}
.cs-row{position:relative}
.cs-row>.cs-nav:last-of-type{margin-right:110px}

/* Le cadre et le fond du canvas s’arrêtent avant les commandes de zoom. */
.mini-zoom-controls{background:transparent}
#csFollowBar{background:transparent}

/* Le choix de structure appartient au menu Fichier, sans la contrainte de largeur de la barre mobile. */
#strOptMenu>#strTemplateMenuBtn{max-width:none;box-sizing:border-box}

/* Progressions : boutons MIDI / Tab / PNG visibles seulement sur une carte dépliée (cf. progRowSetExpanded) */
.prog-exp-hidden{display:none !important}

/* Accords : aperçu au survol (cf. chPreviewType / chStackHover, app.js) */
.ch-stack-cran.ch-pv-on{outline:2px dashed #3ddc84;outline-offset:1px;opacity:1 !important}
.ch-stack-cran.ch-pv-off{opacity:.3 !important;text-decoration:line-through}
.ch-stack-cran.ch-pv-sel{outline:2px dashed var(--c-root);outline-offset:1px}

/* Tabs : sur mobile portrait, les boutons de la barre se partagent la largeur de chaque ligne (moins de lignes) */
@media (max-width:700px){
  #page-tablature .tbe-toolbar-primary .tbe-tg,#page-tablature .tbe-toolbar-primary .tbe-sel,#page-tablature .tbe-toolbar-primary .dpk-select-btn{flex:1 1 auto !important}
}
#page-tablature .tbe-toolbar-primary>.tbe-playopts{display:contents}
#page-tablature .tbe-toolbar-primary>.tbe-playopts>*{flex:0 0 auto;max-width:100%}
/* Tabs : contrôles de réglage reportés dans le menu Fichier (cf. tbeFileMenu) : jamais affichés, mais présents dans le DOM */
#page-tablature #tbeMenuCtl{display:none !important}
/* Tabs mobile : annuler / rétablir sont dans le menu OPT. */
@media(max-width:1023px){#page-tablature #tbeUndoBtn,#page-tablature #tbeRedoBtn{display:none}}
/* Tabs desktop : replier la barre n'a d'intérêt que sur mobile. */
@media(min-width:1024px){#page-tablature #tbeMinBtn{display:none}}
/* Tabs desktop : bouton OPT collé à droite, à côté du plein écran, avec un espace de séparation. */
@media(min-width:1024px){
  #page-tablature .tbe-toolbar-row{padding-right:140px}
  #page-tablature #tbeFileBtn{position:absolute!important;top:6px;right:48px;z-index:2;height:28px}  /* !important : la règle tactile (pointer:coarse) le remettait en relative, donc en surimpression */
}
/* Bouton OPT : une seule apparence pour tous les modules (Tabs compris), hauteur, fond, bord, police et graisse. */
.ft-file-btn,#page-tablature #tbeFileBtn{box-sizing:border-box;height:28px;min-width:74px;padding:0 8px;border:1px solid var(--bd);border-radius:4px;background:var(--sf);color:var(--tx);font-family:var(--fm);font-size:max(10px,.6rem);font-weight:400}
@media(hover:hover){.ft-file-btn:hover,#page-tablature #tbeFileBtn:hover{background:var(--s3);border-color:var(--bd2)}}
#strOptBtn.ft-file-btn{position:sticky;right:0;z-index:2}
/* OPT : même retrait à droite (4 px) dans Backing et Rythme que dans les autres modules. */

#page-rhythm #rhSubbar{margin-right:0;padding-right:12px}  /* OPT décalé du bord : l'ancienne marge négative le poussait contre (et sous) le bord du cadre */
/* Barres du haut : même hauteur partout (36 px de contenu + 1 px de filet = 37 px). */
#intervallesHeader,#azHeader{border-bottom:0!important}
.az-mode-badge{display:inline-flex;align-items:center;height:26px;padding:0 9px}
#triadsInfoRow{height:28px}
@media(min-width:1024px){
  #page-melodie .sq-bar{padding:4px 8px}
  #page-melodie .sq-bar .sq-btn,#page-melodie .sq-bar .sq-mini-btn{height:28px}
}
/* Boutons communs : « i » catalogue = « i » d'info (16 px) ; plein écran de Mélodie comme celui de Tabs et Groove. */
.info-btn-catalog{width:16px;height:16px}
@media(min-width:1024px){#page-melodie .sq-bar #seqBtnFs{width:28px;border-radius:4px}}
/* Boutons de transport communs (Séquenceur) : mêmes coins arrondis (4 px) que le reste de l'app. */
@media(min-width:1024px){
  #page-melodie .sq-bar .sq-btn,#page-melodie .sq-bar .sq-mini-btn{border-radius:4px}
}
/* Groovebox : les styles en ligne de groovebox.js imposaient des coins à 5 px ; on aligne sur le reste de l'app. */
:is(#page-groove,.gsz-gb) button:not(.groove-mode-seg button){border-radius:4px!important}

/* ═══ Tabs : ergonomie tactile, accessibilité, feuilles mobiles, poignées de sélection (2026-10-20) ═══ */
.tbe-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
#page-tablature :is(button,select,input,[tabindex]):focus-visible{outline:2px solid var(--c-root);outline-offset:2px}
#page-tablature #tbeScroll:focus-visible{outline-offset:-3px}
.tbe-pop button:focus-visible{outline:2px solid var(--c-root);outline-offset:-2px;background:var(--s3)}
.tbe-dlg:focus{outline:none}
#page-tablature .tbe-score{position:relative}
/* Déplacer des mesures à la souris : trait de dépose */
.tbe-drop{position:fixed;z-index:10050;width:3px;border-radius:2px;background:var(--c-root);box-shadow:0 0 0 1px #0006;pointer-events:none}
#page-tablature .tbe-score.tbe-moving,#page-tablature .tbe-score.tbe-moving *{cursor:grabbing}
/* Poignées de sélection (tactile) */
.tbe-hnd{position:absolute;z-index:3;width:44px;height:44px;margin:-22px 0 0 -22px;touch-action:none;cursor:grab}
.tbe-hnd::after{content:"";position:absolute;left:50%;top:50%;width:16px;height:16px;margin:-8px 0 0 -8px;border-radius:50%;background:var(--c-root);border:2px solid #fff;box-shadow:0 1px 4px #0009}
/* Première ouverture : mémo en trois gestes */
#page-tablature>.tbe-welcome{flex:none;display:flex;align-items:center;gap:10px;margin:2px 8px 4px;padding:6px 10px;border:1px solid color-mix(in srgb,var(--c-root) 45%,var(--bd));border-radius:8px;background:color-mix(in srgb,var(--c-root) 8%,var(--sf));font-size:.72rem;line-height:1.35}
#page-tablature>.tbe-welcome[hidden]{display:none}
#page-tablature>.tbe-welcome ol{margin:0;padding-left:1.2em;flex:1}
#page-tablature>.tbe-welcome .tbe-tg{flex:none}
/* Bibliothèque vide */
.tbe-empty{display:flex;flex-direction:column;align-items:flex-start;gap:8px;margin:6px 0;padding:14px;border:1px dashed var(--bd2);border-radius:var(--rl)}
.tbe-empty span{color:var(--tx3);font-size:.78rem}
/* Enregistrement : l'état non enregistré reste lisible sur téléphone (le libellé n'est masqué que quand tout est enregistré) */
#page-tablature .tbe-save-state[data-state="modified"],#page-tablature .tbe-save-state[data-state="draft"]{color:var(--c-fifth,#e8a33d);border-color:color-mix(in srgb,currentColor 45%,var(--bd))}
@media(max-width:700px){#page-tablature .tbe-save-state:not([data-state="saved"])>span{display:inline}}

/* Cibles tactiles : les boutons gardent leur taille (la place est comptée sur mobile) mais leur zone active déborde de 3 px
   (moitié de l'espacement), soit ~38 px ; le pavé de cases et les durées, gestes les plus fréquents, sont réellement agrandis. */
@media (pointer:coarse){
  #page-tablature :is(.tbe-btn,.tbe-mini,.tbe-tg,.dpk-select-btn,.tbe-fxb,#tbeFileBtn,.tbe-roseg .pill,#tbeFsBtn,.tbe-zoom button,.tbe-title){position:relative}
  #page-tablature :is(.tbe-btn,.tbe-mini,.tbe-tg,.dpk-select-btn,.tbe-fxb,#tbeFileBtn,.tbe-roseg .pill,#tbeFsBtn,.tbe-zoom button,.tbe-title)::after{content:"";position:absolute;inset:-3px}
  #page-tablature :is(.tbe-fb,.tbe-db){min-height:36px;height:36px!important;min-width:36px}
  #page-tablature .tbe-pad .tbe-fb{width:auto!important;min-width:36px;flex:1 1 36px}
  .tbe-pop button,.tbe-pop.tbe-pop-wide button{min-height:44px}
  .tbe-dlg .tbe-tg,.tbe-dlg .tbe-pbtn,.tbe-seg button{min-height:44px}
  .tbe-x{min-width:44px;min-height:44px}
}

/* Téléphone en paysage : partition à gauche, zone de saisie à droite (la hauteur ne laisse plus de place à l'empilement) */
@media (orientation:landscape) and (max-height:520px) and (pointer:coarse){
  #page-tablature.active{display:grid;grid-template-columns:minmax(0,1fr) minmax(300px,44%);grid-template-rows:auto auto auto minmax(0,1fr)}
  #page-tablature>.tbe-bar{grid-area:1/1/2/3}
  #page-tablature>.tbe-head{grid-area:2/1}
  #page-tablature>.tbe-chords{grid-area:3/1}
  #page-tablature>.tbe-scroll{grid-area:4/1;min-height:0}
  #page-tablature>.tbe-split{display:none}
  #page-tablature>.tbe-dock,#page-tablature>.tbe-dock.tbe-dock-hidden{grid-area:2/2/5/3;height:auto!important;max-height:none!important;min-height:0;padding:6px 8px;overflow-y:auto;border-top:0;border-left:1px solid var(--bd)}
  #page-tablature :is(.tbe-fb,.tbe-db){min-height:32px;height:32px!important}
  #page-tablature>.tbe-welcome{display:none}
}

/* Tabs mobile portrait : zone de saisie compacte (boutons plus bas, un seul flux de boutons, moins d'espaces).
   Placé après les règles tactiles (36 px) ; le paysage téléphone garde sa propre mise en page. */
@media (max-width:700px) and (orientation:portrait){
  #page-tablature > .tbe-dock{gap:3px;padding:4px 6px calc(4px + env(safe-area-inset-bottom))}
  #page-tablature .tbe-dock :is(.tbe-btn,.tbe-tg,.tbe-fxb){height:28px;min-width:28px}
  #page-tablature .tbe-dock .tbe-tg{padding:0 6px}
  #page-tablature .tbe-dock :is(.tbe-fb,.tbe-db){height:30px!important;min-height:30px;min-width:26px;padding:0 3px}
  #page-tablature .tbe-pad .tbe-fb{min-width:26px;flex:1 1 26px}
  #page-tablature .tbe-dock :is(.tbe-nav,.tbe-nav-tools,.tbe-edit-categories,.tbe-row,.tbe-dock-fixed){gap:3px}
  #page-tablature .tbe-edit-categories{row-gap:3px}
  /* Enfants de la colonne .tbe-dock : hauteur = contenu (le « flex:1 1 100% » des règles précédentes créait de grands vides). */
  #page-tablature .tbe-dock>.tbe-nav,#page-tablature .tbe-dock>.tbe-edit-categories,#page-tablature .tbe-dock>.tbe-neckwrap{flex:0 0 auto}
  #page-tablature .tbe-dock .tbe-nav>.tbe-nav-tools{flex:1 1 100%}
  #page-tablature .tbe-dock .tbe-nav>.tbe-dock-fixed{flex:1 1 100%;min-width:0;flex-wrap:wrap}
  #page-tablature .tbe-dock-mode .tbe-mode-label-long{display:none}
  #page-tablature .tbe-dock-mode .tbe-mode-label-short{display:inline}
  #page-tablature .tbe-edit-tools{display:contents}
  #page-tablature .tbe-dock-fixed .tbe-chordrow{flex:0 1 auto}
  #page-tablature .tbe-dock-sep,#page-tablature .tbe-pad-fx-sep{display:none}
  /* Principe : aucune rangée « en dentelle ». Les boutons de texte s'étirent pour remplir leur ligne ; les séries de
     boutons identiques (durées, cases, techniques) sont en grille à colonnes égales, donc jamais tronquées. */
  #page-tablature .tbe-nav-tools>.tbe-tg,#page-tablature .tbe-edit-tools .tbe-tg{flex:1 1 auto}
  #page-tablature .tbe-dock-fixed .tbe-chordrow{flex:1 1 100%}
  #page-tablature .tbe-dock-fixed .tbe-chordrow .tbe-tg{flex:1 1 auto}
  #page-tablature .tbe-dock-fixed .tbe-dock-mode{flex:1 1 0;min-width:0}
  #page-tablature .tbe-dock-fixed .tbe-dockzoom[style*="hidden"]{display:none}
  #page-tablature .tbe-dock-fixed .tbe-dock-mode .tbe-tg{flex:1 1 0;font-size:.58rem}
  #page-tablature .tbe-dock-fixed .tbe-dockzoom{flex:none}
  #page-tablature .tbe-dock-fixed .tbe-dockzoom button{min-width:30px}
  /* Mode « Manche » seul : la ligne d'accord masquée ne laisse plus de vide. */
  #page-tablature.tbe-edit-hidden .tbe-dock-fixed .tbe-chordrow{display:none}
  /* Mesure complète : rien à dire (le message ne sert que s'il manque ou dépasse des temps). */
  #page-tablature .tbe-fill.tbe-ok{display:none}
  #page-tablature .tbe-edit-categories>.tbe-row.tbe-pad,#page-tablature .tbe-edit-categories>.tbe-row.tbe-fxrow,#page-tablature .tbe-edit-categories>.tbe-row.tbe-durs{display:grid;width:100%;overflow:visible;grid-template-columns:repeat(7,minmax(0,1fr))}
  #page-tablature .tbe-edit-categories>.tbe-row.tbe-pad>*,#page-tablature .tbe-edit-categories>.tbe-row.tbe-fxrow>*,#page-tablature .tbe-edit-categories>.tbe-row.tbe-durs>*{width:auto!important;min-width:0!important;padding-left:0;padding-right:0;flex:none}
  #page-tablature .tbe-durs>.tbe-tg[data-act="dot"],#page-tablature .tbe-durs>.tbe-tg[data-act="tup"]{grid-column:span 2}
  #page-tablature .tbe-durs>.tbe-tg[data-act="voice2"]{grid-column:span 3}
  #page-tablature .tbe-durs>.tbe-fill:not(.tbe-ok){grid-column:1/-1;margin-left:0}
}
/* Écran un peu plus large (grand téléphone, petite tablette) : une seule rangée de cases / techniques. */
@media (min-width:480px) and (max-width:700px) and (orientation:portrait){
  #page-tablature .tbe-edit-categories>.tbe-row.tbe-pad{grid-template-columns:repeat(14,minmax(0,1fr))}
  #page-tablature .tbe-edit-categories>.tbe-row.tbe-fxrow{grid-template-columns:repeat(13,minmax(0,1fr))}
}

/* Fenêtres : feuille qui monte du bas sur mobile */
@media (max-width:700px){
  .tbe-modal{align-items:flex-end;padding:0}
  .tbe-dlg,.tbe-dlg.wide{width:100%;max-width:none;border-radius:16px 16px 0 0;max-height:92dvh;padding-bottom:env(safe-area-inset-bottom);animation:tbeSheet .22s ease-out}
  .tbe-dlg-h{position:relative;padding-top:16px}
  .tbe-dlg-h::before{content:"";position:absolute;top:6px;left:50%;width:36px;height:4px;margin-left:-18px;border-radius:2px;background:var(--bd2)}
}
@keyframes tbeSheet{from{transform:translateY(32px);opacity:.4}to{transform:none;opacity:1}}
@media (prefers-reduced-motion:reduce){.tbe-dlg{animation:none!important}}


/* ── Lot mise en page 2026-10-21 ───────────────────────────────────────────────────────────────────────────── */
/* Libellé « Accords complets » rogné (largeur inline 132px + overflow:hidden) : largeur au contenu. */
#diatVoicingModeBtn{width:auto!important;min-width:132px;padding:0 10px!important}
/* Pas de viewport-fit=cover : l'APK Android (barre d'état noire + cutout shortEdges) décalait la topbar d'une seconde marge. */
/* Flottants métronome/mixeur : jamais plus grands que l'écran (contenu en scroll interne), pas de poignée resize au doigt. */
.metro-float{max-width:calc(100vw - 8px);max-height:calc(100vh - 8px);max-height:calc(100dvh - 8px)}
@media (pointer:coarse){.metro-float{resize:none}}
/* Cibles tactiles >= 32px sur écran tactile (hors grilles denses : touches de manche, pas du séquenceur, pads Surface). */
@media (pointer:coarse){
  :is(.tbtn,.rbtn,.rbtn-underline,.pill,.ft-file-btn,.st-btn,.tbe-btn,.rh-play-btn):not(:is(.rrow,#page-play,#gslFloat,.gsz-body,.fretboard,.seq-grid,.step-grid,#grooveWrap) *){min-height:32px;min-width:32px}
}
@media (pointer:coarse) and (min-height:600px){
  #grooveToolbarWrap #grooveCtrlBar button,#grooveToolbarWrap #grooveFsBtn{min-height:32px}
  #grooveToolbarWrap #grooveFsBtn{min-width:32px;flex-basis:32px}
}
/* Topbar compacte en paysage téléphone (113px = 29 % de 390px de haut) : rangées resserrées, aucun contrôle masqué. */
@media (max-height:500px) and (orientation:landscape) and (max-width:1023px){
  .tb1{height:36px}
  .tb2{height:30px}
  .tb3{height:28px}
}

/* Mobile : Séquenceur, boutons de transport / options moins grands (une seule barre, cf. flex-wrap:nowrap plus haut). */
@media(max-width:1023px){
  #page-melodie .sq-transport .sq-btn,#page-melodie .sq-opts .sq-btn{width:30px;height:30px}
  #page-melodie .sq-mini-btn{min-width:28px;height:30px}
  #page-melodie .sq-bar{gap:4px 6px}
  #page-melodie .sq-transport,#page-melodie .sq-opts{gap:4px}
}
/* Rotor plein écran en portrait (mobile) : boutons plus compacts et rapprochés pour libérer la largeur. */
@media(max-width:700px) and (orientation:portrait){
  body.groove-rotor-fullscreen .rotor-bars{--rbh:34px;gap:3px 4px}
  body.groove-rotor-fullscreen .rotor-bars button,body.groove-rotor-fullscreen .rotor-bars .dpk-select-btn{padding-left:6px;padding-right:6px}
  body.groove-rotor-fullscreen .rotor-bars>.rotor-play-bar,body.groove-rotor-fullscreen .rotor-bars>.rotor-bar{gap:3px}
}

/* :has() retiré des règles Paramètres (zpa-* / zsm-in / has-slider / has-tbtn, posées en JS) : chaque changement de classe
   n'importe où dans la page (ex. les temps du Métronome) forçait la ré-évaluation de ces :has() sur tout le DOM de la
   fenêtre de Paramètres, soit ~18 ms de recalcul de style + une saccade à chaque temps (mesuré sur tablette, cf. trace). */

#page-play .gsz-sheet .gsz-pen{display:inline-flex;color:var(--c-root);opacity:.85;margin-left:-4px}

/* Feuille de réglages : catégories, aide repliable (?), remise par défaut */
#page-play .gsz-cat{margin-top:6px;padding-top:6px;border-top:1px solid var(--bd);font:700 .62rem var(--fm);letter-spacing:.08em;text-transform:uppercase;color:var(--c-root)}
#page-play .gsz-sheet-body>.gsz-cat:first-child{margin-top:0;padding-top:0;border-top:0}
#page-play .gsz-q{flex:none;width:18px;height:18px;border-radius:50%;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font:700 .62rem/1 var(--fm);cursor:pointer;padding:0;margin-left:-4px}
#page-play .gsz-q.on{border-color:var(--c-root);color:var(--c-root)}
#page-play .gsz-info{flex:1 0 100%;font-size:.68rem;line-height:1.35;opacity:.8;padding:4px 6px;border-left:2px solid var(--c-root)}
#page-play .gsz-info[hidden],#page-play .gsz-rst[hidden]{display:none}
#page-play .gsz-rst{flex:none;width:22px;height:22px;display:inline-flex;align-items:center;justify-content:center;border-radius:6px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);cursor:pointer;padding:0;margin-left:auto}
#page-play .gsz-rst .gsz-ic{width:12px;height:12px}

/* ═══ Effets visuels DÉSACTIVÉS — revérification 2026-10-25 ═══
   Rapport : « en mode désactivé, j'ai toujours le rétrécissement au toucher ». Le palier 0 ne garde que l'indicateur
   fonctionnel (case allumée, liseré simple) : pas de rétrécissement (grille, hexa, Orbital lisent --gs-press-scale,
   posée en style inline par gsSetPressScale — d'où !important), pas de flash des pads ni des cellules de fonction,
   halo réduit à un liseré comme au palier Réduits. */
#page-play.gs-fx-off{--gs-press-scale:1!important}
#page-play.gs-fx-off .gs-cell.gs-on{box-shadow:0 0 0 2px var(--gs-c,var(--c-root))}
#page-play.gs-fx-off .gs-cell.gs-on[style*="--gs-c"]{box-shadow:0 0 0 2px var(--gs-c)}
#page-play.gs-fx-off .gsd-pad.gsd-hit,#page-play.gs-fx-off .gs-func-flash{animation:none}

/* ═══ Tiroir son + effets compact et redimensionnable (2026-10-25) ═══
   Potards au lieu de curseurs ; SON en haut, EFFETS dessous en cartes (interrupteur + potards) ; poignée basse façon barre
   latérale, deux crans « son » et « son + effets » (surface-synth.js : openDrawer / bindGrip). */
#page-play .gss-drawer.gss-drawer-rs{display:flex;flex-direction:column;max-height:none;overflow:hidden;padding:0;min-height:56px}
#page-play .gss-drawer-rs .gss-scroll{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:6px 10px 4px}
#page-play .gss-drawer-rs .gss-close{top:4px;right:6px;z-index:2}
#page-play .gss-grip{flex:none;height:16px;display:flex;align-items:center;justify-content:center;cursor:row-resize;touch-action:none;user-select:none;-webkit-user-select:none;
  border-top:1px solid var(--bd);background:var(--s2)}
#page-play .gss-grip span{width:28px;height:3px;border-top:1px solid var(--tx3);border-bottom:1px solid var(--tx3);box-sizing:border-box;opacity:.7}
#page-play .gss-grip:hover span,#page-play .gss-resizing .gss-grip span{border-color:var(--c-root);opacity:1}
#page-play .gss-snap .gss-grip{background:color-mix(in srgb,var(--c-root) 22%,var(--s2))}
#page-play .gss-resizing{user-select:none;-webkit-user-select:none}
@media (pointer:coarse){#page-play .gss-grip{height:22px}#page-play .gss-grip span{width:36px}}

#page-play .gss-sec{display:flex;flex-direction:column;gap:4px;min-width:0}
#page-play .gss-sec-fx{margin-top:6px;padding-top:6px;border-top:1px solid var(--bd)}
#page-play .gss-sec .gss-head{gap:6px;padding-right:34px}
#page-play .gss-sec .gss-head .gss-sub{margin:0 4px 0 0}
#page-play .gss-sec .gss-voice,#page-play .gss-sec .gss-osc{height:24px;padding:0 8px}

#page-play .gss-grid.gss-knobs{display:flex;flex-wrap:wrap;gap:2px 4px}

/* cartes d'effet */
#page-play .gss-fxgrid{display:flex;flex-wrap:wrap;gap:4px;align-items:flex-start}
#page-play .gss-fxc{display:flex;flex-direction:column;align-items:center;gap:2px;padding:3px 3px 2px;border-radius:8px;border:1px solid var(--bd);background:var(--s1);min-width:58px}
#page-play .gss-fxc.on{border-color:color-mix(in srgb,var(--c-root) 60%,var(--bd))}
#page-play .gss-fxc>.gsc-chip{width:100%;justify-content:center;height:22px;padding:0 6px;font-size:max(10px,.52rem)}
#page-play .gss-fxk{display:flex;gap:0 2px}
#page-play .gss-fxc:not(.on) .gss-kn-main .ftk-n{color:var(--tx3)}
#page-play .gss-fxc .gss-fxsel,#page-play .gss-fxc .dpk-select-btn{height:20px;max-width:100px;font-size:max(10px,.48rem)}

/* Modèles de nouvelle page (2026-10-25) : une colonne par catégorie, « Page vide » seule ; retour à la ligne si l'écran est étroit. */
.gs-picker-pop.gs-picker-percol .gs-picker-cols{flex-wrap:wrap;gap:10px 6px}
.gs-picker-pop.gs-picker-percol .gs-picker-col{flex:0 0 142px;min-width:0}

.gs-picker-pop.gs-picker-percol .gs-picker-group-lbl{white-space:normal;line-height:1.2;min-height:2.4em;display:flex;align-items:flex-end;border-bottom:1px solid var(--bd);padding-bottom:3px;margin-bottom:3px}
/* panneau FX : cartes de même hauteur (min-height posé par knobPanel), menu déroulant calé en bas */
#page-play .gss-fxgrid{align-items:stretch}
#page-play .gss-fxc{box-sizing:border-box}
#page-play .gss-fxc>.gss-fxsel,#page-play .gss-fxc>.dpk-select-btn{margin-top:auto}
/* deux catégories empilées dans une même colonne (titres gardés) */
.gs-picker-pop.gs-picker-percol .gs-picker-colsep{height:12px;flex:none}
.gs-picker-pop.gs-picker-percol .gs-picker-group-lbl{min-height:0}
/* Listes longues (timbres…) : colonnes rangées en JS, catégories entières sous leur titre (2026-10-25) */
.gs-picker-pop.gs-picker-packed .gs-picker-cols{display:flex;gap:0 14px;column-width:auto}
.gs-picker-pop.gs-picker-packed .gs-picker-col{display:flex;flex-direction:column;flex:1 1 0;min-width:0;margin:0}
.gs-picker-pop.gs-picker-packed .gs-picker-col>.gs-picker-group-lbl:not(:first-child){margin-top:10px}
/* listes rangées en colonnes : entrées plus serrées (2026-10-25) */
.gs-picker-pop.gs-picker-packed .gs-picker-item{padding:4px 8px;line-height:1.3}
@media (pointer:coarse){.gs-picker-pop.gs-picker-packed .gs-picker-item{padding:6px 8px}}
.gs-picker-pop.gs-picker-percol .gs-picker-item{padding:4px 8px;line-height:1.3}
@media (pointer:coarse){.gs-picker-pop.gs-picker-percol .gs-picker-item{padding:6px 8px}}

/* ═══ Potard commun (surface-knob.js, 2026-10-25) — look « atelier » ═══ */
.ftk{display:flex;flex-direction:column;align-items:center;width:48px;min-width:0;cursor:ns-resize;touch-action:none;user-select:none;-webkit-user-select:none;outline:none;border-radius:6px;font-family:var(--fm)}
.ftk:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb,var(--c-root) 60%,transparent)}
.ftk svg{width:32px;height:32px;display:block;overflow:visible}
.ftk .ftk-t{fill:none;stroke:var(--bd2);stroke-width:3;stroke-linecap:round}
.ftk .ftk-v{fill:none;stroke:var(--c-root);stroke-width:3;stroke-linecap:round}
.ftk .ftk-m{fill:none;stroke:color-mix(in srgb,var(--c-root) 55%,#7fd3ff);stroke-width:2;stroke-linecap:round;opacity:.85}
.ftk .ftk-i{stroke:var(--tx);stroke-width:2;stroke-linecap:round}
.ftk .ftk-l{max-width:100%;font-size:max(9px,.46rem);color:var(--tx2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.1;margin-top:1px}
.ftk .ftk-n{max-width:100%;font-size:max(9px,.48rem);color:var(--tx);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.1;font-weight:600}
.ftk.drag .ftk-t{stroke:color-mix(in srgb,var(--c-root) 35%,var(--bd2))}
.ftk-sz-s{width:42px}.ftk-sz-s svg{width:26px;height:26px}
.ftk-sz-l{width:66px}.ftk-sz-l svg{width:48px;height:48px}.ftk-sz-l .ftk-l{font-size:max(10px,.54rem)}.ftk-sz-l .ftk-n{font-size:max(10px,.58rem)}
@media (pointer:coarse){.ftk{width:52px}.ftk svg{width:36px;height:36px}.ftk-sz-s svg{width:30px;height:30px}.ftk-sz-l{width:70px}.ftk-sz-l svg{width:52px;height:52px}}
.ftk-tip{position:fixed;z-index:2000;transform:translate(-50%,-100%);padding:4px 8px;border-radius:6px;background:var(--s3,#333);border:1px solid var(--c-root);
  color:var(--tx);font-family:var(--fm);font-size:max(11px,.62rem);font-weight:700;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .08s}
.ftk-tip.on{opacity:1}
#page-play .gss-fxc:not(.on) .gss-kn-main .ftk-n{color:var(--tx3)}

/* ═══ SynthFace (surface-synthlab.js, 2026-10-25) ═══
   Deux faces : « Jouer » (8 macros + aperçus) et « Éditer » (4 sections dans l'ordre du signal ; onglets si la zone est étroite). */
#page-play .sf-host{display:flex;flex-direction:column;gap:4px;padding:4px;overflow:auto;border-radius:10px;background:var(--s1);border:1px solid var(--bd);box-sizing:border-box;min-width:0}
#page-play .sf-host .sl-top{display:flex;align-items:center;gap:4px;flex-wrap:wrap}
#page-play .sf-host .sl-title{font-family:var(--fm);font-size:max(10px,.6rem);font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--c-root);margin-right:4px}
#page-play .sf-host .sl-btn{height:24px;padding:0 8px;border-radius:7px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:max(10px,.56rem);font-weight:700;cursor:pointer;touch-action:manipulation;white-space:nowrap}
#page-play .sf-host .sl-btn.on{border-color:var(--c-root);color:var(--c-root)}
#page-play .sf-host .sf-name{min-width:110px;max-width:220px;overflow:hidden;text-overflow:ellipsis;border-color:var(--c-root)}
#page-play .sf-host .sf-nav{padding:0 6px}
#page-play .sf-host .sf-face{margin-left:auto}
#page-play .sf-host .sl-seg{display:flex;flex-wrap:wrap;gap:2px}
#page-play .sf-host .sl-seg button{height:20px;padding:0 5px;border-radius:5px;border:1px solid var(--bd);background:transparent;color:var(--tx2);font-family:var(--fm);font-size:max(9px,.5rem);font-weight:700;cursor:pointer;touch-action:manipulation}
#page-play .sf-host .sl-seg button.on{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-play .sf-host .sf-face button{height:24px;padding:0 9px;font-size:max(10px,.56rem)}
#page-play .sf-host .sf-icons button{width:24px;padding:0;display:inline-flex;align-items:center;justify-content:center}
#page-play .sf-host .sf-icons svg{width:16px;height:12px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linejoin:round;stroke-linecap:round}
#page-play .sf-host .sf-tog{height:20px;padding:0 6px;font-size:max(9px,.5rem);border-radius:5px}
#page-play .sf-save{display:flex;gap:4px;align-items:center;flex-wrap:wrap}
#page-play .sf-save input{height:24px;flex:1 1 140px;min-width:100px;max-width:260px;border-radius:6px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:max(11px,.6rem);padding:0 6px}
/* face Jouer */
#page-play .sf-play{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start;padding:4px 2px}
#page-play .sf-macros{display:grid;grid-template-columns:repeat(4,auto);gap:6px 10px;padding:6px 8px;border-radius:10px;background:var(--s2);border:1px solid var(--bd)}
#page-play .sf-play-side{display:flex;flex-direction:column;gap:6px}
#page-play .sf-hint{flex-basis:100%;font-family:var(--fm);font-size:max(10px,.48rem);color:var(--tx3)}
@media (min-width:900px){#page-play .sf-macros{grid-template-columns:repeat(8,auto)}}
/* face Éditer */
#page-play .sf-tabs{display:none;gap:3px}
#page-play .sf-tabs button{flex:1 1 0;height:26px;border-radius:7px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx2);font-family:var(--fm);font-size:max(10px,.52rem);font-weight:700;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#page-play .sf-tabs button.on{border-color:var(--c-root);color:var(--c-root)}
#page-play .sf-sec{display:flex;flex-direction:column;gap:2px}
#page-play .sf-sech{font-family:var(--fm);font-size:max(9px,.46rem);font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--tx3);padding:2px 2px 0}
#page-play .sf-secrow{display:flex;flex-wrap:wrap;gap:4px;align-items:stretch}
#page-play .sf-mod{display:flex;flex-direction:column;gap:3px;padding:3px 6px 4px;border-radius:8px;background:var(--s2);border:1px solid var(--bd);flex:1 1 auto;min-width:0}
#page-play .sf-mod .sl-h{display:flex;align-items:center;gap:5px;flex-wrap:wrap}
#page-play .sf-mod .sl-mh{font-family:var(--fm);font-size:max(9px,.5rem);font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--tx2);white-space:nowrap}
#page-play .sf-mod .sl-row{display:flex;flex-wrap:wrap;gap:2px 0;align-items:flex-start}
#page-play .sf-lock{margin-left:auto;width:20px;height:20px;padding:0;border:none;background:none;color:var(--tx3);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;order:99}
#page-play .sf-lock svg{width:11px;height:13px;fill:none;stroke:currentColor;stroke-width:1.4}
#page-play .sf-lock.on{color:var(--c-root)}
#page-play .sf-viz{flex:none;margin-right:4px}
#page-play .sf-env,#page-play .sf-filt{width:150px;height:60px;border-radius:6px;background:var(--s1);border:1px solid var(--bd);touch-action:none;cursor:crosshair;display:block}
#page-play .sf-play .sf-env,#page-play .sf-play .sf-filt{width:200px;height:80px}
#page-play .sf-note{font-family:var(--fm);font-size:max(9px,.46rem);color:var(--tx3);flex-basis:100%}
#page-play .sf-mx{display:flex;flex-direction:column;gap:2px}
#page-play .sf-mxr{display:flex;align-items:center;gap:4px;opacity:.6}
#page-play .sf-mxr.on{opacity:1}
#page-play .sf-mxr .sl-btn{height:22px;padding:0 6px;min-width:84px;max-width:140px;overflow:hidden;text-overflow:ellipsis;font-size:max(9px,.5rem)}
#page-play .sf-mxa{color:var(--tx3);font-family:var(--fm)}
#page-play .sf-mxr .ftk{flex-direction:row;width:auto;gap:4px}
#page-play .sf-mxr .ftk .ftk-n{min-width:34px;text-align:left}
#page-play .sf-host.sf-narrow .sf-tabs{display:flex}
#page-play .sf-host.sf-narrow .sf-sec{display:none}
#page-play .sf-host.sf-narrow .sf-sec.on{display:flex}
#page-play .sf-host.sf-narrow .sf-sech{display:none}
#page-play .sf-host.sf-narrow .sf-face{margin-left:0}
#page-play .gss-tosf{border-color:var(--c-root);color:var(--c-root)}
#page-play .gss-sfname{font-family:var(--fm);font-size:max(10px,.54rem);color:var(--tx2);white-space:nowrap}

/* SynthFace, face Éditer compacte (2026-10-25) : modules à leur taille (aucun étirement vertical), matrice sur 2 colonnes,
   nom du patch à largeur fixe (les boutons ◀ ▶ ne bougent plus d'un patch à l'autre) */
#page-play .sf-secrow{align-items:flex-start}
#page-play .sf-mod{flex:0 1 auto}
#page-play .sf-mx{display:grid;grid-template-columns:repeat(2,auto);gap:3px 12px}
#page-play .sf-host .sf-name{width:190px;min-width:0;max-width:none;text-align:left;flex:none}
#page-play .sf-host.sf-narrow .sf-name{width:140px}
#page-play .sf-lbl{font-family:var(--fm);font-size:max(9px,.48rem);color:var(--tx3);margin-left:4px}
#page-play .sf-host .ftk{width:52px}
@media (pointer:coarse){#page-play .sf-host .ftk{width:56px}}
/* SynthFace : texture « fibre de carbone » OBLIQUE (sergé à 45°, dégradés CSS, aucune image) — deux trames de bandes
   diagonales croisées en noir et blanc très transparents, posées sur la couleur de surface du thème, plus un léger reflet. */
#page-play .sf-mod,#page-play .sf-macros{
  background-color:var(--s2);
  background-image:
    linear-gradient(180deg,rgba(255,255,255,.05),rgba(255,255,255,0) 45%),
    linear-gradient(45deg,rgba(0,0,0,.24) 25%,transparent 25%,transparent 50%,rgba(0,0,0,.24) 50%,rgba(0,0,0,.24) 75%,transparent 75%),
    linear-gradient(135deg,rgba(255,255,255,.04) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.04) 50%,rgba(255,255,255,.04) 75%,transparent 75%);
  background-size:100% 100%,6px 6px,12px 12px;
  background-position:0 0,0 0,0 0;
}
#page-play .sf-mod{flex:1 1 auto}
#page-play .sf-host .ftk.ftk-sz-l{width:74px}
@media (pointer:coarse){#page-play .sf-host .ftk.ftk-sz-l{width:76px}}
/* Mobile en paysage (peu de hauteur) : face Jouer compactée — 8 macros serrées sur une rangée (au lieu de 4×2)
   avec les 2 graphes à côté, l'indication masquée pour gagner la hauteur. */
@media (orientation:landscape) and (max-height:520px) and (pointer:coarse){
  #page-play .sf-play{flex-wrap:nowrap;gap:6px;align-items:stretch}
  #page-play .sf-macros{grid-template-columns:repeat(8,auto);gap:2px 0;padding:4px 4px;flex:0 1 auto;min-width:0}
  #page-play .sf-host .ftk.ftk-sz-l{width:54px}
  #page-play .sf-play-side{flex-direction:row;gap:4px;flex:0 0 auto}
  #page-play .sf-play .sf-env,#page-play .sf-play .sf-filt{width:128px;height:64px}
  #page-play .sf-hint{display:none}
}

/* ═══ Guitare → MIDI (surface-g2m.js, 2026-10-25) ═══ */
#page-play .g2m-host{display:flex;flex-direction:column;gap:5px;padding:6px;overflow:auto;border-radius:10px;border:1px solid var(--bd);box-sizing:border-box;min-width:0;
  background-color:var(--s1);font-family:var(--fm)}
#page-play .g2m-top,#page-play .g2m-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
#page-play .g2m-title{font-size:max(10px,.6rem);font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--c-root);margin-right:auto}
#page-play .g2m-btn,#page-play .g2m-go{height:26px;padding:0 10px;border-radius:7px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:max(10px,.56rem);font-weight:700;cursor:pointer;white-space:nowrap}
#page-play .g2m-btn.on{border-color:var(--c-root);color:var(--c-root)}
#page-play .g2m-go{min-width:86px}
#page-play .g2m-go.on{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-play .g2m-voice{max-width:170px;overflow:hidden;text-overflow:ellipsis}
#page-play .g2m-seg{display:flex;gap:2px;flex-wrap:wrap}
#page-play .g2m-seg button{height:24px;padding:0 8px;border-radius:6px;border:1px solid var(--bd);background:transparent;color:var(--tx2);font-family:var(--fm);font-size:max(10px,.52rem);font-weight:700;cursor:pointer}
#page-play .g2m-seg button.on{background:var(--c-root);border-color:var(--c-root);color:#111}
#page-play .g2m-disp{display:grid;grid-template-columns:auto 1fr;grid-template-areas:"n c" "n m" "i i";gap:4px 12px;align-items:center;padding:6px 8px;border-radius:8px;background:var(--s2);border:1px solid var(--bd)}
#page-play .g2m-note{grid-area:n;min-width:84px;text-align:center;font-size:max(22px,1.6rem);font-weight:700;color:var(--tx3)}
#page-play .g2m-note.on{color:var(--c-root)}
#page-play .g2m-cents,#page-play .g2m-meter{position:relative;height:10px;border-radius:5px;background:var(--s3,var(--bd))}
#page-play .g2m-cents{grid-area:c}#page-play .g2m-meter{grid-area:m}
#page-play .g2m-cents i{position:absolute;left:50%;top:-2px;bottom:-2px;width:2px;background:var(--tx3)}
#page-play .g2m-cents b{position:absolute;top:-3px;width:8px;height:16px;margin-left:-4px;border-radius:3px;background:var(--c-root);transition:left .05s linear}
#page-play .g2m-meter i{position:absolute;left:0;top:0;bottom:0;border-radius:5px;background:linear-gradient(90deg,#3ba55d,#d4c445 75%,#e5484d)}
#page-play .g2m-meter b{position:absolute;top:-3px;bottom:-3px;width:2px;background:var(--tx)}
#page-play .g2m-info{grid-area:i;font-size:max(10px,.5rem);color:var(--tx2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#page-play .g2m-hint{font-size:max(10px,.46rem);color:var(--tx3)}
#page-play .g2m-outsel{height:26px;max-width:180px;padding:0 6px;border-radius:7px;border:1px solid var(--bd2);background:var(--s2);color:var(--tx);font-family:var(--fm);font-size:max(10px,.54rem);font-weight:700;cursor:pointer}
#page-play .g2m-outsel option{background:var(--s1);color:var(--tx)}
#page-play .g2m-midiinfo{grid-column:1/-1;min-width:0;font-size:max(10px,.5rem);color:var(--tx2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#page-play .g2m-midiinfo.warn{color:#e5484d;font-weight:700}
#page-play .g2m-host .ftk{width:58px}
#page-play .sf-host .ftk.ftk-sz-l{width:86px}
/* SynthFace, face Éditer (2026-10-26) : en zone large, les 4 sections s'écoulent comme un seul bloc de modules (les titres de
   section ne mangent plus de hauteur, plus de rangée à moitié vide) ; potards et aperçus d'enveloppe un peu plus serrés. */
#page-play .sf-flow{display:flex;flex-wrap:wrap;gap:4px;align-items:stretch;align-content:flex-start}
#page-play .sf-host:not(.sf-narrow) .sf-sec,#page-play .sf-host:not(.sf-narrow) .sf-secrow{display:contents}
#page-play .sf-host:not(.sf-narrow) .sf-sech{display:none}
#page-play .sf-host.sf-narrow .sf-flow{display:block}
#page-play .sf-flow .sf-mod{flex:1 1 auto}
#page-play .sf-flow .sf-mod .sl-row{justify-content:space-around;flex:1 1 auto}
#page-play .sf-flow .sf-mod[data-mod="voice"]{flex:0 1 auto;max-width:420px}
#page-play .sf-flow .sf-mod[data-mod="matrix"]{flex:0 1 auto}
#page-play .sf-flow .sf-mod[data-mod="matrix"] .sl-row,#page-play .sf-flow .sf-mod[data-mod="voice"] .sl-row{justify-content:flex-start;flex:0 1 auto}
#page-play .sf-flow .sf-mx{gap:3px 8px}
#page-play .sf-flow .sf-mxr .sl-btn{min-width:72px}
/* petits modules : à leur largeur naturelle (pas d'étirement) ; l'en-tête d'un LFO (formes + Tempo + Relance) passe sur 2 lignes */
#page-play .sf-flow .sf-mod[data-mod="lfo1"],#page-play .sf-flow .sf-mod[data-mod="lfo2"]{flex:0 1 auto;max-width:236px}
#page-play .sf-flow .sf-mod[data-mod="menv"],#page-play .sf-flow .sf-mod[data-mod="vel"],#page-play .sf-flow .sf-mod[data-mod="fx"],
#page-play .sf-flow .sf-mod[data-mod="unison"],#page-play .sf-flow .sf-mod[data-mod="mix"],#page-play .sf-flow .sf-mod[data-mod="fm"]{flex:0 1 auto}
#page-play .sf-flow .sf-mod[data-mod="lfo1"] .sl-row,#page-play .sf-flow .sf-mod[data-mod="lfo2"] .sl-row,#page-play .sf-flow .sf-mod[data-mod="menv"] .sl-row,
#page-play .sf-flow .sf-mod[data-mod="vel"] .sl-row,#page-play .sf-flow .sf-mod[data-mod="fx"] .sl-row,#page-play .sf-flow .sf-mod[data-mod="unison"] .sl-row,
#page-play .sf-flow .sf-mod[data-mod="mix"] .sl-row,#page-play .sf-flow .sf-mod[data-mod="fm"] .sl-row{justify-content:flex-start;flex:0 1 auto}
#page-play .sf-flow .sf-env,#page-play .sf-flow .sf-filt{width:112px;height:50px}

