/* Frise multi-projets — feuille de style unique. * * Deux principes structurent ce fichier : * * 1. Les barres, jalons et cellules d'en-tête sont positionnés en absolu : * timeline.js leur calcule un `left` et une `width` en pixels depuis * l'échelle temporelle. Les couloirs, eux, restent en flux normal. * * 2. La frise entière est une grille dans un unique conteneur défilant, dont * l'en-tête et la colonne des libellés sont figés par `position: sticky`. * Le défilement des deux axes est donc entièrement natif. */ :root { --fond: #f8fafc; --surface: #ffffff; --bordure: #e2e8f0; --bordure-forte: #cbd5e1; --texte: #0f172a; --texte-doux: #64748b; --accent: #3b82f6; --danger: #dc2626; --aujourdhui: #ef4444; --hauteur-barre: 26px; --hauteur-ligne: 38px; --hauteur-entete: 66px; /* trois bandes de 22 px : année, mois, semaine */ --largeur-libelles: 250px; --rayon: 6px; --ombre: 0 1px 2px rgb(15 23 42 / 0.08); } @media (prefers-color-scheme: dark) { :root { --fond: #0f172a; --surface: #1e293b; --bordure: #334155; --bordure-forte: #475569; --texte: #f1f5f9; --texte-doux: #94a3b8; --ombre: 0 1px 2px rgb(0 0 0 / 0.3); } } * { box-sizing: border-box; } /* Indispensable : plus bas, `.panneau` et `.alerte` déclarent `display: flex`, ce qui écrase le `display: none` que le navigateur associe à l'attribut `hidden`. Sans cette règle, ces éléments restent des calques `position: fixed` étendus sur toute la page, invisibles mais interceptant tous les clics — la frise devient inerte à la souris. */ [hidden] { display: none !important; } body { margin: 0; font: 14px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; color: var(--texte); background: var(--fond); height: 100vh; display: flex; flex-direction: column; overflow: hidden; } /* --- barre d'outils (préfixe `outils` : `barre` est réservé aux barres de phase) ------------------------------------------------------ */ .outils { display: flex; align-items: center; gap: 16px; padding: 10px 16px; background: var(--surface); border-bottom: 1px solid var(--bordure); flex-shrink: 0; } .outils h1 { font-size: 15px; font-weight: 600; margin: 0; white-space: nowrap; } .outils__groupe { display: flex; gap: 4px; } .outils__etat { margin: 0 0 0 auto; font-size: 12px; color: var(--texte-doux); min-width: 130px; text-align: right; } .outils__etat[data-etat="erreur"] { color: var(--danger); font-weight: 500; } button { font: inherit; padding: 5px 11px; border: 1px solid var(--bordure-forte); border-radius: var(--rayon); background: var(--surface); color: var(--texte); cursor: pointer; } button:hover { border-color: var(--accent); } button[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: #fff; } .bouton--principal { background: var(--accent); border-color: var(--accent); color: #fff; } .bouton--danger { color: var(--danger); border-color: var(--danger); } .bouton--danger:hover { background: var(--danger); color: #fff; } /* --- structure de la frise ------------------------------------------------ */ /* Un seul conteneur défilant, sur les deux axes. L'en-tête et la colonne des libellés restent en place grâce à `position: sticky`, ce qui évite toute synchronisation de défilement en JavaScript. */ .frise { flex: 1; min-height: 0; overflow: auto; background: var(--fond); } .grille { display: grid; grid-template-columns: var(--largeur-libelles) max-content; grid-template-rows: var(--hauteur-entete) max-content; width: max-content; /* Sans cela, la dernière ligne colle au bas de la fenêtre et la barre de défilement horizontale recouvre le dernier couloir. */ padding-bottom: 24px; } /* Le coin cumule les deux ancrages, et doit donc passer au-dessus des deux. */ .grille__coin { position: sticky; top: 0; left: 0; z-index: 6; background: var(--surface); border-right: 1px solid var(--bordure); border-bottom: 1px solid var(--bordure); } .grille__entete { position: sticky; top: 0; z-index: 5; background: var(--surface); border-bottom: 1px solid var(--bordure); } .grille__libelles { position: sticky; left: 0; z-index: 4; background: var(--surface); border-right: 1px solid var(--bordure); } .grille__couloirs { position: relative; z-index: 1; } /* --- en-tête de dates : trois bandes empilées ----------------------------- */ .bande { position: relative; height: calc(var(--hauteur-entete) / 3); border-bottom: 1px solid var(--bordure); } .bande--semaine { border-bottom: 0; } .bande__cellule { position: absolute; top: 0; height: 100%; display: flex; align-items: center; justify-content: center; border-left: 1px solid var(--bordure); font-size: 11px; color: var(--texte-doux); white-space: nowrap; overflow: hidden; pointer-events: none; } .bande--annee .bande__cellule { font-size: 12px; font-weight: 600; color: var(--texte); border-left-color: var(--bordure-forte); background: color-mix(in srgb, var(--texte) 4%, transparent); } .bande--mois .bande__cellule { font-weight: 500; color: var(--texte); border-left-color: var(--bordure-forte); } /* Les numéros de semaine sont le grain le plus fin : discrets par défaut, ils ne doivent pas concurrencer le mois et l'année. */ .bande--semaine .bande__cellule { font-size: 10px; letter-spacing: -0.2px; } /* --- traits verticaux ----------------------------------------------------- */ .trait { position: absolute; top: 0; width: 1px; background: var(--bordure); pointer-events: none; } .trait--majeur { background: var(--bordure-forte); } .trait--aujourdhui { background: var(--aujourdhui); width: 2px; z-index: 4; } .trait--aujourdhui::after { content: ""; position: absolute; top: 0; left: -3px; border: 4px solid transparent; border-top-color: var(--aujourdhui); } /* --- libellés ------------------------------------------------------------ */ .libelle-projet { display: flex; align-items: center; gap: 6px; height: var(--hauteur-ligne); padding: 0 8px 0 6px; font-weight: 600; border-bottom: 1px solid var(--bordure); background: color-mix(in srgb, var(--couleur-projet) 8%, var(--surface)); } .libelle-projet__nom { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; } .libelle-projet__derive { font-size: 11px; font-weight: 500; padding: 1px 5px; border-radius: 10px; white-space: nowrap; } .libelle-projet__derive[data-sens="retard"] { background: color-mix(in srgb, var(--danger) 15%, transparent); color: var(--danger); } .libelle-projet__derive[data-sens="avance"] { background: color-mix(in srgb, #16a34a 15%, transparent); color: #16a34a; } .plier { border: 0; background: none; padding: 2px 4px; font-size: 10px; color: var(--texte-doux); cursor: pointer; transition: transform 0.15s; } .plier[aria-expanded="true"] { transform: rotate(90deg); } .icone-oeil { border: 0; background: none; padding: 2px 4px; cursor: pointer; opacity: 0.45; font-size: 12px; } .icone-oeil:hover { opacity: 1; } /* `display: block` et non `flex` : `text-overflow: ellipsis` ne s'applique pas au texte d'un conteneur flex, et le nom serait coupé net. Le centrage vertical passe donc par `line-height`. */ .libelle-phase { display: block; line-height: var(--hauteur-ligne); height: var(--hauteur-ligne); padding: 0 8px 0 26px; color: var(--texte-doux); border-bottom: 1px solid var(--bordure); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; } .libelle-phase:hover { color: var(--texte); } .libelle-projet--masque { opacity: 0.45; } /* --- couloirs et barres -------------------------------------------------- */ .couloir { position: relative; height: var(--hauteur-ligne); border-bottom: 1px solid var(--bordure); } .couloir--projet { background: color-mix(in srgb, var(--couleur-projet) 5%, transparent); } .barre { position: absolute; top: calc((var(--hauteur-ligne) - var(--hauteur-barre)) / 2); height: var(--hauteur-barre); border-radius: 4px; background: var(--couleur-projet); color: #fff; font-size: 12px; line-height: var(--hauteur-barre); padding: 0 7px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: grab; box-shadow: var(--ombre); user-select: none; z-index: 2; } .barre:hover { filter: brightness(1.08); } .barre--glissee { cursor: grabbing; z-index: 5; filter: brightness(1.12); box-shadow: 0 4px 12px rgb(15 23 42 / 0.25); } .barre--selectionnee { outline: 2px solid var(--texte); outline-offset: 1px; } /* Le statut se lit à la texture de la barre, pas à sa couleur : la couleur appartient au projet, elle sert à distinguer les couloirs entre eux. */ .barre--todo { background: color-mix(in srgb, var(--couleur-projet) 28%, var(--surface)); color: var(--texte); border: 1px solid var(--couleur-projet); } .barre--doing { background: var(--couleur-projet); } .barre--done { background: color-mix(in srgb, var(--couleur-projet) 55%, #64748b); } .barre--blocked { background: repeating-linear-gradient( 45deg, var(--couleur-projet), var(--couleur-projet) 6px, color-mix(in srgb, var(--danger) 75%, var(--couleur-projet)) 6px, color-mix(in srgb, var(--danger) 75%, var(--couleur-projet)) 12px ); } /* Poignées de redimensionnement, révélées au survol de la barre. */ .poignee { position: absolute; top: 0; width: 7px; height: 100%; cursor: ew-resize; opacity: 0; background: rgb(255 255 255 / 0.5); } .barre:hover .poignee { opacity: 1; } .poignee--debut { left: 0; border-radius: 4px 0 0 4px; } .poignee--fin { right: 0; border-radius: 0 4px 4px 0; } /* --- jalons -------------------------------------------------------------- */ .jalon { position: absolute; top: calc((var(--hauteur-ligne) - 14px) / 2); width: 14px; height: 14px; margin-left: -7px; background: var(--couleur-projet); transform: rotate(45deg); cursor: grab; z-index: 3; box-shadow: var(--ombre); } .jalon--done { background: color-mix(in srgb, var(--couleur-projet) 55%, #64748b); } .jalon--blocked { background: var(--danger); } .jalon--selectionne { outline: 2px solid var(--texte); outline-offset: 1px; } /* Le nom du jalon vit hors du losange, pour ne pas subir la rotation. */ .jalon-nom { position: absolute; top: calc((var(--hauteur-ligne) - 16px) / 2); margin-left: 12px; font-size: 11px; color: var(--texte-doux); white-space: nowrap; pointer-events: none; z-index: 3; } /* --- projet plié --------------------------------------------------------- */ /* Barre unique segmentée par phase. Lecture seule : voir docs/decisions.md, section 8. */ .barre-pliee { position: absolute; top: calc((var(--hauteur-ligne) - var(--hauteur-barre)) / 2); height: var(--hauteur-barre); border-radius: 4px; overflow: hidden; display: flex; box-shadow: var(--ombre); background: color-mix(in srgb, var(--couleur-projet) 15%, var(--surface)); cursor: default; z-index: 2; } .segment { position: absolute; top: 0; height: 100%; background: var(--couleur-projet); } .segment--todo { opacity: 0.3; } .segment--doing { opacity: 1; } .segment--done { opacity: 0.6; } .segment--blocked { background: var(--danger); opacity: 0.85; } /* --- barre fantôme de référence ------------------------------------------ */ .fantome { position: absolute; top: calc( (var(--hauteur-ligne) - var(--hauteur-barre)) / 2 + var(--hauteur-barre) - 3px ); height: 5px; border-radius: 3px; background: repeating-linear-gradient( 90deg, var(--bordure-forte), var(--bordure-forte) 4px, transparent 4px, transparent 7px ); pointer-events: none; z-index: 1; } /* --- panneau de détail --------------------------------------------------- */ .panneau { position: fixed; top: 0; right: 0; bottom: 0; width: 340px; max-width: 90vw; background: var(--surface); border-left: 1px solid var(--bordure); box-shadow: -4px 0 16px rgb(15 23 42 / 0.12); z-index: 20; display: flex; flex-direction: column; } .panneau__entete { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--bordure); } .panneau__entete h2 { margin: 0; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .panneau__entete button { border: 0; font-size: 20px; line-height: 1; padding: 0 6px; background: none; } .panneau__corps { padding: 16px; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; } .panneau__corps label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; font-weight: 500; color: var(--texte-doux); } .panneau__corps input[type="text"], .panneau__corps input[type="date"], .panneau__corps select, .panneau__corps textarea { font: inherit; padding: 6px 8px; border: 1px solid var(--bordure-forte); border-radius: var(--rayon); background: var(--fond); color: var(--texte); width: 100%; } .panneau__corps textarea { resize: vertical; } .champ-double { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .case { flex-direction: row !important; align-items: center; gap: 8px !important; font-weight: 400 !important; color: var(--texte) !important; } .case input { width: auto; } .panneau__erreur { margin: 0; padding: 8px 10px; border-radius: var(--rayon); background: color-mix(in srgb, var(--danger) 12%, transparent); color: var(--danger); font-size: 12px; } .panneau__actions { margin-top: 4px; } .voile { position: fixed; inset: 0; background: rgb(15 23 42 / 0.15); z-index: 15; } /* --- menu contextuel ------------------------------------------------------ */ .menu { position: fixed; z-index: 30; min-width: 210px; padding: 4px; background: var(--surface); border: 1px solid var(--bordure-forte); border-radius: var(--rayon); box-shadow: 0 8px 24px rgb(15 23 42 / 0.18); } .menu__entree { display: block; width: 100%; text-align: left; padding: 6px 10px; border: 0; border-radius: 4px; background: none; white-space: nowrap; } .menu__entree:hover:not(:disabled), .menu__entree:focus-visible { background: color-mix(in srgb, var(--accent) 14%, transparent); border-color: transparent; outline: none; } .menu__entree:disabled { color: var(--texte-doux); opacity: 0.5; cursor: default; } .menu__entree--danger { color: var(--danger); } .menu__entree--danger:hover:not(:disabled), .menu__entree--danger:focus-visible { background: color-mix(in srgb, var(--danger) 14%, transparent); } .menu__separateur { margin: 4px 6px; border: 0; border-top: 1px solid var(--bordure); } .menu-projet { border: 0; background: none; padding: 2px 5px; cursor: pointer; color: var(--texte-doux); font-size: 14px; line-height: 1; border-radius: 4px; } .menu-projet:hover { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--texte); } /* --- dialogues ------------------------------------------------------------ */ .dialogue { padding: 0; border: 1px solid var(--bordure-forte); border-radius: 10px; background: var(--surface); color: var(--texte); max-width: 400px; box-shadow: 0 12px 40px rgb(15 23 42 / 0.28); } .dialogue::backdrop { background: rgb(15 23 42 / 0.45); } .dialogue form { display: flex; flex-direction: column; gap: 14px; padding: 18px 20px 16px; } .dialogue h2 { margin: 0; font-size: 15px; } .dialogue p { margin: 0; color: var(--texte-doux); line-height: 1.5; } .dialogue label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; font-weight: 500; color: var(--texte-doux); } .dialogue input[type="text"] { font: inherit; padding: 7px 9px; border: 1px solid var(--bordure-forte); border-radius: var(--rayon); background: var(--fond); color: var(--texte); } .couleurs { margin: 0; padding: 0; border: 0; } .couleurs legend { padding: 0 0 6px; font-size: 12px; font-weight: 500; color: var(--texte-doux); } .couleurs div { display: flex; flex-wrap: wrap; gap: 7px; } .pastille { width: 24px; height: 24px; padding: 0; border: 2px solid transparent; border-radius: 50%; cursor: pointer; } .pastille[aria-pressed="true"] { border-color: var(--texte); box-shadow: inset 0 0 0 2px var(--surface); } .dialogue__erreur { color: var(--danger) !important; font-size: 12px; } .dialogue__actions { display: flex; justify-content: flex-end; gap: 8px; margin: 0; padding: 0; } /* --- états divers -------------------------------------------------------- */ .vide { margin: auto; text-align: center; color: var(--texte-doux); line-height: 2.4; } .alerte { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; background: var(--fond); z-index: 50; padding: 24px; } .alerte p { max-width: 560px; padding: 16px 20px; border-radius: var(--rayon); border: 1px solid var(--danger); background: color-mix(in srgb, var(--danger) 8%, var(--surface)); color: var(--texte); white-space: pre-wrap; } /* Pendant un glisser, on neutralise la sélection de texte sur toute la page : sinon le navigateur surligne les libellés au passage du curseur. */ body.glisse-en-cours { user-select: none; cursor: grabbing; }