/* Hexavi — professional field survey UI.
   Dense by design: 13px base, compact controls, restrained colour.
   Field screens get larger touch targets without inflating typography. */

:root {
  --ink:#0f172a; --text:#1e293b; --muted:#64748b; --faint:#94a3b8;
  --line:#e2e8f0; --line-strong:#cbd5e1;
  --bg:#ffffff; --bg-sub:#f8fafc; --bg-alt:#f1f5f9; --bg-dark:#0f172a;
  --accent:#1d4ed8; --accent-hover:#1e40af; --accent-soft:#eff6ff; --accent-line:#bfdbfe;
  --ok:#15803d; --ok-soft:#f0fdf4; --warn:#b45309; --warn-soft:#fffbeb;
  --danger:#b91c1c; --danger-soft:#fef2f2;
  /* The three states an element of a survey can be in. Said in colour wherever
     they are offered — the filter bar, the panel, and the plan's own default —
     because three words in a row read as three words, and the same three
     colours in three places read as one idea. Grey for what is there, navy for
     what is proposed, red for what comes out.
     Light always, so a state is legible before it is chosen; the strong tone
     when it is, so the choice reads as made rather than as one more option. */
  --life-existing:#475569; --life-existing-soft:#dee5ed; --life-existing-line:#a9b6c7;
  --life-proposed:#1e3a8a; --life-proposed-soft:#d7e0f7; --life-proposed-line:#9db3e4;
  --life-remove:#b91c1c;   --life-remove-soft:#f9d2d2;   --life-remove-line:#eb9d9d;
  --radius:3px; --radius-lg:5px;
  --shadow-sm:0 1px 2px rgba(15,23,42,.06);
  --shadow:0 4px 12px rgba(15,23,42,.10);
  --shadow-lg:0 12px 32px rgba(15,23,42,.16);
  --topbar-h:46px;
  --font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
}

*,*::before,*::after{box-sizing:border-box}
[hidden]{display:none !important}
html,body{height:100%}
body{
  margin:0; font-family:var(--font); font-size:13px; line-height:1.45;
  color:var(--text); background:var(--bg-sub);
  -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
}
a{color:var(--accent); text-decoration:none}
a:hover{text-decoration:underline}
h1,h2,h3,h4{margin:0; font-weight:600; color:var(--ink); letter-spacing:-.01em}
h1{font-size:17px} h2{font-size:14px} h3{font-size:13px} h4{font-size:12px}
p{margin:0 0 8px}
hr{border:0;border-top:1px solid var(--line);margin:16px 0}
code,.mono{font-family:var(--mono); font-size:11.5px}
.num{font-variant-numeric:tabular-nums}

/* ---------- top bar ---------- */
.topbar{
  height:var(--topbar-h); background:var(--bg-dark); color:#e2e8f0;
  display:flex; align-items:center; gap:2px; padding:0 12px;
  position:sticky; top:0; z-index:60; overflow:hidden;
}
.brand{
  display:flex; align-items:baseline; gap:8px; margin-right:14px;
  color:#fff; font-weight:700; font-size:13.5px; letter-spacing:.08em;
}
.brand:hover{text-decoration:none}
.brand .mark{display:inline-flex;align-items:center;transform:translateY(3px);line-height:0}
.brand .mark svg{display:block}
.brand small{font-weight:400;font-size:10px;color:#64748b;letter-spacing:.04em;text-transform:uppercase}
.topnav{display:flex;align-items:center;gap:1px;flex:1;min-width:0;overflow-x:auto;scrollbar-width:none}
.topnav::-webkit-scrollbar{display:none}
.topnav a{
  color:#cbd5e1; padding:5px 10px; border-radius:var(--radius); font-size:12.5px;
  white-space:nowrap; line-height:1.5;
}
.topnav a:hover{background:rgba(255,255,255,.08); color:#fff; text-decoration:none}
.topnav a.active{background:rgba(255,255,255,.13); color:#fff; font-weight:500}
.topbar-right{display:flex;align-items:center;gap:8px;margin-left:auto;padding-left:10px}
.langswitch{display:flex;border:1px solid rgba(255,255,255,.16);border-radius:var(--radius);overflow:hidden}
.langswitch button{
  background:none;border:0;color:#94a3b8;font:inherit;font-size:10.5px;font-weight:600;
  padding:3px 7px;cursor:pointer;letter-spacing:.04em;text-transform:uppercase;
}
.langswitch button:hover{background:rgba(255,255,255,.08);color:#e2e8f0}
.langswitch button.on{background:#334155;color:#fff}
/* The organisation switch: the language switch's frame and button, with a
   name in it rather than a code — so neither capitals nor unbounded width. */
.orgswitch button{text-transform:none;letter-spacing:0;font-weight:500;max-width:24ch;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* A survey of another client, opened from a link: said, briefly, beside it. */
.orgother{max-width:22ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.usermenu{display:flex;align-items:center;gap:7px;font-size:11.5px;color:#94a3b8;white-space:nowrap;min-width:0}
.usermenu .who{overflow:hidden;text-overflow:ellipsis;max-width:22ch}
.usermenu .who{color:#e2e8f0}
.usermenu .role{font-size:10px;color:#64748b;text-transform:uppercase;letter-spacing:.04em}
.usermenu form{margin:0}
.linkbtn{background:none;border:0;color:#94a3b8;font:inherit;font-size:11.5px;cursor:pointer;padding:2px 4px}
.linkbtn:hover{color:#fff;text-decoration:underline}

/* ---------- shell ---------- */
.shell{max-width:1440px;margin:0 auto;padding:16px}
.shell.wide{max-width:none}
.page-head{display:flex;align-items:flex-start;gap:12px;margin-bottom:14px;flex-wrap:wrap}
.page-head .grow{flex:1;min-width:200px}
.page-head h1{margin-bottom:2px}
.subtle{color:var(--muted);font-size:12px}
.crumbs{font-size:11.5px;color:var(--muted);margin-bottom:3px}
.crumbs a{color:var(--muted)}
.crumbs span{margin:0 5px;color:var(--line-strong)}

/* ---------- cards ---------- */
.card{background:var(--bg);border:1px solid var(--line);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm)}
.card + .card{margin-top:14px}
.grid > .card + .card{margin-top:0}
.card-head{
  display:flex;align-items:baseline;gap:10px;padding:9px 14px;border-bottom:1px solid var(--line);
  background:var(--bg-sub);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
}
.card-head h2,.card-head h3{flex:1;min-width:0}
.card-body{padding:14px}
.card-body.tight{padding:0}
.card-foot{padding:9px 14px;border-top:1px solid var(--line);background:var(--bg-sub);display:flex;gap:8px;align-items:center}

.grid{display:grid;gap:14px}
.grid.c2{grid-template-columns:repeat(2,minmax(0,1fr))}
.grid.c3{grid-template-columns:repeat(3,minmax(0,1fr))}
.grid.c4{grid-template-columns:repeat(4,minmax(0,1fr))}
.grid.sidebar{grid-template-columns:minmax(0,1fr) 320px;align-items:start}
/* Everything on the right, as one cell. A grid of two columns places a third
   child back in column 1 on a second row — which begins below the taller of the
   first row — so a sidebar written as two siblings puts its second half under
   the main content, after a column of white space as tall as its first half. */
.grid.sidebar > .sidecol{display:flex;flex-direction:column;gap:14px;min-width:0}
@media(max-width:980px){.grid.c3,.grid.c4,.grid.sidebar{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:640px){.grid.c2,.grid.c3,.grid.c4,.grid.sidebar{grid-template-columns:1fr}}

/* ---------- stats ---------- */
.stat{background:var(--bg);border:1px solid var(--line);border-radius:var(--radius-lg);padding:11px 13px}
.stat .k{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin-bottom:3px}
.stat .v{font-size:21px;font-weight:600;color:var(--ink);line-height:1.1;font-variant-numeric:tabular-nums}
.stat .v.accent{color:var(--accent)} .stat .v.ok{color:var(--ok)}
.stat .v.warn{color:var(--warn)} .stat .v.danger{color:var(--danger)}
.stat .hint{font-size:10.5px;color:var(--faint);margin-top:2px}

/* ---------- tables ---------- */
.table-wrap{overflow-x:auto}
table.data{width:100%;border-collapse:collapse;font-size:12.5px}
table.data th{
  text-align:left;font-weight:600;font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;
  color:var(--muted);padding:7px 12px;border-bottom:1px solid var(--line);background:var(--bg-sub);
  white-space:nowrap;position:sticky;top:0;z-index:1;
}
table.data td{padding:7px 12px;border-bottom:1px solid var(--line);vertical-align:middle}
table.data tbody tr:hover{background:var(--bg-sub)}
/* The total under a table: read as a total rather than as one more row —
   the heading row's own background, and no line under the last of them. */
table.data tfoot td{background:var(--bg-sub);border-top:1px solid var(--line);border-bottom:none}
table.data tbody tr:last-child td{border-bottom:0}
table.data td.right,table.data th.right{text-align:right;font-variant-numeric:tabular-nums}
table.data td.nowrap{white-space:nowrap}
table.data .rowactions{display:flex;gap:5px;justify-content:flex-end;opacity:.55;transition:opacity .12s}
table.data tr:hover .rowactions{opacity:1}
.empty{padding:30px 16px;text-align:center;color:var(--faint);font-size:12.5px}

/* ---------- buttons ---------- */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:6px;
  height:28px;padding:0 11px;border-radius:var(--radius);border:1px solid var(--line-strong);
  background:var(--bg);color:var(--text);font:inherit;font-size:12.5px;font-weight:500;
  cursor:pointer;white-space:nowrap;transition:background .12s,border-color .12s,color .12s;
}
.btn:hover{background:var(--bg-alt);text-decoration:none;border-color:#94a3b8}
.btn:active{transform:translateY(.5px)}
.btn:disabled{opacity:.5;cursor:not-allowed}
/* A link cannot be :disabled. An end-of-list arrow still has to look spent
   rather than inviting, and must not be followed. */
a.btn.disabled{opacity:.45;pointer-events:none}
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
.btn.primary:hover{background:var(--accent-hover);border-color:var(--accent-hover)}
.btn.ok{background:var(--ok);border-color:var(--ok);color:#fff}
.btn.ok:hover{filter:brightness(.94)}
.btn.danger{color:var(--danger);border-color:#fecaca;background:var(--danger-soft)}
.btn.danger:hover{background:#fee2e2;border-color:#fca5a5}
.btn.ghost{border-color:transparent;background:transparent;color:var(--muted)}
.btn.ghost:hover{background:var(--bg-alt);color:var(--text)}
.btn.sm{height:24px;padding:0 8px;font-size:11.5px}
.btn.lg{height:34px;padding:0 16px;font-size:13px}
.btn.block{width:100%}
.btn-row{display:flex;gap:7px;flex-wrap:wrap;align-items:center}
.btn-row.end{justify-content:flex-end}

/* ---------- badges ---------- */
.badge{
  display:inline-flex;align-items:center;gap:4px;height:18px;padding:0 6px;border-radius:2px;
  font-size:10.5px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;
  background:var(--bg-alt);color:var(--muted);border:1px solid var(--line);white-space:nowrap;
}
.badge.ok{background:var(--ok-soft);color:var(--ok);border-color:#bbf7d0}
.badge.warn{background:var(--warn-soft);color:var(--warn);border-color:#fde68a}
.badge.danger{background:var(--danger-soft);color:var(--danger);border-color:#fecaca}
.badge.accent{background:var(--accent-soft);color:var(--accent);border-color:var(--accent-line)}
.badge.neutral{background:var(--bg-alt)}
/* An id in a badge is read and searched as written: not in capitals. */
.badge.mono{text-transform:none;letter-spacing:0}
.dot-badge{width:7px;height:7px;border-radius:50%;display:inline-block;flex:none}

/* ---------- forms ---------- */
.field{margin-bottom:11px}
.field.inline{display:flex;align-items:center;gap:8px}
label,.label{display:block;font-size:11px;font-weight:600;color:var(--muted);margin-bottom:3px;letter-spacing:.01em}
label .req{color:var(--danger);margin-left:2px}
/* `input:not([type])` belongs in this list. A bare <input> IS a text input as
   far as HTML is concerned and as far as anybody writing a form is concerned,
   and every selector here named a type — so one written without the attribute
   came out 21px tall beside a 30px select, next to controls it was supposed to
   line up with. Styling it by what it IS rather than by what it says it is
   closes the gap for every form that will ever be written. */
input:not([type]),
input[type=text],input[type=password],input[type=email],input[type=number],input[type=date],
input[type=search],input[type=url],select,textarea{
  width:100%;height:30px;padding:0 9px;font:inherit;font-size:12.5px;color:var(--text);
  background:var(--bg);border:1px solid var(--line-strong);border-radius:var(--radius);
  transition:border-color .12s,box-shadow .12s;
}
textarea{height:auto;min-height:64px;padding:7px 9px;line-height:1.5;resize:vertical}
select{appearance:none;padding-right:26px;
  background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath fill='%2364748b' d='M0 0h10L5 6z'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 9px center}
select[multiple]{height:auto;min-height:78px;padding:5px;background-image:none}
input:focus,select:focus,textarea:focus{
  outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(29,78,216,.11)}
input[type=color]{width:44px;height:30px;padding:2px;border:1px solid var(--line-strong);border-radius:var(--radius);background:var(--bg);cursor:pointer}
input.err,select.err,textarea.err{border-color:var(--danger);background:var(--danger-soft)}
.err-msg{color:var(--danger);font-size:11px;margin-top:3px}
.hint{color:var(--faint);font-size:11px;margin-top:3px}
.check{display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--text);cursor:pointer;font-weight:400;margin-bottom:0}
.check input{width:14px;height:14px;margin:0;accent-color:var(--accent);cursor:pointer}
.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 16px}
.form-grid .full{grid-column:1/-1}
/* A field that holds one column rather than the whole row, and one that holds
   a column over two rows so two short fields stack beside it. Same grid, same
   gutter, same field — only the span changes, and the single-column layout
   below ignores both. */
.form-grid .half{grid-column:1}
.form-grid .tall{grid-column:1;grid-row:span 2}
/* Only the two-row one stretches: it stands beside two stacked fields and has
   their height to fill. A one-row textarea keeps the height its `rows` asked
   for — stretched, it pushed its own help line through the bottom of the card. */
.form-grid .tall textarea{height:calc(100% - 22px);min-height:76px}
@media(max-width:640px){.form-grid{grid-template-columns:1fr}}
.w-sm{max-width:140px}
/* La ligne « échelle et nord ». Elle tient sur UNE ligne, y compris dans le
   dialogue étroit de la liste des plans : c'est une phrase — tant de pixels
   font tant de mètres, et le nord est là — et une phrase coupée en deux au
   milieu n'est plus une phrase, c'est trois champs en vrac. Les largeurs sont
   donc posées ici plutôt que laissées au flex, qui étirait chaque case jusqu'à
   déborder. Sur un téléphone elle a le droit de passer à la ligne : à 360 px
   rien ne tient, et une ligne coupée reste lisible là où un débordement ne
   l'est pas. */
.calibline{display:flex;align-items:baseline;gap:4px;flex-wrap:nowrap}
.calibline input{max-width:84px;min-width:0}
.calibline select{max-width:66px;min-width:0}
.calibline .faint{white-space:nowrap}
@media(max-width:640px){.calibline{flex-wrap:wrap}}

.alert{padding:9px 12px;border-radius:var(--radius);font-size:12.5px;margin-bottom:12px;border:1px solid}
.alert.error{background:var(--danger-soft);border-color:#fecaca;color:#991b1b}
.alert.ok{background:var(--ok-soft);border-color:#bbf7d0;color:#166534}
.alert.info{background:var(--accent-soft);border-color:var(--accent-line);color:#1e40af}
.alert.warn{background:var(--warn-soft);border-color:#fde68a;color:#92400e}

/* ---------- tabs / filters ---------- */
/* The strip scrolls on a narrow screen and shows no bar for it. `overflow-x`
   on its own makes `overflow-y` auto as well, and the tabs hang 1px below their
   box on purpose — that overlap is what puts the active tab's underline on the
   border — so the strip was painting a vertical scrollbar for one pixel of
   overflow, on every machine set to show scrollbars always. */
.tabs{display:flex;gap:2px;border-bottom:1px solid var(--line);margin-bottom:14px;
  overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none}
.tabs::-webkit-scrollbar{display:none}
.tabs a{
  padding:6px 11px;font-size:12.5px;color:var(--muted);border-bottom:2px solid transparent;
  margin-bottom:-1px;white-space:nowrap}
.tabs a:hover{color:var(--text);text-decoration:none}
.tabs a.active{color:var(--accent);border-bottom-color:var(--accent);font-weight:600}
.tabs .count{font-size:10.5px;color:var(--faint);margin-left:4px}

/* ---------- login ---------- */
.login-wrap{min-height:100vh;display:grid;grid-template-columns:minmax(0,1fr) 460px}
@media(max-width:900px){.login-wrap{grid-template-columns:1fr}.login-aside{display:none}}
.login-aside{
  background:var(--bg-dark);color:#cbd5e1;padding:44px 40px;display:flex;flex-direction:column;
  justify-content:center;gap:26px;
  background-image:radial-gradient(900px 420px at 10% 0%,rgba(37,99,235,.30),transparent 60%);
}
.login-aside .brand-lg{display:flex;align-items:center;gap:12px;font-size:26px;font-weight:700;
  color:#fff;letter-spacing:.1em;margin-bottom:8px}
.login-aside .brand-lg svg{display:block;flex:none}
.login-aside .tag{font-size:13px;color:#94a3b8;max-width:34ch;line-height:1.55}
.login-aside ul{list-style:none;padding:0;margin:26px 0 0;font-size:12.5px;color:#94a3b8}
.login-aside li{padding:5px 0 5px 16px;position:relative}
.login-aside li::before{content:"";position:absolute;left:0;top:12px;width:5px;height:5px;border-radius:50%;background:#3b82f6}
.login-aside .foot{font-size:11px;color:#475569;margin-top:auto;padding-top:26px}
.login-aside .inner{max-width:46ch}
.login-main{display:flex;align-items:center;justify-content:center;padding:32px 28px;background:var(--bg)}
.login-box{width:100%;max-width:320px}
.login-box h1{font-size:18px;margin-bottom:3px}
.login-box .sub{color:var(--muted);font-size:12.5px;margin-bottom:20px}
.sep{display:flex;align-items:center;gap:9px;margin:18px 0;color:var(--faint);font-size:10.5px;
  text-transform:uppercase;letter-spacing:.06em}
.sep::before,.sep::after{content:"";flex:1;height:1px;background:var(--line)}
.demo-list{display:flex;flex-direction:column;gap:6px}
.demo-btn{
  display:flex;align-items:center;gap:10px;width:100%;padding:8px 10px;text-align:left;
  background:var(--bg);border:1px solid var(--line-strong);border-radius:var(--radius);
  cursor:pointer;font:inherit;transition:border-color .12s,background .12s}
.demo-btn:hover{border-color:var(--accent);background:var(--accent-soft)}
.demo-btn .av{
  width:26px;height:26px;border-radius:3px;background:var(--bg-alt);color:var(--muted);
  display:flex;align-items:center;justify-content:center;font-size:10.5px;font-weight:700;flex:none;
  border:1px solid var(--line)}
.demo-btn:hover .av{background:var(--accent);color:#fff;border-color:var(--accent)}
.demo-btn .t{flex:1;min-width:0}
.demo-btn .t b{display:block;font-size:12.5px;font-weight:600;color:var(--ink)}
.demo-btn .t span{font-size:11px;color:var(--muted)}

/* ---------- modal / confirm ---------- */
.modal{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:300;display:flex;
  align-items:center;justify-content:center;padding:20px;
  animation:modalIn .12s ease-out}
@keyframes modalIn{from{opacity:0}to{opacity:1}}
.modal-card{background:var(--bg);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);
  padding:18px 20px;width:100%;max-width:400px}
.modal-card h3{margin-bottom:5px}
.modal-card .subtle{margin-bottom:14px}
.modal-card .modal-msg{font-size:12.5px;color:var(--text);margin:0 0 16px;line-height:1.5}
.modal-card .modal-msg b{color:var(--ink)}
/* Centred, not top-aligned: the h3 carried a padding-top so it would line up
   with the 32px icon of the confirm dialog, which left the title sitting 4px
   below the meta text beside it in every dialog that has no icon. */
.modal-head{display:flex;align-items:center;gap:11px;margin-bottom:12px}
/* The spacer has to actually spread, or the buttons sit against the title in
   the middle of a wide dialog and the top-right corner — where a close is
   looked for — is empty. */
.modal-head .grow{flex:1;min-width:0}
.modal-icon{width:32px;height:32px;border-radius:50%;flex:none;display:flex;align-items:center;
  justify-content:center;background:var(--bg-alt);color:var(--muted)}
.modal-icon.danger{background:var(--danger-soft);color:var(--danger)}
.modal-head h3{margin:0}

/* Plan preview: a big image needs the room, and the plan's identity has to
   travel with it — a bare picture in a new tab tells you nothing. */
.modal-card.wide{max-width:min(1100px,94vw);padding:14px 16px 16px}
.img-modal .modal-head{gap:10px;margin-bottom:10px}
.img-modal .modal-head h3{font-size:14px}
.modal-meta{font-size:11.5px;color:var(--muted);white-space:nowrap}
/* Only the close button is pushed to the far end. When this said `.iconbtn`
   it caught the prev and next arrows too, and each one pushed the next: the
   two arrows ended up 280px apart with the plan's name marooned between
   them. */
.img-modal .modal-head [data-act="close"]{margin-left:auto}
.iconbtn{background:none;border:1px solid transparent;border-radius:var(--radius);
  color:var(--muted);padding:4px;cursor:pointer;line-height:0}
.iconbtn:hover{background:var(--bg-alt);color:var(--ink)}
/* A password field's eye (js/password-eye.js), inside the field on its right,
   shown once something is typed. */
.pwfield{position:relative;display:block}
.pwfield > input{padding-right:32px}
.pwfield > .pweye{position:absolute;right:3px;top:50%;transform:translateY(-50%);padding:3px}
/* The picture fits the box, and the box fits the screen.

   The original bug was two guesses at the same number: the stage was capped at
   74vh and the image inside it at 74vh as well, so the image plus the stage's
   own 1px border was always a couple of pixels taller than the stage holding
   it — a scrollbar on every plan, on every screen. And 74vh took no account of
   the dialog's own title and buttons, so a short window pushed the card off the
   bottom.

   So: exactly one cap, and it is on the image. The stage shrink-wraps whatever
   the image ends up being, which is why nothing can overflow it — there is no
   second number for the border to break. The cap subtracts the dialog's own
   chrome rather than guessing a fraction of the screen, so the card fits a
   short window too.

   Two things that were tried and are wrong, recorded so they are not tried
   again. Capping the image at `max-height:100%` of a flexing stage does not
   bite: the percentage resolves against a height the flex algorithm has not
   settled, and a flex item's `min-height:auto` stops it shrinking below its
   intrinsic size anyway — measured, a 685px image stayed 685px inside a 580px
   stage. And pinning the image to all four edges with `position:absolute` does
   bite, but an absolutely positioned child contributes no height, so the stage
   collapsed and a 1400x900 plan rendered at 362px wide. */
.img-modal .modal-card{
  display:flex;flex-direction:column;max-height:calc(100vh - 40px);min-height:0}
.img-stage{
  position:relative;display:flex;align-items:center;justify-content:center;
  background:var(--bg-alt);border:1px solid var(--line);border-radius:var(--radius);
  flex:0 1 auto;min-height:160px;overflow:hidden;margin-bottom:12px}
/* 152px is this dialog's chrome: the modal's own padding, the card's, the head,
   the button row and the gap between them. */
.img-stage img{
  max-width:100%;max-height:calc(100vh - 152px);width:auto;height:auto;
  object-fit:contain;display:block;opacity:0;transition:opacity .15s}
.img-stage img.ready{opacity:1}
/* A turned plan. The transform leaves the layout box alone, so app.js measures
   the box, scales the picture to fit sideways and sets the stage's height; all
   the CSS has to do is stop the column flex from shrinking that height back
   again. The transition is on the picture only — the stage's height changes in
   the same frame, and animating both makes the turn look like it slipped. */
.img-stage.turned{flex:none}
.img-stage img{transition:opacity .15s,transform .18s ease}
/* The plan's own labels, over the top of the picture. A plan opened from a list
   of ten identically named plans is otherwise a picture with no name on it. */
.img-tags{
  position:absolute;top:8px;left:8px;right:8px;display:flex;flex-wrap:wrap;gap:4px;
  pointer-events:none}
/* The same badge over a picture. A 10% tint would disappear on a white floor
   plan, so it gets an opaque backdrop behind the tint rather than a colour of
   its own — the badge stays the badge, it just stops being see-through. */
.img-tags .tag{
  font-size:11px;line-height:1.9;padding:0 8px;max-width:none;font-weight:600;
  background:linear-gradient(var(--tag-soft),var(--tag-soft)),var(--bg);
  box-shadow:0 1px 3px rgba(15,23,42,.22)}
.img-load{position:absolute;font-size:12px;color:var(--muted)}
.planchip .pv{background:none;border:0;padding:0 2px;cursor:pointer;color:inherit;
  font:inherit;line-height:1}
.planchip .pv:hover{color:var(--accent)}

/* ---------- misc ---------- */
.kv{display:grid;grid-template-columns:auto 1fr;gap:5px 14px;font-size:12.5px;align-items:baseline}
.kv dt{color:var(--muted);font-size:11px;white-space:nowrap}
.kv dd{margin:0;color:var(--text);min-width:0;overflow-wrap:anywhere}
/* label/value row, used by the BOM summary and the workspace panel */
.metric{display:flex;justify-content:space-between;align-items:baseline;gap:12px;
  font-size:12px;padding:4px 0;border-bottom:1px solid var(--line)}
.metric:last-child{border-bottom:0}
.metric > span{color:var(--muted);min-width:0}
.metric b{font-variant-numeric:tabular-nums;white-space:nowrap;color:var(--ink)}
.pricebox{background:var(--bg-sub);border:1px solid var(--line);border-radius:var(--radius);padding:8px 10px}
.pricebox .big{font-size:15px;font-weight:600;color:var(--ink);font-variant-numeric:tabular-nums}

.progress{height:4px;background:var(--bg-alt);border-radius:2px;overflow:hidden}
.progress > i{display:block;height:100%;background:var(--accent);border-radius:2px;transition:width .3s}
.progress.ok > i{background:var(--ok)}
.swatch{width:9px;height:9px;border-radius:2px;display:inline-block;flex:none}
.muted{color:var(--muted)} .faint{color:var(--faint)}
.right{text-align:right} .center{text-align:center}
.nowrap{white-space:nowrap}
/* How a client reaches the application, down a column: several chips on one
   row, small, wrapping rather than widening the table. */
/* The campaign step: two ways of answering the same question, one under the
   other, each a row of its own so neither reads as a continuation of the last. */
.campchoice > form + form{border-top:1px solid var(--line)}
.connchips{display:inline-flex;flex-wrap:wrap;gap:3px}
.connchips .badge{font-size:10px;padding:1px 5px}
.flex{display:flex;align-items:center;gap:8px}
/* A `grow` inside a `flex` had no rule anywhere: every one of them was scoped
   to a particular container, so the two dialog headers written with the plain
   utility never grew, and the maximise and close buttons sat against the title
   instead of at the right-hand end where every window on the machine puts
   them. */
.flex > .grow{flex:1;min-width:0}
.flex.wrap{flex-wrap:wrap}
.flex.between{justify-content:space-between}
/* A title and the small hint beside it: one line of text, one baseline. */
.flex.baseline{align-items:baseline}
.gap4{gap:4px}.gap12{gap:12px}
.mt0{margin-top:0}.mt8{margin-top:8px}.mt12{margin-top:12px}.mt16{margin-top:16px}
.mb0{margin-bottom:0}.mb8{margin-bottom:8px}.mb12{margin-bottom:12px}
.scroll-y{overflow-y:auto}
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}


/* ---------- narrow screens: the top bar sheds detail, never overlaps ------- */
@media(max-width:820px){
  .topbar{padding:0 8px}
  .brand small{display:none}
  .brand{margin-right:8px}
  .usermenu .role{display:none}
  .topbar-right{gap:6px;padding-left:6px}
  .shell{padding:10px}
  .page-head{gap:8px}
}
@media(max-width:560px){
  .usermenu .who{display:none}
  .topnav a{padding:5px 8px}
}

/* ---------- site setup: progress strip ---------- */
.setup-bar{display:flex;flex-wrap:wrap;gap:0;padding:4px}
.setup-step{
  display:flex;align-items:center;gap:9px;padding:10px 14px;flex:1 1 150px;min-width:0;
  border-right:1px solid var(--line);position:relative}
.setup-step:last-child{border-right:0}
.setup-step .mark{
  width:20px;height:20px;border-radius:50%;flex:none;display:flex;align-items:center;
  justify-content:center;font-size:10.5px;font-weight:700;
  background:var(--bg-alt);color:var(--faint);border:1px solid var(--line-strong)}
.setup-step .t{min-width:0;line-height:1.25}
.setup-step .t b{display:block;font-size:11.5px;font-weight:600;color:var(--muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.setup-step .t .num{font-size:13px;font-weight:600;color:var(--ink);font-variant-numeric:tabular-nums}
.setup-step.ok .mark{background:var(--ok);border-color:var(--ok);color:#fff}
.setup-step.ok .t b{color:var(--ok)}
.setup-step.now{background:var(--accent-soft)}
.setup-step.now .mark{background:var(--accent);border-color:var(--accent);color:#fff}
.setup-step.now .t b{color:var(--accent)}
.card-foot.ready{background:var(--ok-soft);border-top-color:#bbf7d0}

/* ---------- the administration home: four stages, in order ----------
   This was seven cards in a three-column grid. Every card had the same weight,
   so "the client" looked exactly as important as "the equipment catalogue";
   they had no order to be read in; and they answered nothing about what to do
   next. The stages are the order the work happens in, each one a row, and the
   colour is an accent on the number and the left edge rather than a fill — it
   is there to say "these belong together", not to decorate. */
.nextup{
  display:flex;align-items:baseline;gap:9px;margin-bottom:14px;padding:9px 13px;
  border-radius:var(--radius);font-size:12.5px;
  background:var(--accent-soft,#eef2ff);border:1px solid #c7d2fe}
.nextup .mark{
  width:20px;height:20px;border-radius:50%;flex:none;align-self:center;display:flex;align-items:center;
  justify-content:center;font-size:11px;font-weight:700;background:var(--accent);color:#fff}
.nextup .label{margin:0;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;font-size:10px}
.nextup a{font-weight:600}
.nextup.done{background:var(--ok-soft,#f0fdf4);border-color:#bbf7d0}
.nextup.done .mark{background:var(--ok)}

/* The heading on the left, the tiles beside it: one band per stage instead of a
   heading with a lot of white underneath it. Four stacked full-width rows each
   holding one small card was longer and emptier than the grid it replaced. */
.stage{
  display:grid;grid-template-columns:200px 1fr;gap:14px;align-items:center;
  margin-bottom:8px;padding:11px 14px;background:var(--bg);border:1px solid var(--line);
  border-left:3px solid var(--sc,#94a3b8);border-radius:var(--radius)}
.stage-a{--sc:#2563eb;--scs:#eef4ff}   /* who for */
.stage-b{--sc:#0d9488;--scs:#effbf9}   /* where */
.stage-c{--sc:#ea580c;--scs:#fff6ef}   /* what */
.stage-d{--sc:#7c3aed;--scs:#f7f3ff}   /* the work */
.stage-h{display:flex;align-items:flex-start;gap:9px;min-width:0}
.stage-h h2{margin:0;font-size:13px;color:var(--ink)}
.stage-n{
  width:22px;height:22px;border-radius:50%;flex:none;display:flex;align-items:center;
  justify-content:center;font-size:11.5px;font-weight:700;
  background:var(--sc);color:#fff}
.stage-hint{font-size:11.5px;color:var(--muted);margin-top:2px}
/* Every tile the same size, whatever its name is. The old grid sized each card
   to its own content, which is why Organisations was taller than the rest while
   saying no more than any of them. A cap as well as a floor on the column, so a
   stage holding one tile does not stretch it across the page. */
.stage-cards{
  display:grid;grid-template-columns:repeat(auto-fill,minmax(196px,232px));gap:8px}
/* Un choix qui EST l'action : pas une case à cocher qui change ce que fera un
   bouton plus bas, mais un bloc sur lequel on appuie et qui part. Calqué sur
   .rescard — même bordure, même rayon, même bord qui s'accentue au survol —
   avec une icône à gauche et deux lignes de texte, parce qu'ici il n'y a rien
   à compter. */
.pickcard{
  display:flex;align-items:flex-start;gap:11px;width:100%;text-align:left;
  padding:11px 13px;margin-bottom:8px;cursor:pointer;color:inherit;font:inherit;
  border:1px solid var(--line);border-radius:var(--radius);background:var(--bg-sub);
  transition:border-color .12s,box-shadow .12s}
.pickcard:hover{border-color:var(--accent);box-shadow:0 1px 4px rgba(15,23,42,.07)}
.pickcard:disabled{opacity:.5;cursor:not-allowed;box-shadow:none;border-color:var(--line)}
.pickcard .pc-ico{
  width:30px;height:30px;flex:none;border-radius:var(--radius);display:flex;
  align-items:center;justify-content:center;
  background:var(--accent-soft);color:var(--accent)}
.pickcard .pc-t{display:block;font-size:12.5px;font-weight:600;color:var(--ink);margin-bottom:1px}
.pickcard .pc-s{display:block;font-size:11px;color:var(--muted);line-height:1.35}

.rescard{
  display:block;position:relative;min-height:88px;padding:9px 11px;color:inherit;
  border:1px solid var(--line);border-radius:var(--radius);background:var(--scs,var(--bg-sub));
  transition:border-color .12s,box-shadow .12s}
.rescard:hover{text-decoration:none;border-color:var(--sc);box-shadow:0 1px 4px rgba(15,23,42,.07)}
/* Room for the "shared" tag pinned top-right, so a long resource name wraps
   instead of running under it. */
.rc-top{display:flex;align-items:center;gap:7px;padding-right:52px}
.rc-mark{
  width:17px;height:17px;border-radius:50%;flex:none;display:flex;align-items:center;
  justify-content:center;font-size:10px;font-weight:700;
  background:var(--ok);color:#fff}
.rescard.empty .rc-mark{background:var(--bg);color:var(--faint);border:1px dashed var(--line-strong)}
.rc-name{font-size:12px;font-weight:600;color:var(--ink)}
.rc-n{font-size:20px;font-weight:600;color:var(--ink);font-variant-numeric:tabular-nums;line-height:1.3}
.rescard.empty .rc-n{color:var(--faint)}
.rc-sub{font-size:11px;color:var(--muted);line-height:1.35}
.rescard.empty .rc-sub{color:var(--sc);font-weight:600}
/* Optional: an empty one is not a gap in the setup, so it does not get the
   dashed "+" and the call to action an empty required card gets. */
.rescard.opt.empty .rc-mark{background:var(--bg);color:var(--faint);border:1px solid var(--line)}
.rescard.opt.empty .rc-sub{color:var(--muted);font-weight:400}
.rc-tag{
  position:absolute;top:9px;right:10px;font-size:9px;line-height:1.6;padding:0 5px;
  border-radius:2px;background:var(--bg);border:1px solid var(--line-strong);color:var(--faint)}
/* free text somebody typed, with its links made into links ------------------ */
.mdtext{font-size:12.5px;line-height:1.5;color:var(--muted)}
.mdtext p{margin:0 0 6px}
.mdtext p:last-child,.mdtext ul:last-child,.mdtext ol:last-child{margin-bottom:0}
.mdtext ul,.mdtext ol{margin:0 0 6px;padding-left:18px}
.mdtext li{margin:1px 0}
.mdtext code{
  font-family:var(--mono,ui-monospace,monospace);font-size:11px;padding:0 3px;
  background:var(--bg-alt);border-radius:2px}
/* the one button that starts a visit ---------------------------------------- */
.btn.go{display:inline-flex;align-items:center;gap:6px}
.btn.go .gowhere{opacity:.72;font-weight:400}
.adminfoot{margin-top:14px;display:flex;gap:8px;align-items:center}

/* what a code in an imported agreement means here ---------------------------- */
.codemap{display:grid;gap:5px;max-height:420px;overflow-y:auto}
.coderow{
  display:grid;grid-template-columns:80px 1fr 260px;gap:10px;align-items:center;
  padding:5px 8px;border:1px solid var(--line);border-radius:var(--radius);background:var(--bg-sub)}
.coderow b{font-size:12px;font-family:var(--mono,ui-monospace,monospace)}
.coderow .faint{font-size:11px}
.coderow select{height:26px;font-size:12px}
/* A code with nowhere to go loses its references, so it has to look unfinished
   rather than merely unfilled. */
.coderow.unmapped{background:var(--warn-soft,#fef9c3);border-color:var(--warn,#eab308)}
@media (max-width:720px){ .coderow{grid-template-columns:1fr;gap:4px} }

/* What a code covers, in the same table. A count is not something you can
   disagree with, and a list of the same references beside this one is the same
   facts said twice — so constructeur, référence and désignation are three more
   columns of this table, each turned on by its own switch. Off to begin with:
   the question the table exists to answer is what each code means. */
.colswitch{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin:0 0 7px}
.colswitch .checkline{font-size:11.5px}
/* A cell lists what the code covers, one line each, de-duplicated. The row
   grows; the columns stay in step because every row is given the same
   template. */
.coderow .codecell{min-width:0;font-size:11.5px;line-height:1.45;color:var(--ink)}
.coderow .codecell div{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.coderow .codecell.mono div{font-family:var(--mono,ui-monospace,monospace);font-size:11px}
.coderow .codecell.num{text-align:right;font-variant-numeric:tabular-nums}
/* With columns on, a row is several lines tall and its parts should sit at the
   top of it rather than float in the middle. */
.coderow:has(.codecell){align-items:start}
.coderow:has(.codecell) b,.coderow:has(.codecell) .faint{padding-top:1px}
@media (max-width:720px){
  .coderow .codecell div{white-space:normal}
}

/* taking a site somewhere else ---------------------------------------------- */
.exportform .label{margin-bottom:5px}
.exportform .checkline{padding:1px 0}
/* The photographs tick, and why it has a line of its own. */
.warnline{
  margin:5px 0 0 22px;padding:5px 8px;border-radius:3px;font-size:10.5px;line-height:1.4;
  background:var(--warn-soft,#fef9c3);border:1px solid var(--warn,#eab308);color:var(--ink)}
/* what an archive holds, and what an import made of it */
.impcounts{display:flex;flex-wrap:wrap;gap:7px}
.impcount{
  display:flex;align-items:baseline;gap:6px;padding:6px 10px;min-width:104px;
  background:var(--bg-sub);border:1px solid var(--line);border-radius:var(--radius)}
/* What the légende calls itself, beside the counts: a name rather than a
   number, so it is set in the accent and not in the tabular figures. */
.impcount.label{min-width:0;font-weight:600;color:var(--accent,#1d4ed8);
  background:var(--accent-soft,#e8f0fe);border-color:var(--accent,#1d4ed8)}
.impcount b{font-size:16px;color:var(--ink);font-variant-numeric:tabular-nums}
.impcount span{font-size:11px;color:var(--muted)}
@media (max-width:820px){
  .stage{grid-template-columns:1fr;gap:9px;align-items:stretch}
  .stage-cards{grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
}

/* ---------- site setup: the tree ---------- */
.tree-b{border-bottom:1px solid var(--line)}
.tree-b:last-of-type{border-bottom:0}
.tree-b-h{display:flex;align-items:baseline;gap:9px;padding:10px 14px;background:var(--bg-sub)}
.tree-b-name{font-size:13px;font-weight:600;color:var(--ink)}
.tree-empty{padding:8px 14px 8px 34px;font-size:11.5px;color:var(--faint)}
.tree-l{
  display:flex;align-items:baseline;gap:10px;padding:6px 14px 6px 34px;
  border-top:1px solid var(--line);position:relative}
/* The plan chips are a row of their own: they centre on the line rather than
   hang from its baseline, which would push the level down each time a level
   has a plan. The level's name, its number and its actions share one baseline. */
.tree-l > .tree-plans{align-self:center}
.tree-l::before{
  content:"";position:absolute;left:20px;top:0;bottom:0;width:1px;background:var(--line-strong)}
.tree-l:hover{background:var(--bg-sub)}
.tree-l-n{font-size:12.5px;color:var(--text);min-width:120px}
.tree-plans{display:flex;flex-wrap:wrap;gap:5px;flex:1;min-width:0}
.planchip{
  display:inline-flex;align-items:center;gap:6px;height:22px;padding:0 7px;
  border:1px solid var(--line-strong);border-radius:var(--radius);background:var(--bg);
  font-size:11px;max-width:100%}
.planchip .pn{color:var(--ink);font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.planchip .ps{color:var(--muted);font-variant-numeric:tabular-nums;white-space:nowrap}
.planchip.ok{border-color:#bbf7d0;background:var(--ok-soft)}
.planchip.ok .ps{color:var(--ok)}
.planchip.warn{border-color:#fde68a;background:var(--warn-soft)}
.planchip.warn .ps{color:var(--warn)}
/* "Échelle non définie" took more room than the plan's own name and said it on
   every uncalibrated plan on the page. A plan with a scale says nothing at all;
   one without gets a mark, in the colour that already means "not finished", and
   the sentence moves to the tooltip. */
.noscale{
  display:inline-flex;align-items:center;justify-content:center;flex:none;
  width:14px;height:14px;border-radius:50%;
  background:var(--warn,#eab308);color:#fff;font-size:9.5px;font-weight:700;line-height:1}
/* The mark used to be a warning and nothing else: you saw it, and then went
   looking for the screen that fixes it. When there is a mission to fix it in,
   it is the way in — so it has to read as something you can press. The colour
   and the size stay; the cursor, the lift and the ring are what change.
   `.planchip a` sets a colour and a size of its own and is one class heavier
   than `.noscale`, so both are restated here rather than left to luck. */
a.noscale.act{
  text-decoration:none;cursor:pointer;color:#fff;font-size:9.5px;
  transition:box-shadow .15s,transform .12s}
a.noscale.act:hover{transform:scale(1.18);box-shadow:0 0 0 3px var(--warn-soft,rgba(234,179,8,.3))}
a.noscale.act:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.planchip .tag{flex:none}
.planchip a{text-decoration:none;color:var(--muted);font-size:12px}
.planchip a:hover{color:var(--accent)}
.tree-add{
  display:flex;align-items:center;gap:6px;flex-wrap:wrap;
  padding:8px 14px 10px 34px;border-top:1px solid var(--line);background:var(--bg)}
.tree-add input{height:26px;font-size:12px;max-width:200px}
.tree-add-b{padding-left:14px;background:var(--bg-sub);border-top:1px solid var(--line-strong)}
.tree-add-b input{max-width:280px}
.tree-presets{display:flex;align-items:center;gap:4px;margin-left:6px;font-size:10.5px}
.tree-presets .faint{margin-right:2px}
@media(max-width:640px){
  .tree-l{flex-wrap:wrap;padding-left:24px}
  .tree-l::before{left:12px}
  .tree-l-n{min-width:0}
  .tree-add{padding-left:24px}
  .setup-step{flex:1 1 100%;border-right:0;border-bottom:1px solid var(--line)}
  .setup-step:last-child{border-bottom:0}
}

/* Unpriced lines in the bill of materials.
   A missing price has to read as an open item, never as a zero. */
.tbq{color:var(--warn);font-style:italic;font-size:11px}
.warn-strip{
  display:block;margin:0 0 10px;padding:8px 12px;
  border:1px solid #fde68a;border-left:3px solid var(--warn);
  border-radius:var(--radius);background:var(--warn-soft);
  color:#92400e;font-size:12px;line-height:1.45;
}
.warn-strip b{margin-right:4px}

/* Lieux: a building and a zone are the same class of thing, told apart by a
   mark and a tint rather than by living in different lists. */
.kindmark{color:var(--faint);font-size:13px;line-height:1;margin-right:2px}
.tree-b.zone{background:linear-gradient(90deg,var(--ok-soft),transparent 60%)}
.tree-b.zone .kindmark{color:var(--ok)}
.kindseg{display:inline-flex;border:1px solid var(--line-strong);border-radius:var(--radius);overflow:hidden}
.kindseg label{margin:0;cursor:pointer}
.kindseg label input{position:absolute;opacity:0;pointer-events:none}
.kindseg label span{display:block;padding:5px 11px;font-size:11.5px;color:var(--muted);
  background:var(--bg);border-right:1px solid var(--line)}
.kindseg label:last-child span{border-right:0}
.kindseg label input:checked + span{background:var(--accent);color:#fff}
.kindseg label input:focus-visible + span{outline:2px solid var(--accent-line);outline-offset:-2px}

/* Trois raccourcis et une boîte : comment les étages d'un bâtiment sont nommés. */
.lvname{display:flex;gap:8px;align-items:center}
.lvname input[type="text"]{flex:1;min-width:0}

/* Folded form sections: everything a form can ask for later rather than now. */
details.advanced{margin-top:4px;border-top:1px solid var(--line);padding-top:10px}
details.advanced > summary{cursor:pointer;font-size:12px;color:var(--muted);
  list-style:none;padding:2px 0;user-select:none}
details.advanced > summary::-webkit-details-marker{display:none}
details.advanced > summary::before{content:'▸';display:inline-block;margin-right:6px;
  transition:transform .12s;color:var(--faint)}
details.advanced[open] > summary::before{transform:rotate(90deg)}
details.advanced > summary:hover{color:var(--ink)}
details.advanced .form-grid{margin-top:10px}

/* Choosing a plan image: click, drag, or paste. A screenshot of a map is often
   already on the clipboard, and saving it to a file first is pure friction. */
.dropzone{border:1.5px dashed var(--line-strong);border-radius:var(--radius-lg);
  background:var(--bg-sub);padding:22px 16px;text-align:center;cursor:pointer;
  transition:border-color .12s,background .12s;min-height:120px;
  display:flex;align-items:center;justify-content:center;flex-direction:column}
.dropzone:hover,.dropzone:focus-visible{border-color:var(--accent);background:var(--accent-soft);outline:none}
.dropzone.over{border-color:var(--accent);background:var(--accent-soft);border-style:solid}
.dropzone.has{padding:8px;border-style:solid;border-color:var(--line);background:var(--bg)}
.dz-main{font-size:12.5px;color:var(--text)}
.dz-sub{font-size:11.5px;color:var(--muted);margin-top:4px}
.dz-name{font-size:12px;color:var(--ink);font-family:var(--mono,monospace)}
.dz-preview{max-width:100%;max-height:340px;border-radius:var(--radius);display:block}
#planModal .tabs{border-bottom:1px solid var(--line)}
#planModal .form-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}

/* Dimming the plan and locking the view — both govern how the canvas behaves,
   so they sit with the zoom controls rather than in a settings panel. */
.dimmer{position:absolute;right:12px;bottom:56px;z-index:20;display:flex;align-items:center;gap:7px;
  background:var(--bg);border:1px solid var(--line);border-radius:var(--radius-lg);
  padding:5px 10px;box-shadow:var(--shadow-sm);color:var(--muted)}
.dimmer input[type=range]{width:104px;accent-color:var(--accent)}
.dimmer .num{font-size:11px;color:var(--muted);min-width:32px;text-align:right}
.stage-ctl button.on{background:var(--accent);color:#fff;border-color:var(--accent)}
.ws-stage.locked{cursor:default}
.ws-stage.locked .stage-inner{touch-action:none}
.dimcell{display:inline-flex;align-items:center;gap:6px}
.dimcell input[type=range]{width:90px;accent-color:var(--accent)}
.dimcell .num{font-size:11px;color:var(--muted);min-width:32px;text-align:right}

/* Preview / Original: which rendition you are looking at has to be obvious,
   because the display copy is resized and re-encoded and the original is not. */
/* An agreement, as a table you can correct. One reference to a line, with the
   requirement said once down the side of the references it allows: flat rows
   would lose the only question the document exists to answer — what am I
   allowed to put here — and a card apiece turned five items into a page.

   A cell you can type in should look like the cell beside it, not like a form
   dropped into a table: no border until you point at it, no box around a value
   you are only reading. The row keeps the table's own hover, so pointing at a
   line still lights the line. */
/* Fixed layout, or the first column takes the width of an <input> and the
   requirement — the longest thing on the line — gets what is left. */
.mltable{table-layout:fixed}
.mltable input.cell,.mltable select.cell{width:100%}
/* A fixed column will not widen for a long heading, and a heading clipped
   mid-word is a heading that says something else. Two lines cost one row. */
table.data.mltable th{white-space:normal}
/* The requirement's own Save, mid-table, is a button you almost never need: it
   turns up when the requirement has been edited and is out of the way until
   then, rather than cutting the table in half on every line. */
.mltable .linesave{text-align:right}
.mltable .linesave .btn[disabled]{display:none}
/* The drawing beside the key that names it: the chip is a chip, not an input,
   because it says what the key resolves to rather than asking for anything. */
.mltable .symcell{display:flex;align-items:center;gap:6px}
.mltable .symcell .cell,.mltable .mlpresets .cell{font-size:11.5px}
.mltable .symcell .cell{flex:1;min-width:0}
/* Put away by the switch above the table. A column goes with its heading, so
   `display:none` is set on both and the fixed layout gives the width back to
   the requirement, which is the column that can always use it. */
.mltable.off-spec .mlc-spec,.mltable.off-maker .mlc-maker,.mltable.off-part .mlc-part,
.mltable.off-power .mlc-power,.mltable.off-price .mlc-price{display:none}
.symchip.sm{width:24px;height:24px;border-radius:5px}
.symchip.sm svg{display:block}
/* An empty chip is a line drawing with the type's own symbol. Shown as an
   outline rather than a hole, so the row does not read as broken. */
.symchip.sm:empty{border-style:dashed;background:transparent}

/* The rule between two requirements, drawn by the grouping itself. */
.mltable tbody.mlgroup + tbody.mlgroup > tr:first-child > td{border-top:1px solid var(--line-strong)}
/* A requirement spanning its references sits against their first line, not
   floating in the middle of three. */
.mltable td[rowspan]{vertical-align:top}
input:not([type]).cell,input.cell,select.cell{
  /* background-color, not background: the shorthand would drop the caret a
     select draws with background-image, and a choice that looks like a word is
     a choice nobody makes. */
  border-color:transparent;background-color:transparent;height:26px;padding:0 7px;
  border-radius:var(--radius)}
select.cell{padding-right:22px;background-position:right 6px center}
input.cell:hover,select.cell:hover{border-color:var(--line-strong);background-color:var(--bg)}
input.cell:focus,select.cell:focus{background-color:var(--bg)}
input.cell::placeholder{color:var(--faint)}
.mltable td{padding:3px 6px}
.mltable th:first-child,.mltable td:first-child{padding-left:12px}
/* The row's own buttons come up with the row, like every other list here. */
.mltable .rowactions{align-items:center}

/* The header row an imported file may carry, folded away. A card that opens is
   still a card: the summary reads as a card head, and the body under it is the
   same table as everywhere else. */
details.mlkeys{margin-top:12px}
details.mlkeys > summary{cursor:pointer;list-style:none;padding:10px 14px;
  font-weight:600;font-size:12.5px;color:var(--ink)}
details.mlkeys > summary::-webkit-details-marker{display:none}
details.mlkeys > summary::before{content:'\25B8';display:inline-block;margin-right:7px;
  color:var(--muted);transition:transform .12s}
details.mlkeys[open] > summary::before{transform:rotate(90deg)}
details.mlkeys[open] > summary{border-bottom:1px solid var(--line)}
details.mlkeys .tag{font-size:10.5px}

.segmented{display:inline-flex;border:1px solid var(--line-strong);border-radius:var(--radius);overflow:hidden}
.segmented button,.segmented a{background:var(--bg);border:0;border-right:1px solid var(--line);
  padding:5px 11px;font-size:11.5px;color:var(--muted);cursor:pointer;font:inherit;font-size:11.5px;
  /* Three choices on one line: a label that wraps makes the control twice as
     tall as its neighbours and reads as a mistake. */
  white-space:nowrap}
/* A segment is sometimes a link rather than a button — choosing how a list is
   laid out is a place you can be, so it is a URL you can go back to. Links come
   with an underline and a line-height of their own. */
.segmented a{display:flex;align-items:center;text-decoration:none;line-height:0}
.segmented button:last-child,.segmented a:last-child{border-right:0}
.segmented button.on,.segmented a.on{background:var(--accent);color:#fff}
.segmented button:hover:not(.on),.segmented a:hover:not(.on){background:var(--bg-alt);color:var(--ink)}
.btn-row .grow{flex:1}
.checkline{display:flex;align-items:center;gap:7px;margin:0;cursor:pointer;font-size:12px;color:var(--text)}
.checkline input{margin:0}

/* A dialog that asks you to click the page behind it cannot be a blocking one.
   `.modal.side` keeps the card and drops the backdrop, so the plan underneath
   stays live — calibration was unusable without this, because .modal is a
   full-screen inset:0 layer that swallowed both clicks it was asking for. */
.modal.side{background:none;pointer-events:none;align-items:flex-start;
  justify-content:flex-end;padding:66px 16px 16px;animation:none}
.modal.side .modal-card{pointer-events:auto;max-width:330px;
  box-shadow:var(--shadow-lg);border:1px solid var(--line)}

/* approved models, on a site's setup page --------------------------------- */
.modelgroup{margin-bottom:12px}
.modelgroup h3{
  font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);
  margin:0 0 5px;padding-bottom:3px;border-bottom:1px solid var(--line)}
/* The heading is the control: a whole row to hit, not a caret to aim at. */
.mg-fold{
  display:flex;align-items:center;gap:6px;width:100%;padding:2px 0;border:0;background:none;
  font:inherit;color:inherit;text-transform:inherit;letter-spacing:inherit;cursor:pointer;text-align:left}
.mg-fold:hover{color:var(--ink)}
.mg-fold .caret{display:inline-block;transition:transform .12s;font-size:9px;color:var(--faint)}
.modelgroup:not(.folded) .mg-fold .caret{transform:rotate(90deg)}
.mg-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mg-n{
  flex:none;font-size:9px;line-height:1.6;padding:0 5px;border-radius:2px;letter-spacing:0;
  background:var(--bg-sub);border:1px solid var(--line);color:var(--faint)}
/* A group holding something the site has picked says so without being opened. */
.modelgroup:not(.folded) .mg-n,.modelgroup .mg-n:not(:empty){color:var(--muted)}
.modelgroup.folded .checkline{display:none}
.modelgroup .checkline{padding:2px 0}
.modelgroup .mono{font-size:10.5px;margin-left:5px}
/* Search, select-all and the tally on one line, sticky to the top of the card
   body: with three hundred products the bar scrolls away exactly when it is
   needed. */
.pickbar{
  position:sticky;top:0;z-index:2;display:flex;flex-direction:column;gap:6px;
  margin:0 0 10px;padding:7px 0;background:var(--bg);border-bottom:1px solid var(--line)}
.pickfind{
  width:100%;font-size:12px;padding:5px 9px;
  border:1px solid var(--line-strong);border-radius:var(--radius);
  background-color:var(--bg);color:var(--ink)}
.pickfind:focus{outline:none;border-color:var(--sc)}
.pickrow{display:flex;align-items:center;gap:10px}
.pickrow .checkline{flex:1 1 auto;font-weight:600}
.pickrow .faint{flex:0 0 auto;font-size:11px;white-space:nowrap;
  font-variant-numeric:tabular-nums}
#mdlHidden{margin:0 0 10px}

/* export column picker ----------------------------------------------------- */
.colgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:14px}
.colgroup h3{
  font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);
  margin:0 0 5px;padding-bottom:3px;border-bottom:1px solid var(--line)}
.colgroup .checkline{padding:2px 0;font-size:12px}
/* An identifier among the columns: the same line, in the accent colour the
   application uses for what points at something. */
.colgroup .checkline.idcol span{color:var(--accent)}
/* And one that stands for several columns at once, in the colour the
   application uses for what has gone through. */
.colgroup .checkline.groupcol span{color:var(--ok)}

/* catalogue: fields that belong together, shown together ------------------- */
/* A form of forty identical rows hides the two things that decide a quote:
   what it costs, and what it takes to put it up. Each block gets its own tint
   and its own heading so they can be found without reading everything. */
.fgroup{
  border:1px solid var(--line);border-left-width:3px;border-radius:var(--radius);
  padding:12px 14px 4px;margin:16px 0 0;background:var(--bg-alt)}
.fgroup > legend{
  font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);
  padding:0 6px;margin-left:-6px}
/* The whole heading folds the block; the chevron alone is too small to aim at. */
/* Same rule as the survey panel: a heading that folds has to look like it
   folds, or it reads as a title with an arrow drawn next to it. */
.fold-toggle{
  display:inline-flex;align-items:center;gap:6px;border:1px solid transparent;background:none;
  padding:2px 6px;margin:-2px -6px;border-radius:var(--radius);cursor:pointer;
  font:inherit;font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;color:inherit;
  transition:background .12s,border-color .12s,color .12s}
.fold-toggle:hover{color:var(--ink);background:var(--bg);border-color:var(--line-strong)}
.fold-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
/* Minus when open, plus when closed - see the note in workspace.css. */
.fold-toggle .chev{
  flex:none;width:17px;height:17px;display:flex;align-items:center;justify-content:center;
  border:1px solid var(--accent);border-radius:3px;background:var(--accent-soft,#eef2ff);
  font-size:12px;line-height:1;font-weight:700;color:var(--accent)}
.fold-toggle .chev::before{content:'\2212'}
.fgroup.closed .fold-toggle .chev::before{content:'+'}
.fgroup.closed .fold-toggle .chev{
  border-color:var(--line-strong);color:var(--muted);background:var(--bg)}
.fold-toggle:hover .chev{border-color:var(--accent);color:var(--accent)}
.fgroup.closed{padding-bottom:12px}
.fgroup.closed .fgroup-body{display:none}
.foldall{display:flex;justify-content:flex-end;margin-top:10px}
.fgroup-coverage{border-left-color:#93c5fd;background:#f5f9ff}
.fgroup-pricing{border-left-color:#86efac;background:#f4fdf7}
.fgroup-pricing > legend{color:#166534}
.fgroup-install{border-left-color:#fdba74;background:#fff8f1}
.fgroup-install > legend{color:#9a3412}

/* what a line needs before the van leaves ---------------------------------- */
.conds{display:flex;flex-wrap:wrap;gap:3px;margin-top:3px}
.cond{
  font-size:9.5px;line-height:1.5;padding:0 5px;border-radius:2px;
  background:#fff8f1;border:1px solid #fdba74;color:#9a3412;white-space:nowrap}

/* a plan's own labels ------------------------------------------------------- */
/* One colour, everywhere a tag appears: the mission's plan buttons, the site
   planner's plan chips, the administration list. A tag is not a status — it
   does not mean good, bad or pending — so it gets a hue of its own rather than
   borrowing the green/amber/red the rest of the app spends on saying how
   something is going. Stated once here, derived for the tint and the ink, so
   changing the hue is changing one line. */
/* One hue, three strengths, and one badge built from them. There is no second
   variant: a tag on the plans list, on a plan chip, on a mission's plan button
   and over a plan in the preview are all the same object, so they are all the
   same badge. The filled version that used to sit inside buttons is gone —
   two treatments for one thing meant two places to change the colour, and the
   quieter of the two was the one worth keeping. */
:root{
  --tag: #8000ff;
  --tag-soft: #8000ff1a;       /* 10% — the tint the badge sits on */
  --tag-line: #8000ff59;       /* 35% — enough edge to read on any background */
  --tag-ink: #5b00b5;          /* the same hue, dark enough to read on the tint */
}
.tags{display:inline-flex;flex-wrap:wrap;gap:3px;align-items:center}
.tag{
  font-size:9.5px;line-height:1.6;padding:0 5px;border-radius:2px;white-space:nowrap;
  max-width:120px;overflow:hidden;text-overflow:ellipsis;
  background:var(--tag-soft);border:1px solid var(--tag-line);color:var(--tag-ink)}
/* Inside a plan's button, in the mission and on the site planner alike: the
   first tag only, so the button stays the width of a plan name rather than the
   width of whatever was typed. Width and weight only — the colour is the one
   above, and there is only one. */
.tag.in-btn{max-width:90px;font-size:10px;letter-spacing:.02em;font-weight:600}

/* what a mission is about --------------------------------------------------- */
.scopebar{display:flex;flex-wrap:wrap;gap:4px 10px;align-items:center;margin-top:5px}
.scopebar .layerchip{font-size:10.5px}

/* which system an element belongs to --------------------------------------- */
.layerchip{
  display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--ink);white-space:nowrap}
.layerchip::before{
  content:'';width:7px;height:7px;border-radius:50%;background:var(--lc, #64748b);flex:none}

/* several answers, not one -------------------------------------------------- */
.checklist{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:2px 14px}
.fgroup-classification{border-left-color:#c4b5fd;background:#faf8ff}
.fgroup-classification > legend{color:#5b21b6}

/* picking a place on a map -------------------------------------------------- */
.mp-search{display:flex;gap:6px;align-items:center;padding:10px 14px 0}
.mp-search input{flex:1}
.mp-search .faint{font-size:11px;white-space:nowrap}
.mp-map{height:min(58vh,460px);margin:10px 14px;border:1px solid var(--line);border-radius:var(--radius)}
.mp-foot{display:flex;gap:8px;align-items:center;padding:0 14px 14px}
.mp-foot .mono{font-size:11.5px}

/* what kind of site this is ------------------------------------------------- */
.sitecls{display:flex;flex-wrap:wrap;gap:4px;margin-top:10px}
.cls{
  font-size:10px;line-height:1.7;padding:0 6px;border-radius:2px;
  background:#faf8ff;border:1px solid #c4b5fd;color:#5b21b6}
.mp-map.mp-down{display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:12.5px;height:120px}

/* which organisation the administration is working inside -------------------- */
.orgscope{
  display:flex;align-items:baseline;gap:8px;margin:0 0 12px;padding:7px 10px;
  background:var(--bg-alt);border:1px solid var(--line);border-radius:var(--radius)}
/* One line, one baseline: the small capitals, the name and the hint sit on the
   same line of text. The generic label rule's bottom margin lifted the first
   of them above the others. */
.orgscope label{margin:0;font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)}
.orgscope select{height:26px;font-size:12px;max-width:280px}
.orgscope .faint{font-size:11px}
.planchip .pdel{display:inline-flex}
.planchip .pv.del{border-color:transparent;color:var(--muted);font-size:13px;line-height:1}
.planchip .pv.del:hover{border-color:var(--danger);color:var(--danger);background:#fff5f5}
.lvmove{padding:0 5px;min-width:0;font-size:11px;line-height:1.6}
.lvmove:disabled{opacity:.3;cursor:default}
.trail{font-size:11.5px}

/* an input with a button beside it ------------------------------------------ */
.withbtn{display:flex;gap:6px;align-items:center}
.withbtn > input{flex:1;min-width:0}
.withbtn > .btn{flex:none;white-space:nowrap;height:30px}   /* the field's height, not the button's */
/* Waiting for the Ctrl+V the button could not do on its own. */
.dropzone.awaiting{border-color:var(--accent);background:var(--accent-soft,#eef2ff)}

/* Folding everything at once. A control nobody can see is not a control: this
   was a ghost button, which is to say text, and it read as a label. */
.foldall-btn{display:inline-flex;align-items:center;gap:6px}
.foldall-btn .fa-chev{
  display:inline-flex;align-items:center;justify-content:center;
  width:16px;height:16px;border:1px solid currentColor;border-radius:3px;
  font-size:12px;line-height:1;font-weight:700}
.foldall-btn .fa-chev::before{content:'\2212'}          /* will fold them */
.foldall-btn.folded .fa-chev::before{content:'+'}        /* will unfold them */

/* ---------- the device sheet ----------
   One page, one device: the reference across the top, what it is under that,
   the device on the left with what it hangs on and what feeds it beside it, and
   what it is supposed to see along the bottom. The same bands the PDF draws,
   because the modal exists so that what you look at is what comes out. */
.fiche{background:var(--bg);color:var(--ink);font-size:12.5px}
.fiche .fh{display:flex;align-items:center;gap:10px;background:var(--fc);color:#fff;padding:7px 11px}
.fiche .fh-lab{font-size:10px;font-weight:600;letter-spacing:.04em;opacity:.85;text-transform:uppercase}
.fiche .fh-code{font-size:14px;letter-spacing:.01em}
.fiche .fh-st{margin-left:auto;font-size:10.5px;font-weight:600;opacity:.9;text-transform:uppercase}
.fiche .fband{background:#64748b;color:#fff;text-align:center;padding:4px 8px;margin-top:6px;
  font-size:10.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase}
.fiche .frows{width:100%;border-collapse:collapse;table-layout:fixed}
.fiche .frows th{width:38%;background:#c7d2fe;color:#1e3a8a;text-align:left;font-weight:700;
  font-size:11px;padding:5px 8px;border-bottom:1px solid #fff;vertical-align:top}
.fiche .frows td{background:#eef2ff;font-weight:600;padding:5px 9px;border-bottom:1px solid #fff;
  word-break:break-word}
/* The device and the bands beside it read as one block, not as a picture and a
   table that happen to be adjacent. */
.fiche .fbody{display:grid;grid-template-columns:42% 1fr;gap:8px;margin-top:6px;align-items:stretch}
/* The photograph fills the column beside the bands rather than setting the
   height of the page: a portrait shot of a camera on a mast is three times the
   height of the four rows next to it, and left to itself it pushes them to the
   top of an empty half-page. Absolutely positioned inside a cell with no
   intrinsic height, so the bands decide how tall the block is and the picture
   is cropped to fit — which is what the paper fiche does. */
.fiche .fphoto{background:var(--bg-sub);border:1px solid var(--line);
  position:relative;overflow:hidden;min-height:210px}
.fiche .fphoto a{position:absolute;inset:0;display:block}
.fiche .fphoto img{display:block;width:100%;height:100%;object-fit:cover}
.fiche .fside > .fband:first-child{margin-top:0}
.fiche .fcap{position:absolute;left:0;right:0;bottom:0;font-size:10.5px;color:#fff;padding:3px 7px;
  background:rgba(15,23,42,.55)}
.fiche .fnone{display:grid;place-items:center;min-height:150px;color:var(--faint);font-size:11px}
.fiche .fnone.sm{min-height:44px}
.fiche .fviews{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-top:8px}
.fiche .fviews figure{margin:0}
.fiche .fviews img{display:block;width:100%;height:auto;border:1px solid var(--line)}
.fiche .fviews figcaption{font-size:10.5px;color:var(--muted);margin-top:3px}
.fiche .fcomment{margin:6px 0 0;font-size:12px;color:#334155}
.fiche .ffoot{display:flex;flex-wrap:wrap;gap:10px;margin-top:10px;padding-top:7px;
  border-top:1px solid var(--line);font-size:10.5px;color:var(--faint)}
/* The dialog is a sheet of paper: wider than a form, and it scrolls. */
.modal-card.sheet{max-width:min(840px,94vw);padding:14px 16px 16px}
.sheet-body{max-height:min(74vh,900px);overflow:auto;padding:2px}
.sheet-body .empty{padding:40px 0;text-align:center;color:var(--faint)}
@media (max-width:720px){
  .fiche .fbody{grid-template-columns:1fr}
  .fiche .fviews{grid-template-columns:1fr}
}
@media print{
  .fiche .fviews img,.fiche .fphoto{break-inside:avoid}
}

/* ---------- a client's mark ----------
   Small on purpose: it goes beside a name, not on a wall. The box is the size
   the logo is allowed to be, so an empty one already says how much room there
   is, and a mark with a transparent background sits on the page's own colour
   rather than on a white square somebody has to crop out later. */
.logorow{display:flex;gap:14px;align-items:stretch}
/* Both halves of the row are one height because the row stretches them, not
   because two numbers were kept equal by hand — which is how they came to
   differ by eight pixels in the first place. The box states the width; the
   height comes from whichever of the two is taller. */
.logobox{width:126px;flex:none;border:1px solid var(--line);border-radius:var(--radius);
  display:grid;place-items:center;background:var(--bg-sub);overflow:hidden;font-size:11px;padding:8px}
.logobox img{max-width:100%;max-height:100%;display:block}
.logorow .dropzone{margin:0;min-height:0}

/* The rule behind a field, where the field is. A sentence under every code box
   at five levels is five paragraphs of the same paragraph; a mark you can point
   at is the same explanation without the wall. Focusable, so it is reachable
   without a mouse. */
.tip{display:inline-grid;place-items:center;width:14px;height:14px;margin-left:5px;
  border:1px solid var(--line);border-radius:50%;color:var(--faint);
  font-size:10px;font-weight:600;line-height:1;cursor:help;vertical-align:1px}
.tip:hover,.tip:focus{color:var(--fg);border-color:var(--fg);outline:none}

/* A client's mark beside its name in the list. Fixed square, and the row keeps
   the height its text gives it: an icon that pushes every line down turns a
   list of forty clients into a list you have to scroll. */
td.rowlogo,th.rowlogo{width:1px;padding-right:0}
td.rowlogo .mark{display:grid;place-items:center;width:20px;height:20px;margin:-3px 0;
  border-radius:4px;background:var(--bg-sub);overflow:hidden}
td.rowlogo .mark img{max-width:100%;max-height:100%;display:block}
td.rowlogo .mark.none{background:none}

/* The way out of the survey. Named on a wide screen; on a phone the toolbar has
   no room for words and the arrow stands alone, which is what it always was. */
.wsback{display:inline-flex;align-items:center;gap:5px}
@media (max-width: 780px){ .wsback-t{display:none} }

/* --- drawing on a photograph ------------------------------------------------
   The picture on the left at whatever size the window allows, the tools on the
   right in a fixed column: a drawer that reflows under the picture on a narrow
   screen puts the colour you are about to use off the bottom of it. */
.annot-card{max-width:min(1180px,96vw);padding:12px 14px 14px;display:flex;flex-direction:column}
.annot-body{display:flex;gap:12px;align-items:stretch;flex:1 1 auto;min-height:0}
/* The stage has a height of its own rather than one borrowed from the picture:
   the script then draws the picture as large as that box allows. */
.annot-stage{flex:1;min-width:0;height:min(70vh,760px);background:var(--bg-sub);
  border:1px solid var(--line);border-radius:var(--radius);display:grid;place-items:center;
  overflow:hidden}
.annot-stage canvas{display:block;touch-action:none;cursor:crosshair}
/* Save at the bottom of the dialog, on the right, where a dialog is finished. */
.annot-actions{display:flex;justify-content:flex-end;gap:6px;padding-top:10px;margin-top:10px;
  border-top:1px solid var(--line)}
/* Most of the window, and the PICTURE gets the room rather than the frame
   around it: the drawer keeps its width and the canvas takes what is left. */
.modal.annot.maxi .annot-card{max-width:92vw;width:92vw;height:88vh}
.modal.annot.maxi .annot-stage{height:auto}
/* Un bouton à deux états se colore quand il est enfoncé — la règle existait
   pour les outils d'annotation, elle vaut pour « emporter », qui est le même
   genre de bouton : une mission est sur cet appareil ou elle n'y est pas. */
.iconbtn[aria-pressed="true"],.btn[aria-pressed="true"]{background:var(--accent-soft,#e8f0fe);color:var(--accent,#1d4ed8)}
/* « Emporter » demande plus que la teinte ci-dessus. Un outil d'annotation est
   enfoncé pendant qu'on s'en sert et on le voit parce qu'on vient de le
   cliquer ; une mission emportée doit se lire d'un coup d'œil dans une liste de
   douze lignes, à bout de bras, dehors. Le bleu pâle sur fond blanc ne le
   faisait pas. C'est un état ACQUIS, pas un outil choisi, et le projet a déjà
   un bouton pour ça : `.btn.ok`, plein, blanc dessus, celui du « c'est fait ».
   Mêmes déclarations, à la lettre — aucune couleur inventée ici. */
.btn[data-offbtn][aria-pressed="true"]{background:var(--ok);border-color:var(--ok);color:#fff}
.btn[data-offbtn][aria-pressed="true"]:hover{background:var(--ok);border-color:var(--ok);filter:brightness(.94)}
.annot-drawer{width:212px;flex:none;display:flex;flex-direction:column;gap:8px}
.annot-tools{display:flex;flex-wrap:wrap;gap:4px}
.annot-lab{font-size:11px;color:var(--muted);margin-top:2px}
.annot-hint{font-size:11px;color:var(--faint);line-height:1.35}
.annot-colours{display:flex;gap:5px;flex-wrap:wrap}
.annot-sw{width:22px;height:22px;border-radius:5px;border:1px solid var(--line);cursor:pointer;padding:0}
.annot-sw.on{outline:2px solid var(--accent);outline-offset:1px}
.annot-icons{display:grid;grid-template-columns:repeat(5,1fr);gap:4px}
.annot-ic{display:grid;place-items:center;height:28px;border:1px solid var(--line);
  border-radius:5px;background:var(--bg);cursor:pointer;padding:0}
.annot-ic.on{border-color:var(--accent);background:var(--bg-sub)}
.annot-drop{margin:0;min-height:56px;font-size:11px}
.annot-foot{display:flex;gap:5px;margin-top:auto;padding-top:8px;border-top:1px solid var(--line)}
/* A photograph that has been drawn on says so, and one that cannot be drawn on
   does not offer. */
.photo.marked img{outline:2px solid var(--accent);outline-offset:-2px}
.photo .pmark{position:absolute;left:3px;top:3px;min-width:15px;height:15px;padding:0 3px;
  border-radius:8px;background:var(--accent);color:#fff;font-size:10px;line-height:15px;text-align:center}
.photo img{cursor:zoom-in}
.photo.ro img{cursor:default}
@media (max-width: 820px){
  .annot-body{flex-direction:column}
  .annot-drawer{width:100%}
  .annot-icons{grid-template-columns:repeat(10,1fr)}
}

/* A photograph and what was drawn on it, as one thing that can be laid out.
   The canvas is over the picture and never in the way of a click on it. */
.markwrap{position:relative;display:inline-block;line-height:0;max-width:100%}
.markwrap img{display:block;max-width:100%}
.markover{position:absolute;left:0;top:0;pointer-events:none}

/* --- reading a place out of a pasted link ---------------------------------- */
/* The bar spans whatever row it lands in: three of the four pickers put their
   coordinates in a grid and one puts them in a flex row, and a paste box one
   column wide is a paste box nobody can read a link in. */
.geodec{display:flex;gap:6px;align-items:center;margin-bottom:6px;grid-column:1/-1;flex-basis:100%}
.geodec > input{flex:1;min-width:0}
.geodec > .btn{flex:none;white-space:nowrap;height:30px}
.geolab{display:block;grid-column:1/-1;flex-basis:100%;margin-bottom:4px}
.geosaid{min-height:14px;margin:0 0 8px;grid-column:1/-1;flex-basis:100%}
.geosaid.good{color:var(--ok,#15803d)}
.geosaid.bad{color:var(--danger)}
/* An import row whose file named a code the catalogue does not know. */
.faint.bad{color:var(--danger)}
.geocopy{flex:none;white-space:nowrap;height:30px}
/* A field that was just filled in should look like it, or a decode that worked
   and one that found nothing look the same. */
@keyframes geoflash{from{background:var(--accent-soft,#eef2ff)}to{background:transparent}}
.geoflash{animation:geoflash .9s ease-out}

/* --- finding a row in a list that has grown -------------------------------- */
.listsearch{display:flex;gap:6px;align-items:center;flex:none}
/* Paging, out of the buttons the rest of the application already uses: the
   rule is the row they sit in, not a new control. */
.pager{display:flex;gap:6px;align-items:center;margin-top:10px;font-size:11.5px}
.listsearch input[type="search"]{width:230px;max-width:40vw}
.listsearch > .btn{height:30px;white-space:nowrap}
/* A heading you can press. It reads as a heading until it is hovered, because a
   row of blue links across the top of a table is a row of things that look
   pressed. */
th > .sortcol{color:inherit;text-decoration:none;display:inline-block;white-space:nowrap}
th > .sortcol:hover{color:var(--accent);text-decoration:none}
th > .sortcol.on{color:var(--accent)}
@media(max-width:720px){
  .listsearch input[type="search"]{width:100%}
  .listsearch{width:100%;order:3}
}

/* ---- the drawing library --------------------------------------------------
   Three chips per drawing, because those are the three styles the plan draws
   in: a symbol that only works in one of them is visible here rather than on
   somebody's report. */
.symchips{display:inline-flex;gap:6px;background:#e9eef5;padding:5px 7px;border-radius:8px}
.symchips svg{display:block}
.symchips > svg:nth-child(2),
.symchips > svg:nth-child(3){background:#1668c9;border-radius:5px}
/* A drawing that kept its own colours is drawn the same way in all three,
   because that is what the plan does with it: it has nothing to knock out
   white, so it keeps a white chip whatever the mission's marker style. Showing
   it on a coloured chip here would promise something the plan will not do. */
.symchips.own > svg:nth-child(2),
.symchips.own > svg:nth-child(3){background:#fff;box-shadow:0 0 0 1px #cbd5e1}
.symchip{display:inline-flex;align-items:center;justify-content:center;
  width:32px;height:32px;border-radius:6px;background:#fff;border:1px solid var(--line);flex:none}
/* Two tracks, not a flex row: the drawing and the name each get a column, so
   every name in a column starts at the same x instead of wherever the icon
   before it happened to end. A long key wraps rather than being cut — a
   truncated key is one nobody can copy into a spreadsheet. */
.symgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(196px,1fr));gap:2px 16px}
.symcell{display:grid;grid-template-columns:32px minmax(0,1fr);align-items:center;
  gap:10px;padding:5px 7px;margin-left:-7px;border-radius:6px}
.symcell:hover{background:var(--bg-soft,#f1f5f9)}
.symcell code{font-size:11px;line-height:1.35;color:var(--muted);overflow-wrap:anywhere}
/* `.card-body.tight` is zero padding, which is right for a TABLE that should
   span the card and wrong for everything else in one: prose, a search box or a
   grid sitting in a tight body starts on the card's own border.
   So the body stays tight — the table still spans — and everything that is not
   a table gets the card's own margins, by what it is rather than by which fold
   it is in. Two search boxes doing the same job on the same screen line up
   because one rule pads both, not because each screen was padded by hand. */
.card-body.tight > .hint{padding:12px 14px 0}
.card-body.tight > .hint:last-child{padding-bottom:12px}
.card-body.tight > .colswitch,.card-body.tight > .symfilter{padding:12px 14px 0}
.card-body.tight > .symgrid{padding:0 14px 14px}
/* A group inside the imported card: the same fold as the card, one step in. */
details.symgroup{margin-top:8px}
details.symgroup > summary .badge{font-weight:400}
.symfilter{display:flex;align-items:center;gap:10px;margin:0 0 12px}
.symfilter input{width:240px;height:30px}
details.symbuiltin > summary .faint{font-weight:400}
/* A summary that carries an action. The marker, the title and the button, with
   the button pushed to the end — and it must not fold the section on its way. */
details.mlkeys > summary{display:flex;align-items:baseline;gap:8px}
details.mlkeys > summary .grow{flex:1}
details.mlkeys > summary form{display:inline-flex;margin:0}
/* The agreement's own three facts. Full-width fields, because a name and a
   marché number are the length they are and a box sized for neither is a box
   you edit by scrolling inside it. */
#edit .form-grid input,#edit input[name=notes]{width:100%}

/* ---------- straightening a photographed plan ---------- */
/* The photograph, as big as the modal will allow: the corners are being put on
   the corners of a drawing, and that is not a thing you can do in a thumbnail. */
/* A column, so the stage can take whatever height is left after the header and
   the buttons — which is what makes maximising give the drawing the space it
   just gained rather than only making the window bigger. */
#rectModal .modal-card{display:flex;flex-direction:column;max-height:88vh}
#rectModal .rect-stage{flex:1 1 auto;min-height:200px}
/* Most of the window. Marking the corners of a plan is not a thing you can do
   in a box sized for a paragraph. */
#rectModal.maxi .modal-card{width:85vw;max-width:85vw;height:85vh;max-height:85vh}
#rectMax[aria-pressed="true"]{background:var(--accent-soft,#e8f0fe);color:var(--accent,#1d4ed8)}
.rect-stage{display:flex;align-items:center;justify-content:center;min-height:180px;
  background:var(--bg-alt,#f1f5f9);border:1px solid var(--line,#e2e8f0);border-radius:var(--radius,6px);
  padding:10px;overflow:hidden}
#rectCanvas{max-width:100%;display:block;touch-action:none;cursor:crosshair}
/* A shape in front of the name: a row of them, one chosen. Same square as the
   colour swatches in the annotator, so the two dialogs pick things the same
   way. */
.rect-marks{display:flex;gap:5px;flex-wrap:wrap;margin-top:5px}
.markbtn{width:28px;height:28px;padding:0;line-height:1;font-size:14px;cursor:pointer;
  border:1px solid var(--line,#e2e8f0);border-radius:5px;background:var(--bg,#fff);
  color:var(--ink,#0f172a);display:grid;place-items:center}
.markbtn:hover{border-color:var(--accent,#1d4ed8)}
.markbtn.on{border-color:var(--accent,#1d4ed8);background:var(--accent-soft,#e8f0fe);
  color:var(--accent,#1d4ed8)}

/* The QR code made for a phone (partials/qrdialog.ejs): black on white, large
   enough to scan from a screen, with its link under it. */
.qr-new{text-align:center;margin:0 0 12px}
.qr-img{width:220px;margin:0 auto 6px}
.qr-img svg{display:block;width:100%;height:auto}
.qr-new input{width:100%;font-family:var(--mono,monospace);font-size:11.5px}
.qr-pin{font-family:var(--mono);font-size:26px;font-weight:700;letter-spacing:.25em;color:var(--ink);margin:2px 0 4px}
/* The list of codes grows with every code made: the dialog scrolls rather than
   pushing the new code off the top of the screen. */
#qrModal .modal-card{max-height:92vh;overflow-y:auto}
/* The current and the ended codes, folded side by side; the open one's list
   sits under both, the width of the dialog. */
.qr-folds{display:flex;gap:16px}
.qr-folds details.advanced{flex:1 1 0;min-width:0}
.qr-fold-body{margin-top:10px}
