/* ========================================================
   TINY TEMPLE TOOLBOX - COMPONENTS
   Componenti tb-* riusabili nelle pagine della Toolbox: tile
   della dashboard, picker, pill, bottoni, segmented, select,
   range, toggle, campo numerico, info+foglio, stato, toast,
   consenso microfono. Richiede shared/tokens.css e shared/base.css
   caricati prima. Nomi generici (nessun "toolbox"): riusabili da
   un altro sottodominio (spec 02 §6). Regole comuni ai componenti
   nuovi (spec 03 §2): altezza >= --tap (44px), hover solo con
   puntatore fine, :active/.is-pressed scala 0.97, :focus-visible
   arancione (gia' globale in base.css), disabilitato a opacita'
   0.45 senza cambi di colore (regola in fondo a questo blocco),
   transizioni --dur-fast, azzerate da reduced-motion (regola
   globale gia' in base.css, agisce su transition/animation-duration
   di qualunque selettore quindi copre anche questi).

   CONTRATTO per shared/select.js, shared/sheet.js, shared/dash.js
   (spec 03 §9 — builder scrive SOLO il markup statico e questo CSS,
   zero JS):

   .tb-select (select.js). Markup gia' pronto in ogni pagina che lo usa:
     <div class="tb-select" data-value="<valore iniziale>">
       <select class="tb-select-native" id="<id>">...<option value>...</select>
       <button type="button" class="tb-select-trigger" aria-haspopup="listbox"
               aria-expanded="false" aria-controls="<id>-panel" id="<id>-trigger">
         <span class="tb-select-label" data-i18n="...">...</span>
         <span class="tb-select-value">...</span>
         <svg class="tb-select-chevron" aria-hidden="true"><use href="#tb-icon-chevron-down"></use></svg>
       </button>
       <div class="tb-select-panel" role="listbox" id="<id>-panel" hidden
            aria-labelledby="<id>-trigger" tabindex="-1">
         <button type="button" class="tb-select-option" role="option" data-value="..."
                 aria-selected="true|false">
           <span>...</span>
           <svg class="tb-select-check" aria-hidden="true"><use href="#tb-icon-check"></use></svg>
         </button>...
       </div>
     </div>
   Il <select> nativo e' il valore vero e il ripiego senza JS (visibile,
   utilizzabile, come lo era prima di questa spec): select.js aggiunge
   .tb-select.is-enhanced, che nasconde il nativo (sr-only, resta nel DOM
   e nel form) e mostra pillola+pannello, tenuti sincronizzati (cambiare
   l'uno aggiorna l'altro). Apertura: click/Invio/Spazio/freccia sul
   trigger; dentro il pannello frecce/Home/End/lettere spostano l'opzione
   attiva (.tb-select-option.is-active + aria-activedescendant sul
   pannello); Invio o click scelgono (aria-selected sull'opzione, il
   valore sul trigger/nativo); Esc, tocco fuori o scelta chiudono
   ([hidden] sul pannello, aria-expanded=false) e riportano il focus al
   trigger. Sopra 480px il pannello e' ancorato sotto il trigger (sopra
   se manca spazio: .tb-select-panel.is-above); sotto 480px diventa un
   .tb-sheet (aggiungi anche quella classe al pannello, vedi sotto).

   .tb-info + .tb-sheet (sheet.js). Markup gia' pronto:
     <button type="button" class="tb-info" aria-haspopup="dialog" aria-expanded="false"
             aria-controls="<id>" data-i18n-aria="info-aria" aria-label="Che cos'e'">
       <span class="tb-info-glyph" aria-hidden="true">i</span>
     </button>
     <div class="tb-sheet-scrim" id="<id>-scrim" hidden></div>
     <div class="tb-sheet" id="<id>" role="dialog" aria-modal="true" aria-labelledby="<id>-title" hidden>
       <div class="tb-sheet-head">
         <h2 id="<id>-title" data-i18n="...">...</h2>
         <button type="button" class="tb-sheet-close" data-i18n-aria="sheet-close" aria-label="Chiudi">&times;</button>
       </div>
       <div class="tb-sheet-body" data-i18n="...">...</div>
     </div>
   sheet.js apre/chiude togliendo/mettendo [hidden] su scrim+foglio insieme
   a .is-open (l'animazione sta sulle classi); sotto 768px e' un foglio dal
   basso con scrim scuro; da 768px un popover ancorato vicino al bottone,
   senza scrim scuro (solo per chiudere al tocco fuori: .tb-sheet-scrim
   resta presente ma trasparente, vedi CSS). Chiude con la X, lo scrim o
   Esc; gestisce da solo focus trap, inert sul contenuto dietro e
   scroll-lock. Riporta il focus al bottone .tb-info che l'ha aperto. Il
   contratto di .tb-tile-grip (dash.js) e' descritto subito sotto, nel
   blocco DASHBOARD: TILE (spec 24 §3-4). */

/* --- DASHBOARD: TILE (spec 24 §3-4, erede della spec 02 §4) --- */
/* CONTRATTO shared/dash.js (markup gia' pronto in index.html: griglia
   vuota #tb-tiles, sprite icone #tb-icon-<slug>, live region #tb-dash-live,
   #tb-dash-hint sotto la griglia):
     <li data-slug="<slug>">
       <button type="button" class="tb-tile-grip" aria-pressed="false"
               data-i18n-aria="dash-move-aria" data-i18n-tool="tool-<slug>">
         <svg aria-hidden="true"><use href="#tb-icon-grip"></use></svg></button>
       <a class="tb-tile" href="/<slug>">
         <span class="tb-tile-icon"><svg><use href="#tb-icon-<slug>"></use></svg></span>
         <span class="tb-tile-name" data-i18n="tool-<slug>">...</span>
       </a>
     </li>                                                       -- status: 'live'
     <li data-slug="<slug>">
       <button class="tb-tile-grip">...</button>
       <div class="tb-tile is-soon" aria-disabled="true" tabindex="-1">
         ...stessa struttura...
         <span class="sr-only" data-i18n="pill-soon">In arrivo</span>
       </div>
     </li>                                                       -- status: 'soon'
   Nessun "Modifica"/"togli": tutti gli strumenti stanno sempre in
   dashboard, si spostano soltanto (anche la tile 'soon'). Ordine: da
   prefs.get('dash','order') se c'e', poi shared/tools.js in coda per gli
   slug nuovi. */
.tb-tiles {
    list-style: none;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
    gap: 14px;
    max-width: 960px;
    margin: 0 auto;
    /* Spec 24 §3: il riordino parte con una pressione lunga (dito) o un
       trascinamento (mouse/tastiera); niente menu contestuale ne' testo
       selezionato per sbaglio mentre si tiene una tile. */
    -webkit-touch-callout: none;
    user-select: none;
}

/* Riordino (dash.js): la tile trascinata segue il dito/mouse senza
   transizione, le altre scivolano al nuovo posto con il FLIP. La
   transizione sta qui, cosi' dash.js deve solo mettere e togliere la
   classe. */
.tb-tiles > li.is-moving {
    transition: transform 180ms var(--ease-cinematic);
    will-change: transform;
}

.tb-tiles > li.is-dragging {
    transition: none;
    z-index: 5;
}

/* Pressione riuscita (450ms dito, o presa da tastiera) e trascinamento:
   la tile si solleva appena percettibilmente e resta sollevata finche'
   segue il dito/mouse/frecce, cosi' si sente che ha "staccato" dalla
   griglia (dash.js toglie .is-lifting e mette .is-dragging appena parte
   il trascinamento vero: stessa resa per entrambe). */
.tb-tiles > li.is-lifting,
.tb-tiles > li.is-dragging {
    transform: scale(1.04);
    box-shadow: var(--shadow-pop);
}

@media (prefers-reduced-motion: reduce) {
    .tb-tiles > li.is-moving {
        transition: none;
    }

    .tb-tiles > li.is-lifting,
    .tb-tiles > li.is-dragging {
        transform: none;
    }
}

/* Ancora per dash.js durante il riordino. */
.tb-tiles > li {
    position: relative;
}

@media (min-width: 480px) {
    .tb-tiles {
        grid-template-columns: repeat(4, 1fr);
    }
}

/* Da 768px: tile quadrate ~120px (non scalate a riempire il container,
   che sembrava vuoto con poche tile), gap piu' ampio, contenitore piu' largo. */
@media (min-width: 768px) {
    .tb-tiles {
        grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
        gap: 16px;
        max-width: 1100px;
    }

    .tb-tile-icon {
        width: 32px;
        height: 32px;
    }
}

.tb-tile {
    position: relative;
    aspect-ratio: 1 / 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 10px 6px;
    border-radius: var(--r-lg);
    border: 1px solid rgba(255, 255, 255, 0.1);
    background: rgba(255, 255, 255, 0.03);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    color: inherit;
    text-decoration: none;
    text-align: center;
    -webkit-touch-callout: none;
    user-select: none;
    transition: border-color 0.3s ease, background 0.3s ease;
}

.tb-tile-icon {
    width: 28px;
    height: 28px;
    color: var(--accent-orange);
}

/* L'<svg> dei tb-icon-* non ha larghezza/altezza proprie (solo viewBox):
   senza questa regola resta 300x150 (default UA) e sfonda il tile. */
.tb-tile-icon svg {
    width: 100%;
    height: 100%;
    display: block;
}

.tb-tile-name {
    font-size: 0.72rem;
    font-weight: 500;
    line-height: 1.25;
    color: var(--text-primary);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* In arrivo: attenuato, non apribile (spec 02 §4). */
.tb-tile.is-soon {
    border-style: dashed;
    opacity: 0.55;
}

.tb-tile.is-soon .tb-tile-icon {
    color: var(--text-muted);
}

.tb-tile.is-soon .tb-tile-name {
    color: var(--text-muted);
}

.tb-tile.is-pressed,
a.tb-tile:active {
    border-color: var(--accent-orange);
}

@media (hover: hover) and (pointer: fine) {
    a.tb-tile:hover {
        border-color: var(--accent-orange);
        background: rgba(207, 92, 54, 0.08);
    }
}

/* --- MANIGLIA TILE (spec 24 §3, erede della spec 03 §5): serve la
   TASTIERA (e il mouse); il dito trascina la tile intera dopo la pressione
   lunga, senza bisogno della maniglia. Per questo e' visibile SOLO con
   puntatore fine (mouse/trackpad): su un touch-screen puro resta nel DOM
   (tastiera Bluetooth) ma non si vede e non occupa spazio, cosi' l'icona
   non "galleggia" sopra il contenuto della tile senza motivo. Discreta:
   nessun cerchio vetro, solo l'icona a sei punti attenuata; l'area di
   tocco 44px resta ma trasparente. touch-action:none: il trascinamento
   da qui parte SOLO da qui (il resto del <li> torna a pan-y in dash.js). */
.tb-tile-grip {
    display: none;
    position: absolute;
    top: 0;
    left: 0;
    z-index: 2;
    width: var(--tap);
    height: var(--tap);
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    color: var(--text-muted);
    opacity: 0.6;
    touch-action: none;
    cursor: grab;
}

@media (hover: hover) and (pointer: fine) {
    .tb-tile-grip {
        display: flex;
    }
}

.tb-tile-grip:active,
.tb-tile-grip[aria-pressed="true"] {
    cursor: grabbing;
    color: var(--accent-orange);
    opacity: 1;
}

.tb-tile-grip svg {
    width: 18px;
    height: 18px;
}

/* Riservato per "Recenti" quando uno strumento salvera' record (spec 02 §4):
   per ora resta sempre nascosto, nessun contenuto. */
.tb-slot[hidden] {
    display: none !important;
}

/* --- DASHBOARD: TESTATA --- */
.tb-dash-top {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: center;
    gap: 8px 16px;
    max-width: 960px;
    margin: 0 auto;
    padding: 4px 0 20px;
}

/* Sottotitolo (spec 18 §7.11): l'unico h1 e' sr-only, questa e' la riga che
   dice cosa sono le tile. */
.tb-dash-sub {
    flex: 1 1 220px;
    min-width: 0;
    margin: 0;
    font-size: 0.82rem;
    line-height: 1.45;
    color: var(--text-muted);
}

/* Suggerimento di riordino sotto la griglia (spec 24 §3): testo scelto e
   nascosto da shared/dash.js (dash-hint-touch/-mouse), vuoto finche' non
   c'e' nulla da mostrare -- niente da vedere se #tb-dash-hint e' vuoto. */
.tb-dash-hint {
    max-width: 960px;
    margin: 14px auto 0;
    padding: 0 4px;
    font-size: 0.78rem;
    line-height: 1.4;
    color: var(--text-muted);
    text-align: center;
}

.tb-dash-hint:empty {
    display: none;
}

/* Pill di stato compatta: "In arrivo" sulla tile (spec 02 §3/§7). */
.tb-pill {
    flex-shrink: 0;
    display: inline-block;
    padding: 3px 10px;
    border-radius: var(--r-pill);
    border: 1px solid rgba(255, 255, 255, 0.18);
    background: rgba(255, 255, 255, 0.04);
    font-size: 0.58rem;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--text-muted);
}

/* --- BOTTONI (spec 03 §2) --- */
/* Base = variante "secondaria" (vetro scuro): usata cosi' com'e' per i
   bottoni secondari (Tap/Conta del metronomo, Ripristina ordine...). Una
   sola --primary per schermata (disciplina di chi scrive il markup, non
   imposta dal CSS). */
.tb-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
    min-height: var(--tap);
    padding: 14px 32px;
    border-radius: var(--r-pill);
    border: 1px solid rgba(255, 255, 255, 0.2);
    background: rgba(255, 255, 255, 0.04);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    font-family: var(--font-text);
    font-size: 0.82rem;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 2px;
    color: var(--text-primary);
    text-decoration: none;
    transition: all var(--dur-med) var(--ease-cinematic);
}

.tb-btn:active {
    transform: scale(0.97);
    border-color: var(--accent-orange);
}

/* Icona dentro un bottone (es. tb-back): stessa altezza del testo uppercase. */
.tb-btn svg {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
}

/* --primary (e .tb-install, suo alias per il CTA di installazione):
   vetro colorato, non piatto - lo stesso backdrop-filter della base sopra
   il gradiente, bordo chiaro in alto, --shadow-btn. */
.tb-btn--primary,
.tb-install {
    background: var(--grad-accent);
    border-color: var(--accent-orange);
    border-top-color: rgba(255, 255, 255, 0.5);
    box-shadow: var(--shadow-btn);
    color: #ffffff;
}

.tb-btn--primary:active,
.tb-install:active {
    background: #b64d2c;
    border-color: #b64d2c;
}

/* --ghost: solo testo, nessun bordo/sfondo (es. "Disattiva microfono"). */
.tb-btn--ghost {
    min-height: var(--tap);
    padding: 0 var(--sp-2);
    border: none;
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    color: var(--text-muted);
    text-transform: none;
    letter-spacing: normal;
    font-weight: 600;
}

.tb-btn--ghost:active {
    border-color: transparent;
    color: var(--accent-orange);
}

/* --icon: tondo 44px, per i passi del metronomo (+1/-1/+5/-5) e simili. */
.tb-btn--icon {
    min-width: var(--tap);
    min-height: var(--tap);
    padding: 0 10px;
    border-radius: var(--r-pill);
    font-size: 0.95rem;
    letter-spacing: normal;
    text-transform: none;
}

/* --lg: azione principale a piena evidenza (es. Avvia/Ferma). */
.tb-btn--lg {
    min-height: 56px;
    min-width: 200px;
    padding: 16px 40px;
    font-size: 0.95rem;
    letter-spacing: 0.05em;
}

@media (hover: hover) and (pointer: fine) {
    .tb-btn:hover {
        border-color: var(--accent-orange);
        background: rgba(207, 92, 54, 0.15);
    }
    .tb-btn--primary:hover,
    .tb-install:hover {
        background: #b64d2c;
        border-color: #b64d2c;
    }
    .tb-btn--ghost:hover {
        color: var(--accent-orange);
        background: none;
    }
}

.tb-btn[hidden] {
    display: none !important;
}

/* --- SEGMENTED (spec 03 §2/§7): sostituisce le file di bottoni fatte a
   mano (.met-choices, .acc-mode...). Markup: .tb-segment[role=radiogroup]
   > .tb-segment-btn[role=radio][aria-checked]. --scroll: una riga sola a
   scorrimento orizzontale con snap, per i gruppi piu' lunghi (battute,
   suddivisioni). */
.tb-segment {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-1);
    padding: var(--sp-1);
    border-radius: var(--r-pill);
    background: var(--surface-1);
    border: 1px solid var(--glass-border);
}

.tb-segment-btn {
    flex: 1;
    min-width: var(--tap);
    min-height: var(--tap);
    padding: 0 var(--sp-3);
    border-radius: var(--r-pill);
    color: var(--text-muted);
    font-weight: 600;
    font-size: 0.85rem;
    transition: background var(--dur-med) ease, color var(--dur-med) ease;
}

/* Gradiente "tenue" (spec 03 §2): il token --grad-accent smorzato con un
   velo scuro sopra, non piatto ma neanche a piena intensita' come --primary. */
.tb-segment-btn[aria-checked="true"] {
    background:
        linear-gradient(rgba(8, 8, 8, 0.78), rgba(8, 8, 8, 0.78)),
        var(--grad-accent);
    color: #ffffff;
}

@media (hover: hover) and (pointer: fine) {
    .tb-segment-btn:not([aria-checked="true"]):hover {
        color: var(--text-primary);
    }
}

.tb-segment--scroll {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 16px, #000 calc(100% - 16px), transparent 100%);
    mask-image: linear-gradient(90deg, transparent 0, #000 16px, #000 calc(100% - 16px), transparent 100%);
}

.tb-segment--scroll::-webkit-scrollbar {
    display: none;
}

.tb-segment--scroll .tb-segment-btn {
    flex: none;
    scroll-snap-align: center;
}

/* --- SELECT (spec 03 §2/§9: vedi il CONTRATTO select.js in cima al file) --- */
.tb-select {
    position: relative;
    width: 100%;
}

.tb-select-native {
    width: 100%;
    min-height: var(--tap);
    padding: 0 var(--sp-3);
    border-radius: var(--r-md);
    border: 1px solid var(--glass-border);
    background: var(--surface-1);
    color: var(--text-primary);
    font-family: var(--font-text);
    font-size: 0.9rem;
}

/* Preso in carico da select.js: nativo fuori vista ma nel DOM/nel form
   (sr-only, non display:none: resta annunciabile e nel tab order se la
   pillola per qualche motivo non lo e'). */
.tb-select.is-enhanced .tb-select-native {
    position: absolute !important;
    width: 1px !important;
    height: 1px !important;
    padding: 0 !important;
    margin: -1px !important;
    overflow: hidden !important;
    clip: rect(0, 0, 0, 0) !important;
    white-space: nowrap !important;
    border: 0 !important;
}

.tb-select-trigger {
    display: none;
    align-items: center;
    gap: var(--sp-2);
    width: 100%;
    min-height: var(--tap);
    padding: 0 var(--sp-4);
    border-radius: var(--r-pill);
    border: 1px solid var(--glass-border);
    border-top: 1px solid var(--glass-border-top);
    background: var(--glass-bg);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    color: var(--text-primary);
    text-align: left;
}

.tb-select.is-enhanced .tb-select-trigger {
    display: flex;
}

.tb-select-trigger:active {
    transform: scale(0.99);
}

.tb-select-label {
    flex-shrink: 0;
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--text-muted);
}

.tb-select-value {
    flex: 1;
    min-width: 0;
    font-size: 0.9rem;
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tb-select-chevron {
    flex-shrink: 0;
    width: 16px;
    height: 16px;
    color: var(--text-muted);
    transition: transform var(--dur-med) ease;
}

.tb-select-trigger[aria-expanded="true"] .tb-select-chevron {
    transform: rotate(180deg);
}

.tb-select-panel {
    position: absolute;
    z-index: 30;
    top: calc(100% + var(--sp-2));
    left: 0;
    right: 0;
    max-height: 280px;
    overflow-y: auto;
    scrollbar-width: none;
    padding: var(--sp-2);
    border-radius: var(--r-pop);
    background: var(--glass-bg);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--glass-border);
    border-top: 1px solid var(--glass-border-top);
    box-shadow: var(--shadow-pop);
}

.tb-select-panel::-webkit-scrollbar {
    display: none;
}

.tb-select-panel[hidden] {
    display: none;
}

.tb-select-panel.is-above {
    top: auto;
    bottom: calc(100% + var(--sp-2));
}

/* Bug (feedback Genna, punto 4): sotto 480px select.js aggiunge anche
   .tb-sheet al pannello, ma .tb-select-panel e .tb-sheet hanno la stessa
   specificita' (una classe): chi vince per ogni proprieta' dipende
   dall'ordine nel file, e nessuna delle due regole tocca "top" dell'altra.
   .tb-sheet vince su position (diventa fixed) ma il "top: calc(100% +
   8px)" di .tb-select-panel resta: su un elemento fixed e' relativo al
   viewport, quindi il pannello parte sotto lo schermo, e con "bottom: 0"
   di .tb-sheet anche impostato la sua altezza risulta negativa (quindi
   0): il foglio non si vede mai. Selettore composto (specificita' piu'
   alta di entrambe, non dipende dall'ordine) per azzerare "top" e
   confermare fixed/z-index quando i due si sommano. */
.tb-select-panel.tb-sheet {
    position: fixed;
    top: auto;
    z-index: 10031;
}

.tb-select-option {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-2);
    width: 100%;
    min-height: var(--tap);
    padding: 0 var(--sp-3);
    border-radius: var(--r-sm);
    color: var(--text-primary);
    font-size: 0.88rem;
    text-align: left;
}

.tb-select-option.is-active {
    background: rgba(255, 255, 255, 0.08);
}

.tb-select-check {
    flex-shrink: 0;
    width: 16px;
    height: 16px;
    color: var(--accent-orange);
    visibility: hidden;
}

.tb-select-option[aria-selected="true"] {
    color: var(--accent-orange);
    font-weight: 700;
}

.tb-select-option[aria-selected="true"] .tb-select-check {
    visibility: visible;
}

@media (hover: hover) and (pointer: fine) {
    .tb-select-trigger:hover {
        border-color: var(--accent-orange);
    }
    .tb-select-option:hover {
        background: rgba(255, 255, 255, 0.06);
    }
}

/* Sotto 480px il pannello diventa un .tb-sheet (select.js aggiunge anche
   quella classe): niente altro da fare qui, le regole .tb-sheet sotto
   bastano perche' .tb-select-panel non porta posizionamento fisso proprio
   a quella larghezza (lo sovrascrive .tb-sheet). */

/* --- RANGE (spec 03 §2/§7): contenitore 44px, traccia 4px sottile,
   riempimento arancione via --tb-fill, thumb 26px "vetro". --tb-fill lo
   aggiorna lo strumento a ogni "input" con
   el.style.setProperty('--tb-fill', 'NN%') (mai style="" nel markup: CSSOM
   via JS, stesso principio di .met-wheel/.acc-needle). Fallback 50% qui
   sotto finche' l'JS non gira. */
.tb-range {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    height: var(--tap);
    background: transparent;
    --tb-fill: 50%;
}

.tb-range::-webkit-slider-runnable-track {
    height: 4px;
    border-radius: var(--r-pill);
    background: linear-gradient(to right, var(--accent-orange) 0%, var(--accent-orange) var(--tb-fill), var(--track) var(--tb-fill), var(--track) 100%);
}

.tb-range::-moz-range-track {
    height: 4px;
    border-radius: var(--r-pill);
    background: var(--track);
}

.tb-range::-moz-range-progress {
    height: 4px;
    border-radius: var(--r-pill);
    background: var(--accent-orange);
}

.tb-range::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 26px;
    height: 26px;
    margin-top: -11px;
    border-radius: 50%;
    background: var(--glass-bg);
    border: 1px solid var(--glass-border-top);
    box-shadow: var(--shadow-pop);
    cursor: pointer;
}

.tb-range::-moz-range-thumb {
    width: 26px;
    height: 26px;
    border: 1px solid var(--glass-border-top);
    border-radius: 50%;
    background: var(--glass-bg);
    box-shadow: var(--shadow-pop);
    cursor: pointer;
}

.tb-range-value {
    flex-shrink: 0;
    min-width: 2.5em;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    font-size: 0.85rem;
    text-align: right;
}

/* --- TOGGLE (spec 03 §2): interruttore button[role=switch], per stati
   on/off che non sono una scelta fra piu' opzioni (a differenza di
   .tb-segment). aria-checked pilota lo stato, come i radio. */
.tb-toggle {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    width: 52px;
    height: 32px;
    padding: 2px;
    border-radius: var(--r-pill);
    background: var(--track);
    transition: background var(--dur-med) ease;
}

.tb-toggle-thumb {
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: var(--glass-bg);
    border: 1px solid var(--glass-border-top);
    box-shadow: var(--shadow-pop);
    transition: transform var(--dur-med) var(--ease-cinematic);
}

.tb-toggle[aria-checked="true"] {
    background: var(--accent-orange);
}

.tb-toggle[aria-checked="true"] .tb-toggle-thumb {
    transform: translateX(22px);
}

/* --- NUMERO (spec 03 §2): campo inline (es. il BPM editabile). Invio o
   uscita confermano, Esc annulla, i valori fuori scala rientrano nei
   limiti: comportamento dello strumento, qui solo l'aspetto. */
.tb-number {
    width: 100%;
    min-height: var(--tap);
    padding: 0 var(--sp-3);
    border: 1px solid var(--glass-border);
    border-radius: var(--r-md);
    background: var(--surface-1);
    color: var(--text-primary);
    font-family: var(--font-title);
    font-variant-numeric: tabular-nums;
    text-align: center;
    font-size: 1.6rem;
    font-weight: 700;
}

.tb-number:focus-visible {
    border-color: var(--accent-orange);
}

.tb-number::-webkit-inner-spin-button,
.tb-number::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

.tb-number[hidden] {
    display: none !important;
}

/* --- INFO + FOGLIO (spec 03 §2/§9: vedi il CONTRATTO sheet.js in cima al
   file). Il glifo "i" e' testo (non un'icona): come "X" per chiudere, e'
   una convenzione universale, non un simbolo decorativo. */
.tb-info {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--tap);
    height: var(--tap);
    flex-shrink: 0;
    border-radius: var(--r-pill);
}

.tb-info-glyph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    border: 1px solid var(--glass-border);
    background: var(--surface-1);
    font-family: var(--font-title);
    font-style: italic;
    font-weight: 700;
    font-size: 0.85rem;
    color: var(--text-muted);
    transition: border-color var(--dur-fast) ease, color var(--dur-fast) ease;
}

.tb-info[aria-expanded="true"] .tb-info-glyph,
.tb-info:active .tb-info-glyph {
    border-color: var(--accent-orange);
    color: var(--accent-orange);
}

.tb-sheet-scrim {
    position: fixed;
    inset: 0;
    z-index: 10030;
    background: rgba(0, 0, 0, 0.55);
    opacity: 0;
    transition: opacity var(--dur-med) ease;
}

.tb-sheet-scrim[hidden] {
    display: none;
}

.tb-sheet-scrim.is-open {
    opacity: 1;
}

.tb-sheet {
    position: fixed;
    z-index: 10031;
    left: 0;
    right: 0;
    bottom: 0;
    box-sizing: border-box;
    max-height: min(70vh, 520px);
    padding: var(--sp-4) var(--sp-4) calc(var(--sp-4) + var(--tb-safe-b));
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    background: var(--glass-bg);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--glass-border);
    border-top: 1px solid var(--glass-border-top);
    box-shadow: var(--shadow-pop);
    overflow-y: auto;
    scrollbar-width: none;
    transform: translateY(100%);
    transition: transform var(--dur-med) var(--ease-cinematic);
}

.tb-sheet::-webkit-scrollbar {
    display: none;
}

.tb-sheet[hidden] {
    display: none;
}

.tb-sheet.is-open {
    transform: translateY(0);
}

/* Un solo componente su ogni larghezza (spec 03 criterio 5, decisione
   post-lancio strumento 3): niente piu' popover separato da 768px, solo
   un limite di larghezza perche' un foglio a schermo intero non stia bene
   su desktop. Scrim scuro e foglio dal basso restano identici ovunque. */
.tb-sheet {
    width: min(480px, 100%);
    margin-left: auto;
    margin-right: auto;
}

.tb-sheet-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    margin-bottom: var(--sp-3);
}

.tb-sheet-head h2 {
    font-family: var(--font-title);
    font-size: 1rem;
    font-weight: 600;
    text-transform: uppercase;
    color: var(--text-primary);
}

.tb-sheet-close {
    flex-shrink: 0;
    width: var(--tap);
    height: var(--tap);
    min-height: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.2rem;
    color: var(--text-muted);
}

.tb-sheet-body {
    font-size: 0.88rem;
    line-height: 1.6;
    color: var(--text-muted);
}

@media (hover: hover) and (pointer: fine) {
    .tb-info:hover .tb-info-glyph,
    .tb-sheet-close:hover {
        color: var(--accent-orange);
    }
}

/* --- Disabilitato: opacita' 0.45, nessun cambio di colore (spec 03 §2). --- */
.tb-btn:disabled,
.tb-btn[aria-disabled="true"],
.tb-segment-btn:disabled,
.tb-select-trigger:disabled,
.tb-range:disabled,
.tb-toggle:disabled,
.tb-toggle[aria-disabled="true"],
.tb-number:disabled,
.tb-info:disabled {
    opacity: 0.45;
    pointer-events: none;
}

/* --- ROTELLA BPM (.tb-wheel*, spec 12 §4/§5) --- */
/* CONTRATTO shared/bpm-control.js: createBpmControl({ wheel, drum, readout,
   input, min, max, value, ticks, classes: { tick, major, dragging },
   onChange }) prende gli elementi gia' pronti nel markup, non li crea.
   Markup atteso (un'istanza per rotella: calcolatore-tempo ne monta due,
   una per scheda "Delay e riverbero" e una per "Sidechain", vedi il
   commento in calcolatore-tempo/index.html):
     <div class="tb-wheel" role="slider" tabindex="0" aria-valuemin="30"
          aria-valuemax="300" aria-valuenow="120" aria-valuetext="120 BPM">
       <div class="tb-wheel-drum" aria-hidden="true"></div>
       <div class="tb-wheel-face" aria-hidden="true"></div>
       <div class="tb-wheel-readout">
         <span class="tb-wheel-value" aria-live="polite" aria-atomic="true">120</span>
         <input type="number" class="tb-wheel-input tb-number" inputmode="numeric" min="30" max="300" step="1" value="120" hidden>
         <span class="tb-wheel-unit">BPM</span>
       </div>
     </div>
   wheel=.tb-wheel, drum=.tb-wheel-drum (vuoto: bpm-control.js vi crea/posa
   le tacche <span> con element.style.transform, mai style="" nel markup,
   stesso principio di .met-wheel), readout=.tb-wheel-value, input=
   .tb-wheel-input; classes di default tick/major/dragging sotto (uguali
   ai nomi usati qui, il chiamante puo' passarne altri). Stessa "grammatica"
   di .met-wheel/.met-tick/.met-wheel-drum in metronomo.css (che RESTA
   invariato: la rotella del metronomo non usa queste classi, e'
   l'estrazione a farla passare per gli stessi parametri, non a
   rinominarla). Piu' compatta di quella del metronomo (spec 12 §4: una
   schermata per scheda a 390x844 con tabella sotto). */
.tb-wheel {
    position: relative;
    width: 100%;
    height: 84px;
    overflow: hidden;
    perspective: 1000px;
    transform-style: preserve-3d;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: grab;
    touch-action: none;
    -webkit-user-select: none;
    user-select: none;
}

.tb-wheel:active,
.tb-wheel.is-dragging {
    cursor: grabbing;
}

.tb-wheel.is-dragging .tb-wheel-tick {
    transition: none;
}

.tb-wheel-drum {
    position: absolute;
    inset: 0;
    transform-style: preserve-3d;
    pointer-events: none;
}

.tb-wheel-tick {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 2px;
    height: 18px;
    margin: -9px 0 0 -1px;
    border-radius: 1px;
    background: var(--text-muted);
    opacity: 0.5;
    transition: transform 0.4s cubic-bezier(0.25, 1, 0.5, 1), opacity 0.4s ease;
    backface-visibility: hidden;
}

.tb-wheel-tick.is-major {
    width: 3px;
    height: 28px;
    margin-left: -1.5px;
    background: var(--text-primary);
    opacity: 0.85;
}

.tb-wheel-face {
    position: absolute;
    inset: 0;
    border-radius: var(--r-lg);
    background: var(--glass-bg);
    border-top: 1px solid var(--glass-border-top);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    box-shadow: var(--glass-shadow);
    -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 18%, #000 82%, transparent 100%);
    mask-image: linear-gradient(90deg, transparent 0%, #000 18%, #000 82%, transparent 100%);
    pointer-events: none;
}

.tb-wheel-readout {
    position: relative;
    z-index: 2;
    display: flex;
    flex-direction: column;
    align-items: center;
    pointer-events: none;
}

.tb-wheel-value,
.tb-wheel-input {
    pointer-events: auto;
}

.tb-wheel-value {
    font-family: var(--font-title);
    font-size: 2.6rem;
    font-weight: 700;
    line-height: 1.1;
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
    cursor: text;
}

/* Sovrascrive il campo "scatola" di .tb-number: qui e' il numero enorme
   sopra la rotella, resta a sottolineatura (feedback Genna: il bordo
   pieno e i 16px di .tb-number sono per i campi numerici normali, non
   per questa lettura). border:none prima di border-bottom azzera anche i
   lati che .tb-number ha impostato. */
.tb-wheel-input {
    display: block;
    width: 7ch;
    padding: 0;
    border: none;
    border-bottom: 2px solid var(--accent-orange);
    border-radius: 0;
    background: transparent;
    color: var(--text-primary);
    font-family: var(--font-title);
    font-size: 2.6rem;
    font-weight: 700;
    line-height: 1.1;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

.tb-wheel-input[hidden] {
    display: none !important;
}

.tb-wheel-input::-webkit-inner-spin-button,
.tb-wheel-input::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

.tb-wheel-unit {
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--text-muted);
}

@media (prefers-reduced-motion: reduce) {
    .tb-wheel-tick {
        transition: none;
    }
}

/* --- STATO (idle/busy/ok/error/denied) --- */
/* Vuoto (nessun messaggio): non deve occupare spazio. */
.tb-status:empty {
    display: none;
}

.tb-status {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    font-size: 0.82rem;
    font-weight: 500;
    color: var(--text-muted);
    min-height: 24px;
}

.tb-status::before {
    content: '';
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: currentColor;
    flex-shrink: 0;
}

.tb-status--idle { color: var(--text-muted); }
.tb-status--busy { color: var(--text-primary); }
.tb-status--ok { color: #4dd97a; }
.tb-status--error { color: #e8604c; }
.tb-status--denied { color: #e8604c; }

@media (prefers-reduced-motion: no-preference) {
    .tb-status--busy::before {
        animation: tb-pulse 1.2s ease-in-out infinite;
    }
}

@keyframes tb-pulse {
    0%, 100% { opacity: 0.4; }
    50% { opacity: 1; }
}

/* --- TOAST --- */
.tb-toast-region {
    position: fixed;
    left: 50%;
    bottom: calc(96px + var(--tb-safe-b));
    transform: translateX(-50%);
    z-index: 10020;
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: min(92vw, 420px);
    pointer-events: none;
}

.tb-toast {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    padding: 14px 18px;
    border-radius: var(--r-md);
    background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(16, 15, 14, 0.85) 100%);
    backdrop-filter: blur(25px) saturate(180%);
    -webkit-backdrop-filter: blur(25px) saturate(180%);
    border: 1px solid rgba(255, 255, 255, 0.15);
    box-shadow: 0 16px 40px rgba(0, 0, 0, 0.6);
    color: var(--text-primary);
    font-size: 0.85rem;
    opacity: 0;
    transform: translateY(12px);
    transition: opacity 0.3s ease, transform 0.3s ease;
    pointer-events: auto;
}

.tb-toast.is-visible {
    opacity: 1;
    transform: translateY(0);
}

.tb-toast-action {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
    padding: 6px 16px;
    border-radius: var(--r-pill);
    border: 1px solid var(--accent-orange);
    color: var(--accent-orange);
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 1px;
}

/* La "x" di chiusura (shared/ui.js, toast senza scadenza, spec 18 §7.11):
   stesso bottone di .tb-toast-action, ma il glifo e' stretto e da solo
   resterebbe sotto i 44px di larghezza. */
.tb-toast-close {
    justify-content: center;
    min-width: var(--tap);
    font-size: 1.1rem;
}

/* --- CONSENSO (microfono, mic.js) --- */
.tb-consent {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 14px;
    padding: 18px 20px;
    border-radius: var(--r-lg);
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(18px);
    -webkit-backdrop-filter: blur(18px);
}

.tb-consent p {
    font-size: 0.85rem;
    line-height: 1.6;
    color: var(--text-muted);
}

.tb-consent__title {
    font-family: var(--font-title);
    font-size: 1rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.01em;
    color: var(--text-primary);
}

.tb-consent__text {
    font-size: 0.85rem;
    line-height: 1.6;
    color: var(--text-muted);
}

.tb-consent--denied .tb-consent__text,
.tb-consent--unavailable .tb-consent__text {
    color: #e8604c;
}

.tb-consent--compact {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
}

.tb-consent[hidden] {
    display: none !important;
}

/* --- STATO VUOTO / ERRORE --- */
.tb-empty {
    text-align: center;
    padding: 60px 20px;
    color: var(--text-muted);
    font-size: 0.9rem;
}

/* --- SINCRONIZZAZIONE: riquadro del codice (spec 17 §5, spec 18 §4/§5) ---
   Nata in Penna (#pen-sync) come .pen-sync-code, ora condivisa perche' la
   sincronizzazione e' di tutta la Toolbox (Impostazioni, #imp-sync-code e
   #imp-sync-code-on). Font-family del testo (non mono, e' un codice da
   leggere ad alta voce, non da allineare), dimensione generosa e interlinea
   ampia per le sei parole su piu' righe a 390px, user-select:all per
   prenderlo con un tocco, overflow-wrap:anywhere perche' una parola lunga
   non sbordi. Vetro coerente con --glass-* (stessa ricetta di
   .tb-settings/.tb-tile), raggio --r-md. */
.tb-sync-code {
    margin: 0;
    padding: var(--sp-4);
    border-radius: var(--r-md);
    background: var(--glass-bg);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border: 1px solid var(--glass-border);
    border-top: 1px solid var(--glass-border-top);
    font-family: var(--font-text);
    font-size: 1.15rem;
    line-height: 1.8;
    letter-spacing: 0.02em;
    text-align: center;
    color: var(--text-primary);
    user-select: all;
    overflow-wrap: anywhere;
    word-break: break-word;
}

.tb-sync-code[hidden] {
    display: none;
}

/* --- AIUTO IN-APP UNIFORME (spec 18 §6) ---
   shared/aiuto.js (implementer) monta i tre pezzi qui sotto da
   <slug>/aiuto.js (default export { it:[{t,d}...], en:[...], hint:{it,en} },
   builder); qui solo il markup/CSS, il mount e' di mountAiuto({slug}).
   Contratto atteso (letto nel sorgente di shared/aiuto.js):

   1. button.tb-help.tb-btn.tb-btn--ghost (etichetta visibile,
      svg><use href="#tb-icon-help"> + span[data-i18n=help-open]).
      mountAiuto() gli imposta da solo aria-haspopup/aria-controls/
      aria-expanded e al tocco costruisce un .tb-sheet unico per pagina
      (#tb-help-sheet, riusato ad ogni apertura) con dentro ul.tb-help-list
      > li.tb-help-item > h3.tb-help-item-title + p.tb-help-item-text, una
      voce per elemento di aiuto.js, e lo apre con openSheet() di sheet.js.
      Due posizioni secondo le pagine: subito dopo .tb-page-header (nessuna
      barra propria) con anche la classe .tb-help--center, che lo centra da
      solo senza dipendere dal text-align del genitore; oppure dentro la
      barra dello strumento quando c'e' (es. .pen-editor-bar), bottone come
      gli altri della riga, senza il modificatore.

   2. div.tb-hint#tb-hint[hidden] subito sotto la barra/il titolo: riga non
      bloccante, mountAiuto() ci scrive dentro p.tb-hint-text (il testo di
      hint.it/hint.en) + button.tb-hint-close (×, aria-label). Mostrata al
      primo avvio di ogni strumento (ricordato per slug in tt.<slug>.hint),
      si chiude anche da sola dopo la prima interazione con lo strumento,
      non solo dal ×. Non e' un overlay: scorre col contenuto, non copre
      nulla, non e' fissa.

   3. .tb-tip: popover ancorato a un bottone icona con [data-tip="<chiave
      i18n>"] (regola spec 18 §6: ogni pulsante icona ha etichetta visibile
      o tb-tip). mountAiuto() crea un solo div#tb-tip.tb-tip per pagina, ci
      mette t(chiave) dentro, lo posiziona con left/top inline (stessa
      logica di place() in sheet.js) e alterna [hidden]/.is-open; .is-above
      segna solo che e' stato ancorato sopra il bottone invece che sotto
      (spazio insufficiente in basso), nessuna geometria diversa richiesta
      qui. Mostrato al tocco lungo, al fuoco da tastiera o al mouse fermo
      sopra; chiuso da un tocco fuori, da Esc, scorrendo, uscendo col
      fuoco/mouse. */
.tb-help {
    flex-shrink: 0;
}

/* Standalone dopo .tb-page-header: si centra da solo (display:flex +
   width:fit-content, non inline-flex, cosi' margin:auto funziona) senza
   contare sul text-align:center del genitore, che qui non c'e' piu'. */
.tb-help--center {
    display: flex;
    width: fit-content;
    margin: var(--sp-3) auto 0;
}

.tb-hint {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-3);
    max-width: 640px;
    margin: 0 auto var(--sp-4);
    padding: var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-4);
    border-radius: var(--r-pill);
    border: 1px solid var(--glass-border);
    background: rgba(255, 255, 255, 0.04);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    text-align: left;
}

.tb-hint[hidden] {
    display: none;
}

.tb-hint-text {
    flex: 1 1 auto;
    font-size: 0.82rem;
    line-height: 1.5;
    color: var(--text-muted);
}

.tb-hint-close {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--tap);
    height: var(--tap);
    min-height: 0;
    font-size: 1.2rem;
    color: var(--text-muted);
}

@media (hover: hover) and (pointer: fine) {
    .tb-hint-close:hover {
        color: var(--accent-orange);
    }
}

/* Foglio "Come funziona" (mountAiuto/buildSheet): riusa .tb-sheet/
   .tb-sheet-scrim/.tb-sheet-head/.tb-sheet-body cosi' come sono (stessa X,
   stesso scrim, stessa trappola del fuoco), solo il corpo cambia forma
   (lista invece di un paragrafo). */
.tb-help-list {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
}

.tb-help-item + .tb-help-item {
    padding-top: var(--sp-4);
    border-top: 1px solid var(--glass-border);
}

.tb-help-item-title {
    margin-bottom: var(--sp-1);
    font-family: var(--font-title);
    font-size: 0.85rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--text-primary);
}

.tb-help-item-text {
    font-size: 0.85rem;
    line-height: 1.6;
    color: var(--text-muted);
}

/* Popover ancorato dal JS (mountAiuto/placeTip: position:fixed + left/top
   inline, stessa idea di place() in sheet.js); qui solo l'aspetto e lo
   stato mostrato/nascosto. .is-above: ancorato sopra il bottone invece che
   sotto, stessa classe con lo stesso significato di .tb-select-panel. */
.tb-tip {
    z-index: 500;
    padding: var(--sp-2) var(--sp-3);
    border-radius: var(--r-pop);
    border: 1px solid var(--glass-border);
    border-top: 1px solid var(--glass-border-top);
    background: var(--glass-bg);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    box-shadow: var(--shadow-pop);
    font-size: 0.78rem;
    line-height: 1.5;
    color: var(--text-primary);
    pointer-events: none;
    opacity: 0;
    transition: opacity var(--dur-fast) ease;
}

.tb-tip[hidden] {
    display: none;
}

.tb-tip.is-open {
    opacity: 1;
}

@media (prefers-reduced-motion: no-preference) {
    .tb-hint {
        animation: tb-hint-in var(--dur-fast) var(--ease-cinematic);
    }
}

@keyframes tb-hint-in {
    from { opacity: 0; transform: translateY(-4px); }
    to { opacity: 1; transform: translateY(0); }
}
