/* ============================================================
   QreaLAB V01 — objects.css
   ------------------------------------------------------------
   LES OBJETS : le Q, les modules, Q-ARCHIVE, les anomalies, les
   présences abstraites.

   ── Direction V02 ──────────────────────────────────────────
   Une installation expérimentale dans un espace abstrait.
   Le Q est le point focal : blanc froid au centre, cyan et violet
   électriques autour du noyau. Les modules sont des objets
   expérimentaux légers — anneaux, disques, prismes, membranes —
   jamais des machines d'entrepôt.

   ── Le mécanisme de profondeur ─────────────────────────────
   Chaque objet porte `--d` (0 proche → 1 lointain), écrit par le
   JS. Tout le relief 2.5D en découle, ici et nulle part ailleurs :
     échelle   → un objet lointain est plus petit
     opacité   → il se fond dans le noir
     flou      → il perd ses arêtes
     saturation→ il perd sa couleur avant de perdre sa forme

   ── Grammaire visuelle commune ─────────────────────────────
   Géométrie légère, éléments translucides, un seul point de
   lumière vive par objet. Les couleurs lumineuses sont rares.
   ============================================================ */

/* ══════════════════════════════════════════════════════════
   1. SOCLE COMMUN
   ══════════════════════════════════════════════════════════ */

.obj {
    position: absolute;
    /* left/top désignent le POINT D'APPUI AU SOL de l'objet : on
       translate donc de -50% en x et -100% en y. Poser les objets par
       leur base (et non par leur centre) est ce qui les fait
       appartenir à l'espace. */
    transform:
        translate(-50%, -100%)
        scale(calc(
            var(--size, 1) *
            (var(--depth-scale-near) - (var(--depth-scale-near) - var(--depth-scale-far)) * var(--d, 0))
        ));
    transform-origin: 50% 100%;

    /* Profondeur : opacité, flou, désaturation. */
    opacity: calc(1 - (1 - var(--depth-fade-far)) * var(--d, 0));
    filter:
        blur(calc(var(--depth-blur-far) * var(--d, 0)))
        saturate(calc(1 - 0.55 * var(--d, 0)));

    transition:
        opacity var(--t-slow) var(--ease-out),
        filter var(--t-slow) var(--ease-out);
}

/* Ombre de contact : ce qui ancre l'objet à son assise.
   Elliptique, car la scène est vue en légère plongée. */
.obj__contact {
    position: absolute;
    left: 50%;
    bottom: -10px;
    width: 132%;
    height: 34px;
    transform: translateX(-50%);
    background: var(--contact-shadow);
    pointer-events: none;
    z-index: -1;
}

/* ══════════════════════════════════════════════════════════
   2. LE Q
   ══════════════════════════════════════════════════════════
   Le point focal absolu. Taille humaine, posé sur sa plateforme,
   et principale source de lumière de la scène.

   Signature : blanc froid au centre, cyan électrique et violet
   électrique vivant autour du noyau. Le logo garde ses couleurs ;
   ce sont les halos qui font du Q une lumière dans la scène.
   ══════════════════════════════════════════════════════════ */

.obj--core {
    width: 280px;
    height: 300px;
    /* Le Q passe devant tout : il est le sujet. */
    z-index: 60 !important;
}

/* Halo central : blanc froid, large et faible. C'est de la
   lumière, pas un contour. */
.core__halo {
    position: absolute;
    left: 50%;
    top: 42%;
    width: 940px;
    height: 940px;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: radial-gradient(50% 50% at 50% 50%,
        rgba(243, 249, 255, calc(0.13 + var(--world-power, 0) * 0.05)) 0%,
        rgba(190, 215, 235, 0.055) 30%,
        rgba(120, 150, 180, 0.02) 56%,
        rgba(0, 0, 0, 0) 100%);
    pointer-events: none;
    animation: core-breathe 7s var(--ease-in-out) infinite;
}

/* Halo cyan : décalé vers le bas gauche, il baigne la plateforme
   et le sol proche. */
.core__halo--cyan {
    width: 680px;
    height: 680px;
    transform: translate(-64%, -36%);
    background: radial-gradient(50% 50% at 50% 50%,
        rgba(95, 232, 255, calc(0.17 + var(--world-power, 0) * 0.06)) 0%,
        rgba(95, 232, 255, 0.055) 38%,
        rgba(0, 0, 0, 0) 100%);
    animation: core-breathe-cyan 9s var(--ease-in-out) infinite;
}

/* Halo violet : décalé vers le haut droit, il colore l'arrière
   du noyau. Plus saturé, plus mystérieux. */
.core__halo--violet {
    width: 680px;
    height: 680px;
    transform: translate(-36%, -62%);
    background: radial-gradient(50% 50% at 50% 50%,
        rgba(169, 112, 255, 0.14) 0%,
        rgba(169, 112, 255, 0.05) 38%,
        rgba(0, 0, 0, 0) 100%);
    animation: core-breathe-violet 11s var(--ease-in-out) infinite;
}

/* Anneaux gyroscopiques : aplaties comme l'assise, en pointillés.
   Leur rotation lit comme un flux le long de l'anneau, jamais
   comme une toupie. */
.core__ring {
    position: absolute;
    left: 50%;
    top: 45%;
    border-radius: 50%;
    border: 1px dashed rgba(95, 232, 255, 0.2);
    transform: translate(-50%, -50%) rotateZ(0deg);
    pointer-events: none;
}

.core__ring--outer {
    width: 330px;
    height: 128px;
    border-color: rgba(95, 232, 255, 0.15);
    animation: ring-spin 48s linear infinite;
}

.core__ring--inner {
    width: 248px;
    height: 94px;
    border-color: rgba(169, 112, 255, 0.22);
    animation: ring-spin 32s linear infinite reverse;
}

/* La plateforme : ce qui pose le Q dans l'espace. */
.core__platform {
    position: absolute;
    left: 50%;
    bottom: -8px;
    width: 380px;
    height: 100px;
    transform: translateX(-50%);
    border-radius: 50%;
    background: radial-gradient(50% 50% at 50% 38%,
        #1a2230 0%,
        #0d1219 56%,
        #05070b 100%);
    box-shadow:
        inset 0 1px 0 rgba(185, 201, 217, 0.08),
        0 18px 44px rgba(0, 0, 0, 0.62);
    pointer-events: none;
}

/* Le liseré de la plateforme : alimenté par le monde. */
.core__platform-rim {
    position: absolute;
    inset: -1px;
    border-radius: 50%;
    border: 1px solid rgba(95, 232, 255, calc(0.22 + var(--world-power, 0) * 0.2));
    box-shadow:
        0 0 22px rgba(95, 232, 255, 0.1),
        inset 0 0 22px rgba(95, 232, 255, 0.05);
}

/* Le cœur. Le logo garde ses couleurs — c'est l'identité. Les
   ombres portées ajoutent la lumière du Q autour de lui. */
.core__logo {
    position: absolute;
    left: 50%;
    top: 44%;
    width: 192px;
    height: auto;
    transform: translate(-50%, -50%);
    /* Le même langage lumineux que le Q du Gatekeeper : un cœur blanc
       froid entouré de cyan et de violet, net et vivant. */
    filter:
        brightness(1.14) saturate(1.08)
        drop-shadow(0 0 14px rgba(220, 250, 255, 0.7))
        drop-shadow(0 0 26px rgba(95, 232, 255, 0.65))
        drop-shadow(0 0 72px rgba(169, 112, 255, 0.45));
    pointer-events: none;
    animation: core-glow 5.5s var(--ease-in-out) infinite;
}

/* Marqueur d'anomalie : n'apparaît que lorsque le Q a un fragment
   à délivrer. C'est le violet le plus saturé de la scène. */
.core__anomaly {
    position: absolute;
    left: 50%;
    top: 44%;
    width: 236px;
    height: 236px;
    transform: translate(-50%, -50%) scale(0.8);
    border-radius: 50%;
    background: radial-gradient(50% 50% at 50% 50%,
        rgba(162, 107, 255, 0.26) 0%,
        rgba(162, 107, 255, 0.07) 46%,
        rgba(162, 107, 255, 0) 100%);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--t-slow) var(--ease-out), transform var(--t-slow) var(--ease-out);
}

.obj--core.has-anomaly .core__anomaly {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
    animation: anomaly-pulse 3.4s var(--ease-in-out) infinite;
}

/* Q-ARCHIVE déverrouillée : le Q devient une porte d'entrée. Lumière
   plus forte, plateforme qui passe au violet, activité lumineuse
   nouvelle. C'est le seul moment où le Q sort de sa retenue. */
.obj--core.is-unlocked .core__halo--violet {
    background: radial-gradient(50% 50% at 50% 50%,
        rgba(169, 112, 255, 0.22) 0%,
        rgba(169, 112, 255, 0.08) 38%,
        rgba(0, 0, 0, 0) 100%);
    animation: core-breathe-violet 5.6s var(--ease-in-out) infinite;
}

.obj--core.is-unlocked .core__platform-rim {
    border-color: rgba(169, 112, 255, 0.5);
    box-shadow:
        0 0 26px rgba(169, 112, 255, 0.32),
        inset 0 0 26px rgba(169, 112, 255, 0.14);
}

.obj--core.is-unlocked .core__ring--inner {
    border-color: rgba(169, 112, 255, 0.5);
}

.obj--core.is-unlocked .core__anomaly {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
    animation: anomaly-pulse 2.4s var(--ease-in-out) infinite;
}

/* Survol : le Q réagit franchement, c'est le sujet de la scène. */
.obj--core:hover .core__logo,
.obj--core:focus-visible .core__logo {
    filter:
        brightness(1.22) saturate(1.12)
        drop-shadow(0 0 20px rgba(200, 248, 255, 0.85))
        drop-shadow(0 0 40px rgba(95, 232, 255, 0.8))
        drop-shadow(0 0 96px rgba(169, 112, 255, 0.55));
}

.obj--core:hover .core__ring--outer,
.obj--core:focus-visible .core__ring--outer {
    border-color: rgba(95, 232, 255, 0.36);
}

.obj--core:hover .core__ring--inner,
.obj--core:focus-visible .core__ring--inner {
    border-color: rgba(169, 112, 255, 0.4);
}

/* Seul le halo central reçoit la surtension : les halos de couleur
   ont leur propre translation, que l'animation ne doit pas écraser. */
.obj--core.is-surging .core__halo:not(.core__halo--cyan):not(.core__halo--violet) {
    animation: core-surge 1.4s var(--ease-out);
}

@keyframes core-breathe {
    0%, 100% { opacity: 0.86; transform: translate(-50%, -50%) scale(1); }
    50% { opacity: 1; transform: translate(-50%, -50%) scale(1.045); }
}

@keyframes core-breathe-cyan {
    0%, 100% { opacity: 0.8; transform: translate(-64%, -36%) scale(1); }
    50% { opacity: 1; transform: translate(-64%, -36%) scale(1.06); }
}

@keyframes core-breathe-violet {
    0%, 100% { opacity: 0.75; transform: translate(-36%, -62%) scale(1); }
    50% { opacity: 1; transform: translate(-36%, -62%) scale(1.07); }
}

@keyframes core-glow {
    0%, 100% { opacity: 0.94; }
    50% { opacity: 1; }
}

@keyframes ring-spin {
    from { transform: translate(-50%, -50%) rotateZ(0deg); }
    to { transform: translate(-50%, -50%) rotateZ(360deg); }
}

@keyframes core-surge {
    0% { transform: translate(-50%, -50%) scale(1); opacity: 0.9; }
    22% { transform: translate(-50%, -50%) scale(1.3); opacity: 1; }
    100% { transform: translate(-50%, -50%) scale(1); opacity: 0.9; }
}

@keyframes anomaly-pulse {
    0%, 100% { opacity: 0.55; }
    50% { opacity: 1; }
}

/* ══════════════════════════════════════════════════════════
    3. MODULES
    ══════════════════════════════════════════════════════════
    « Instrument » : chaque module est un objet autonome, lisible
    dès l'arrivée. Une grammaire commune — enveloppe + cadre +
    bandeau (voyant / identifiant / signal) + écran intérieur +
    pied d'identification — et un contenu par forme : l'écran
    raconte l'expérience de manière abstraite, jamais littérale.
    ══════════════════════════════════════════════════════════ */

.obj--module {
    width: 230px;
    height: 248px;
}

/* Lumière locale : n'existe que si le module est alimenté. */
.module__glow {
    position: absolute;
    left: 50%;
    bottom: -26px;
    width: 380px;
    height: 380px;
    transform: translateX(-50%);
    border-radius: 50%;
    background: radial-gradient(50% 50% at 50% 50%,
        rgba(88, 210, 238, 0.13) 0%,
        rgba(88, 210, 238, 0.04) 40%,
        rgba(0, 0, 0, 0) 100%);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--t-slow) var(--ease-out);
}

/* Châssis : l'enveloppe qui donne une silhouette et un poids. */
.module__chassis {
    position: absolute;
    left: 50%;
    bottom: 2px;
    width: 216px;
    height: 224px;
    transform: translateX(-50%);
    border: 2px solid var(--chassis-border);
    border-radius: 16px;
    background: linear-gradient(165deg,
        var(--chassis-face) 0%,
        rgba(11, 15, 23, 0.84) 54%,
        rgba(3, 5, 9, 0.92) 100%);
    box-shadow:
        inset 0 0 0 1px rgba(0, 0, 0, 0.42),
        inset 0 1px 0 var(--chassis-raised),
        var(--shadow-md);
    transition:
        border-color var(--t-normal) var(--ease-out),
        box-shadow var(--t-normal) var(--ease-out);
}

/* Cadre intérieur : une seconde arête, le relief de l'enveloppe. */
.module__frame {
    position: absolute;
    inset: 6px;
    border: 1px solid var(--line-subtle);
    border-radius: 11px;
    pointer-events: none;
}

/* Coins d'équerre : détails techniques, jamais décoratifs. */
.module__corner {
    position: absolute;
    width: 10px;
    height: 10px;
    pointer-events: none;
}

.module__corner--tl { top: 6px; left: 6px; border-top: 2px solid var(--steel-300); border-left: 2px solid var(--steel-300); border-top-left-radius: 5px; }
.module__corner--tr { top: 6px; right: 6px; border-top: 2px solid var(--steel-300); border-right: 2px solid var(--steel-300); border-top-right-radius: 5px; }
.module__corner--bl { bottom: 6px; left: 6px; border-bottom: 2px solid var(--steel-300); border-left: 2px solid var(--steel-300); border-bottom-left-radius: 5px; }
.module__corner--br { bottom: 6px; right: 6px; border-bottom: 2px solid var(--steel-300); border-right: 2px solid var(--steel-300); border-bottom-right-radius: 5px; }

/* Bandeau supérieur : voyant + identifiant + jauge de signal. */
.module__header {
    position: absolute;
    left: 14px;
    right: 14px;
    top: 12px;
    height: 18px;
    display: flex;
    align-items: center;
    gap: 7px;
    pointer-events: none;
}

/* Voyant : le point de lumière vive du module. */
.module__lamp {
    flex: none;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--signal-dead);
    transition: background var(--t-normal) var(--ease-out),
                box-shadow var(--t-normal) var(--ease-out);
}

.module__id {
    font-size: 9px;
    letter-spacing: var(--ls-wide);
    color: var(--text-dim);
    transition: color var(--t-normal) var(--ease-out);
}

/* Jauge de signal : 5 barres, l'état s'y lit aussi. */
.module__signal {
    margin-left: auto;
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: 9px;
}

.module__signal-bar {
    width: 3px;
    height: 100%;
    background: rgba(180, 205, 230, 0.12);
    transition: background var(--t-normal) var(--ease-out);
}

/* Écran : la fenêtre noire qui contient l'abstraction. */
.module__screen {
    position: absolute;
    left: 12px;
    right: 12px;
    top: 38px;
    height: 116px;
    border: 1px solid var(--screen-border);
    border-radius: 8px;
    background: var(--screen-face);
    overflow: hidden;
    /* Encre du dessin : dim quand éteint, teintée quand alimenté. */
    --ink: rgba(159, 179, 200, 0.22);
    transition: border-color var(--t-normal) var(--ease-out);
}

.module__screen svg {
    display: block;
    width: 100%;
    height: 100%;
    opacity: 0;
    transition: opacity var(--t-slow) var(--ease-out);
}

.module__screen svg * {
    vector-effect: non-scaling-stroke;
}

/* Traits du dessin. */
.module__screen .e__line,
.module__screen .e__cell,
.module__screen .e__card,
.module__screen .e-escape__speed line,
.module__screen .e-escape__jump,
.module__screen .e-escape__trail,
.module__screen .e-lattice__link,
.module__screen .e-wave__sine,
.module__screen .e-memory__sym {
    fill: none;
    stroke: var(--ink);
    stroke-width: 1.2;
}

/* Ligne de défilement du parcours (404 ESCAPE). */
.module__screen .e-escape__motion {
    fill: none;
    stroke: var(--ink);
    stroke-width: 1;
    stroke-dasharray: 14 12;
}

/* Surfaces pleines (obstacles, blocs, cartes révélées). */
.module__screen .e__fill {
    fill: var(--ink);
    stroke: var(--ink);
    stroke-width: 1.2;
}

/* Nœuds de constellation. */
.module__screen .e-lattice__node {
    fill: var(--ink);
    stroke: none;
}

/* Le curseur du Q : le seul point vif de l'écran. */
.module__screen .e-escape__dot {
    fill: var(--acc);
    stroke: none;
    filter: drop-shadow(0 0 4px var(--acc-glow, rgba(95, 232, 255, 0.8)));
}

/* Carte MEMORY : une face sombre, un liseré. */
.module__screen .e__card {
    fill: rgba(2, 4, 7, 0.55);
}

/* Grille de mesure (module-04) : discrète, elle cadre le signal. */
.module__screen .e-wave__grid {
    fill: none;
    stroke: var(--ink);
    stroke-width: 0.6;
    opacity: 0.32;
}

/* Onde secondaire : plus faible, elle donne de la profondeur. */
.module__screen .e-wave__ghost {
    fill: none;
    stroke: var(--ink);
    stroke-width: 0.8;
    opacity: 0.42;
}

/* Balayage : la lecture du signal en cours. */
.module__screen .e-wave__scan {
    fill: none;
    stroke: var(--acc);
    stroke-width: 0.8;
    opacity: 0.4;
    filter: drop-shadow(0 0 4px var(--acc-glow, rgba(95, 232, 255, 0.6)));
}

/* Nœud central (module-05) : le point actif de la constellation. */
.module__screen .e-lattice__core {
    fill: var(--acc);
    stroke: none;
    filter: drop-shadow(0 0 4px var(--acc-glow, rgba(95, 232, 255, 0.8)));
}

/* Pied : identification (titre + état) et code de fonction. */
.module__footer {
    position: absolute;
    left: 14px;
    right: 14px;
    bottom: 10px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    border-top: 1px solid var(--chassis-border);
    pointer-events: none;
    opacity: calc(1 - var(--d, 0) * 0.8);
    transition: opacity var(--t-slow) var(--ease-out);
}

.module__plate {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
}

.module__title {
    color: var(--text-secondary);
    transition: color var(--t-normal) var(--ease-out),
                text-shadow var(--t-normal) var(--ease-out);
}

.module__subtitle {
    font-size: 8px;
    letter-spacing: var(--ls-label);
    color: var(--text-dim);
    transition: color var(--t-normal) var(--ease-out);
}

/* Code de fonction : un mot technique discret à droite. */
.module__tag {
    flex: none;
    font-size: 9px;
    letter-spacing: var(--ls-wide);
    color: var(--text-dim);
    transition: color var(--t-normal) var(--ease-out);
}

/* ══════════════════════════════════════════════════════════
    4. ÉTATS DES MODULES
    ----------------------------------------------------------
    L'état se lit dans la forme autant que dans la lumière :
    écran éteint (verrouillé) → contenu visible (alimenté) →
    nom révélé (exploré) → lumière froide et calme (stabilisé).
    Le voyant, la jauge de signal et le contenu de l'écran
    évoluent avec l'état. Chaque module porte sa signature
    (--acc, --acc-hot, --acc-dim, --acc-glow) posée par le JS.
    ══════════════════════════════════════════════════════════ */

/* INCONNU : enveloppe identifiable, écran noir, sans signal. */
.obj--module[data-state="UNKNOWN"] {
    opacity: calc((1 - (1 - var(--depth-fade-far)) * var(--d, 0)) * 0.5);
    filter: blur(calc(var(--depth-blur-far) * var(--d, 0))) saturate(0.5);
}

/* DORMANT : le châssis est lisible, l'écran reste éteint. */
.obj--module[data-state="DORMANT"] {
    opacity: calc((1 - (1 - var(--depth-fade-far)) * var(--d, 0)) * 0.72);
}

/* ── Alimenté : le contenu de l'écran apparaît ───────────── */

.obj--module[data-state="AVAILABLE"] .module__glow,
.obj--module[data-state="EXPLORED"] .module__glow,
.obj--module[data-state="COMPLETE"] .module__glow {
    opacity: 1;
}

.obj--module[data-state="AVAILABLE"] .module__glow,
.obj--module[data-state="EXPLORED"] .module__glow {
    background: radial-gradient(50% 50% at 50% 50%,
        var(--acc-glow, rgba(88, 210, 238, 0.16)) 0%,
        rgba(0, 0, 0, 0) 100%);
}

/* Châssis en tension : bordure colorée + lumière portée. */
.obj--module[data-state="AVAILABLE"] .module__chassis,
.obj--module[data-state="EXPLORED"] .module__chassis,
.obj--module[data-state="COMPLETE"] .module__chassis {
    border-color: var(--acc, var(--signal));
    box-shadow:
        inset 0 0 0 1px rgba(0, 0, 0, 0.42),
        inset 0 1px 0 rgba(200, 225, 245, 0.2),
        0 0 22px var(--acc-glow, rgba(88, 210, 238, 0.12)),
        var(--shadow-lg);
}

/* Écran allumé : l'encre prend la signature du module. */
.obj--module[data-state="AVAILABLE"] .module__screen,
.obj--module[data-state="EXPLORED"] .module__screen,
.obj--module[data-state="COMPLETE"] .module__screen {
    border-color: var(--acc-dim, var(--signal-dim));
    --ink: var(--acc-dim, var(--signal-dim));
}

.obj--module[data-state="AVAILABLE"] .module__screen svg,
.obj--module[data-state="EXPLORED"] .module__screen svg,
.obj--module[data-state="COMPLETE"] .module__screen svg {
    opacity: 1;
}

/* Identifiant et texte accentués. */
.obj--module[data-state="AVAILABLE"] .module__id,
.obj--module[data-state="EXPLORED"] .module__id,
.obj--module[data-state="COMPLETE"] .module__id {
    color: var(--text-secondary);
}

.obj--module[data-state="AVAILABLE"] .module__title,
.obj--module[data-state="EXPLORED"] .module__title,
.obj--module[data-state="COMPLETE"] .module__title {
    font-size: var(--fs-sm);
    color: var(--text-bright);
    text-shadow: 0 0 12px var(--acc-glow, rgba(88, 210, 238, 0.35));
}

.obj--module[data-state="AVAILABLE"] .module__subtitle,
.obj--module[data-state="EXPLORED"] .module__subtitle,
.obj--module[data-state="COMPLETE"] .module__subtitle,
.obj--module[data-state="AVAILABLE"] .module__tag,
.obj--module[data-state="EXPLORED"] .module__tag,
.obj--module[data-state="COMPLETE"] .module__tag {
    color: var(--text-muted);
}

/* Voyant et jauge : l'état se lit sans texte. */
.obj--module[data-state="AVAILABLE"] .module__lamp {
    background: var(--acc, var(--signal));
    box-shadow: 0 0 8px var(--acc-glow, rgba(88, 210, 238, 0.8)),
                0 0 16px var(--acc-glow, rgba(88, 210, 238, 0.5));
    animation: lamp-blink 2.6s var(--ease-in-out) infinite;
}

.obj--module[data-state="EXPLORED"] .module__lamp {
    background: var(--acc-hot, var(--signal-hot));
    box-shadow: 0 0 10px var(--acc-glow, rgba(46, 230, 255, 0.85));
}

.obj--module[data-state="AVAILABLE"] .module__signal-bar:nth-child(-n+2),
.obj--module[data-state="EXPLORED"] .module__signal-bar:nth-child(-n+3) {
    background: var(--acc, var(--signal));
    box-shadow: 0 0 4px var(--acc-glow, rgba(88, 210, 238, 0.6));
}

/* ── Mouvement : le module « vit » quand il est disponible ── */

/* Écran animé (sol qui défile, curseur qui saute, lignes de vitesse). */
.obj--module[data-state="AVAILABLE"] .e-escape__motion,
.obj--module[data-state="EXPLORED"] .e-escape__motion {
    animation: escape-flow 0.9s linear infinite;
}

.obj--module[data-state="AVAILABLE"] .e-escape__speed,
.obj--module[data-state="EXPLORED"] .e-escape__speed {
    animation: escape-blink 2.4s var(--ease-in-out) infinite;
}

.obj--module[data-state="AVAILABLE"] .e-escape__runner,
.obj--module[data-state="EXPLORED"] .e-escape__runner {
    animation: escape-jump 1.6s var(--ease-in-out) infinite;
}

/* TETRIS : la pièce « flotte » en attendant de se poser. */
.obj--module[data-state="AVAILABLE"] .e-tetris__piece,
.obj--module[data-state="EXPLORED"] .e-tetris__piece {
    animation: tetris-bob 2.4s var(--ease-in-out) infinite;
}

/* MEMORY : les deux symboles révélés respirent doucement. */
.obj--module[data-state="AVAILABLE"] .e-memory__sym,
.obj--module[data-state="EXPLORED"] .e-memory__sym {
    animation: memory-pulse 3s var(--ease-in-out) infinite;
}

/* module-04 : le balayage lit le signal. */
.obj--module[data-state="AVAILABLE"] .e-wave__scan,
.obj--module[data-state="EXPLORED"] .e-wave__scan {
    animation: wave-scan 4s linear infinite;
}

/* module-05 : le nœud central pulse. */
.obj--module[data-state="AVAILABLE"] .e-lattice__core,
.obj--module[data-state="EXPLORED"] .e-lattice__core {
    animation: lattice-pulse 2.8s var(--ease-in-out) infinite;
}

/* Respiration du châssis disponible : discrète, plus marquée au survol. */
.obj--module[data-state="AVAILABLE"]:not(:hover) .module__chassis {
    animation: chassis-breathe 4.4s var(--ease-in-out) infinite;
}

@keyframes escape-flow {
    to { stroke-dashoffset: -26; }
}

@keyframes escape-blink {
    0%, 100% { opacity: 0.22; }
    50% { opacity: 0.7; }
}

@keyframes escape-jump {
    0%, 55%, 100% { transform: translate(0, 0); }
    24% { transform: translate(8px, -22px); }
    42% { transform: translate(20px, -4px); }
}

@keyframes tetris-bob {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(7px); }
}

@keyframes memory-pulse {
    0%, 100% { opacity: 0.55; }
    50% { opacity: 1; }
}

@keyframes wave-scan {
    from { transform: translateX(0); }
    to { transform: translateX(190px); }
}

@keyframes lattice-pulse {
    0%, 100% { opacity: 0.7; }
    50% { opacity: 1; }
}

@keyframes chassis-breathe {
    0%, 100% { border-color: var(--acc, var(--signal)); }
    50% { border-color: var(--acc-dim, var(--signal-dim)); }
}

/* ── STABILISÉ : lumière froide et constante ─────────────── */

.obj--module[data-state="COMPLETE"] .module__chassis {
    border-color: var(--q-cyan);
    box-shadow:
        inset 0 0 0 1px rgba(0, 0, 0, 0.42),
        inset 0 1px 0 rgba(200, 225, 245, 0.2),
        0 0 28px var(--acc-glow, rgba(95, 232, 255, 0.2)),
        var(--shadow-lg);
    animation: none;
}

.obj--module[data-state="COMPLETE"] .module__glow {
    background: radial-gradient(50% 50% at 50% 50%,
        var(--acc-glow, rgba(95, 232, 255, 0.19)) 0%,
        rgba(0, 0, 0, 0) 100%);
}

.obj--module[data-state="COMPLETE"] .module__lamp {
    background: var(--q-white);
    box-shadow: 0 0 12px rgba(243, 249, 255, 0.9), 0 0 26px var(--acc-glow, rgba(95, 232, 255, 0.5));
    animation: none;
}

.obj--module[data-state="COMPLETE"] .module__signal-bar:nth-child(-n+5) {
    background: var(--q-cyan);
    box-shadow: 0 0 4px rgba(95, 232, 255, 0.6);
}

/* ── Survol / focus clavier ─────────────────────────────── */

.obj--module[data-state="AVAILABLE"]:hover,
.obj--module[data-state="EXPLORED"]:hover,
.obj--module[data-state="COMPLETE"]:hover,
.obj--module[data-state="AVAILABLE"]:focus-visible,
.obj--module[data-state="EXPLORED"]:focus-visible,
.obj--module[data-state="COMPLETE"]:focus-visible {
    filter: blur(0) saturate(1) brightness(1.16);
}

.obj--module[data-state="AVAILABLE"]:hover .module__chassis,
.obj--module[data-state="EXPLORED"]:hover .module__chassis,
.obj--module[data-state="COMPLETE"]:hover .module__chassis,
.obj--module[data-state="AVAILABLE"]:focus-visible .module__chassis,
.obj--module[data-state="EXPLORED"]:focus-visible .module__chassis,
.obj--module[data-state="COMPLETE"]:focus-visible .module__chassis {
    border-color: var(--acc-hot, var(--signal-hot));
    box-shadow:
        inset 0 0 0 1px rgba(0, 0, 0, 0.42),
        inset 0 1px 0 rgba(200, 225, 245, 0.24),
        0 0 30px var(--acc-glow, rgba(88, 210, 238, 0.18)),
        var(--shadow-xl);
    animation: none;
}

.obj--module:hover .module__title,
.obj--module:focus-visible .module__title {
    color: var(--text-bright);
    text-shadow: 0 0 16px var(--acc-glow, rgba(88, 210, 238, 0.5));
}

/* ── Interactions ───────────────────────────────────────── */

.obj--module.is-denied {
    animation: obj-deny 0.62s var(--ease-out);
}

.obj--module.is-surging .module__glow {
    animation: obj-surge 1.5s var(--ease-out);
}

/* Premier repère mobile : une respiration supplémentaire suffit à signaler
   l'invitation. Elle disparaît dès que l'opérateur commence à explorer. */
@media (max-width: 767px) {
    .obj--module.is-calling .module__glow {
        opacity: 1;
        animation: module-call 2.8s var(--ease-in-out) infinite;
    }

    .obj--module.is-calling .module__chassis {
        border-color: var(--acc-hot, var(--signal-hot));
    }
}

@keyframes module-call {
    0%, 100% { opacity: 0.5; transform: translateX(-50%) scale(0.92); }
    50% { opacity: 1; transform: translateX(-50%) scale(1.04); }
}

@keyframes lamp-blink {
    0%, 100% { opacity: 1; }
    46% { opacity: 0.32; }
    52% { opacity: 1; }
}

@keyframes obj-deny {
    0%, 100% { filter: blur(calc(var(--depth-blur-far) * var(--d, 0))); }
    22% { filter: brightness(0.62) saturate(0.3); }
    46% { filter: brightness(1.08) saturate(0.8); }
    70% { filter: brightness(0.8) saturate(0.5); }
}

@keyframes obj-surge {
    0% { opacity: 0; }
    18% { opacity: 1; transform: translateX(-50%) scale(1.22); }
    100% { opacity: 1; transform: translateX(-50%) scale(1); }
}

/* ══════════════════════════════════════════════════════════
   5. ANOMALIES
   ══════════════════════════════════════════════════════════
   Une très faible lueur violette là où il ne devrait rien y
   avoir. Jamais une icône, jamais un objet à ramasser.
   ══════════════════════════════════════════════════════════ */

.obj--anomaly {
    width: 64px;
    height: 64px;
    /* La profondeur ne doit pas dissoudre complètement une anomalie :
       on la voit à peine, mais on la voit. */
    opacity: 1;
    filter: none;
}

.anomaly__bloom {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 260px;
    height: 260px;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: radial-gradient(50% 50% at 50% 50%,
        rgba(162, 107, 255, calc(var(--intensity, 0.4) * 0.4)) 0%,
        rgba(162, 107, 255, calc(var(--intensity, 0.4) * 0.1)) 40%,
        rgba(162, 107, 255, 0) 100%);
    pointer-events: none;
}

.anomaly__point {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 6px;
    height: 6px;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: var(--anomaly-hot);
    box-shadow: 0 0 10px rgba(201, 164, 255, 0.9),
                0 0 26px rgba(201, 164, 255, 0.55);
}

.anomaly__ripple {
    position: absolute;
    left: 50%;
    top: 50%;
    width: 64px;
    height: 64px;
    transform: translate(-50%, -50%);
    border: 1px solid rgba(162, 107, 255, 0.24);
    border-radius: 50%;
    opacity: 0;
}

/* Cachée : réellement invisible et non focusable. */
.obj--anomaly[data-state="hidden"] {
    opacity: 0;
    pointer-events: none;
}

/* Présente : elle respire très lentement. Une pulsation rapide
   ferait « collectible de jeu vidéo ». */
.obj--anomaly[data-state="present"] {
    opacity: 1;
    animation: anomaly-breathe 5.2s var(--ease-in-out) infinite;
}

.obj--anomaly[data-state="present"] .anomaly__ripple {
    opacity: 1;
    animation: anomaly-ripple 4.6s var(--ease-out) infinite;
}

/* Récupérée : il reste une trace, pas un vide. */
.obj--anomaly[data-state="taken"] {
    opacity: 0.24;
    animation: none;
    pointer-events: none;
}

.obj--anomaly[data-state="taken"] .anomaly__bloom {
    background: radial-gradient(50% 50% at 50% 50%,
        rgba(120, 130, 148, 0.14) 0%,
        rgba(120, 130, 148, 0) 100%);
}

.obj--anomaly[data-state="present"]:hover,
.obj--anomaly[data-state="present"]:focus-visible {
    filter: brightness(1.5);
}

.obj--anomaly.is-emerging {
    animation: anomaly-emerge 2.4s var(--ease-out);
}

@keyframes anomaly-breathe {
    0%, 100% { opacity: 0.6; }
    50% { opacity: 1; }
}

@keyframes anomaly-ripple {
    0% { transform: translate(-50%, -50%) scale(0.5); opacity: 0.5; }
    70% { opacity: 0; }
    100% { transform: translate(-50%, -50%) scale(2.6); opacity: 0; }
}

@keyframes anomaly-emerge {
    0% { opacity: 0; transform: translate(-50%, -100%) scale(0.4); }
    40% { opacity: 1; }
    100% { opacity: 1; transform: translate(-50%, -100%) scale(1); }
}

/* ══════════════════════════════════════════════════════════
   6. PRÉSENCES — décor abstrait
   ══════════════════════════════════════════════════════════
   Elles donnent l'échelle et suggèrent une installation plus
   grande que ce qui est visible. Sans fonction, mais pas sans
   réaction.
   ══════════════════════════════════════════════════════════ */

.obj--struct {
    pointer-events: auto;
}

.struct__body {
    position: absolute;
    left: 50%;
    bottom: 0;
    transform: translateX(-50%);
}

.struct__detail {
    position: absolute;
    background: rgba(185, 201, 217, 0.04);
}

/* Arche : au tout premier plan, elle cadre la scène par en bas.
   Un simple trait courbe, pas une architecture. */
.obj--struct--arc { width: 1100px; height: 240px; }
.obj--struct--arc .struct__body {
    width: 100%;
    height: 100%;
    border: 1px solid rgba(170, 200, 225, 0.07);
    border-bottom: none;
    border-radius: 50% 50% 0 0 / 100% 100% 0 0;
    background: linear-gradient(180deg, rgba(20, 26, 36, 0.4) 0%, rgba(0, 0, 0, 0) 30%);
}
.obj--struct--arc .struct__detail { display: none; }
.obj--struct--arc .obj__contact { display: none; }

/* Anneau debout : une géométrie pure perdue dans le noir. */
.obj--struct--ring { width: 300px; height: 300px; }
.obj--struct--ring .struct__body {
    width: 100%;
    height: 100%;
    border: 1px solid rgba(170, 200, 225, 0.08);
    border-radius: 50%;
    background: radial-gradient(50% 50% at 50% 50%,
        rgba(0, 0, 0, 0) 62%,
        rgba(27, 35, 48, 0.16) 100%);
}
.obj--struct--ring .struct__detail { display: none; }

/* Plan : une surface inclinée, à peine éclairée. */
.obj--struct--plane { width: 320px; height: 200px; }
.obj--struct--plane .struct__body {
    width: 100%;
    height: 100%;
    clip-path: polygon(0 16%, 100% 0, 100% 100%, 0 100%);
    background: linear-gradient(160deg,
        rgba(170, 200, 225, 0.05) 0%,
        rgba(27, 35, 48, 0.1) 55%,
        rgba(0, 0, 0, 0.2) 100%);
}
.obj--struct--plane .struct__detail {
    inset: auto 10% 18% 10%;
    height: 1px;
    background: rgba(170, 200, 225, 0.07);
}

/* Fragment : un éclat géométrique, sans fonction identifiable.
   Il peut porter un fragment : il devient alors interactive et se
   dissout au clic. Volontairement clair (acier/frost) : c'est la
   seule présence blanche de la zone, elle doit se remarquer. */
.obj--struct--shard { width: 96px; height: 130px; }
.obj--struct--shard .struct__body {
    width: 100%;
    height: 100%;
    clip-path: polygon(50% 0, 100% 78%, 62% 100%, 0 62%);
    background: linear-gradient(160deg,
        var(--frost-200) 0%,
        var(--steel-300) 35%,
        var(--metal-400) 75%,
        var(--metal-900) 100%);
    filter: drop-shadow(0 0 16px rgba(185, 201, 217, 0.18));
}
.obj--struct--shard .struct__detail {
    left: 50%;
    top: 30%;
    width: 1px;
    height: 46%;
    background: rgba(243, 249, 255, 0.5);
    transform: rotate(12deg);
}

/* Porteuse de fragment : elle s'offre au regard, puis répond au
   survol et au focus comme les anomalies. */
.obj--struct--shard[data-state="present"]:hover,
.obj--struct--shard[data-state="present"]:focus-visible {
    filter: brightness(1.6);
}

/* Dissolution : la forme se défait (éclat, rotation, fondu) avant
   de disparaître. La disparition finale est portée par taken. */
.obj--struct--shard.is-shattering {
    animation: shard-burst 0.6s var(--ease-out) forwards;
    pointer-events: none;
}

/* Récupérée : plus rien ne subsiste de la forme. */
.obj--struct--shard[data-state="taken"] {
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--t-normal) var(--ease-out);
}

@keyframes shard-burst {
    0% { opacity: 1; transform: scale(1) rotate(0deg); filter: brightness(1); }
    45% { opacity: 1; transform: scale(1.18) rotate(8deg); filter: brightness(2.4); }
    100% { opacity: 0; transform: scale(1.5) rotate(16deg); filter: brightness(3) blur(4px); }
}

/* Signal : une minuscule lumière autonome, perdue au fond.
   C'est l'activité résiduelle incarnée. */
.obj--struct--beacon { width: 14px; height: 14px; }
.obj--struct--beacon .struct__body {
    left: 50%;
    bottom: 5px;
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: var(--signal);
    box-shadow: 0 0 10px rgba(88, 210, 238, 0.8);
    animation: beacon-blink 5.4s steps(1, end) infinite;
}
.obj--struct--beacon .struct__detail { display: none; }
.obj--struct--beacon .obj__contact { display: none; }

/* Lame : une très fine surface verticale, presque rien. */
.obj--struct--fin { width: 8px; height: 380px; }
.obj--struct--fin .struct__body {
    width: 100%;
    height: 100%;
    background: linear-gradient(180deg,
        rgba(127, 149, 173, 0) 0%,
        #1b2330 42%,
        #0d1219 100%);
}
.obj--struct--fin .struct__detail {
    left: 50%;
    top: 26%;
    width: 44px;
    height: 1px;
    background: rgba(170, 200, 225, 0.06);
}

@keyframes beacon-blink {
    0%, 58% { opacity: 1; }
    59%, 63% { opacity: 0.1; }
    64%, 82% { opacity: 0.7; }
    83%, 84% { opacity: 0.15; }
    85%, 100% { opacity: 1; }
}

/* Le décor accuse réception : une brève montée de lumière, sans
   message ni interface. */
.obj--struct.is-touched .struct__body {
    animation: struct-touch 0.6s var(--ease-out);
}

@keyframes struct-touch {
    0%, 100% { filter: brightness(1); }
    30% { filter: brightness(1.7); }
}

/* ══════════════════════════════════════════════════════════
   7. MOUVEMENT RÉDUIT
   ══════════════════════════════════════════════════════════
   base.css neutralise déjà les durées. On coupe ici ce qui
   tournerait en boucle, pour ne rien laisser vibrer.
   ══════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
    .core__halo,
    .core__logo,
    .core__ring,
    .core__anomaly,
    .module__chassis,
    .module__lamp,
    .e-escape__motion,
    .e-escape__speed,
    .e-escape__runner,
    .e-tetris__piece,
    .e-memory__sym,
    .e-wave__scan,
    .e-lattice__core,
    .obj--anomaly,
    .anomaly__ripple,
    .obj--struct--beacon .struct__body,
    .cable__flow {
        animation: none !important;
    }

    .cable__flow {
        opacity: 0.4;
    }
}
