  :root{
    --ink:#1a2332;
    --ink-soft:#4a5568;
    --paper:#f6f5f2;
    --panel:#ffffff;
    --line:#dcdad2;
    --accent:#c2540f;
    --accent-soft:#f2e3d5;
    --ok:#2f6b4f;
    --ok-soft:#e2efe8;
    --bad:#a5311f;
    --bad-soft:#f7e4e0;
    --steel:#3a5a78;
    --fira:#f7ce46;   /* colour-picked from FIRA's own logo */
    /* Shopify's own green, picked by Sales. It sat in `static/sales.css` while
       only Sales drew it; it is here from 6 October 2026 because Finance draws
       account tags on Transactions and Balance in each account's colours and
       `sales.css` loads only on `/sales/` pages — so `var(--shopify-dark)` was
       undefined on a Finance screen and the badge came out unpainted.

       Finance restated the hex rather than leave it broken, which is two copies
       of one colour and the drift rule 4 of `claude/two-chats-one-tree.md`
       exists to prevent. Two modules drawing it is the test for moving it, and
       `--fira` above is the precedent: a brand colour belongs here the moment
       more than one screen uses it.

       `-dark` rather than the `-soft` the rest of this block uses, because that
       is what both callers already say and renaming would break them both. */
    --shopify:#98b756;
    --shopify-dark:#658845;
    --steel-soft:#e4ecf3;
    --purple:#5b4b8a;
    --purple-soft:#eae6f4;
    --teal:#0f766e;
    --teal-soft:#dcf3f0;
    --magenta:#a3266b;
    --magenta-soft:#f6e0ec;
    --brown:#8a5a34;
    --brown-soft:#f0e3d4;
    --olive:#6b7a2e;
    --olive-soft:#eaf0d8;
    --indigo:#4338a8;
    --indigo-soft:#e6e3f8;
    --coral:#c1543f;
    --coral-soft:#f7e2dd;
    --cyan:#1c7a9c;
    --cyan-soft:#dcf0f7;
    --slate:#51606e;
    --slate-soft:#e6eaee;
    --gold:#8a6a00;
    --gold-soft:#f7edcf;
    --warn:#f2990f;
    --warn-soft:#faf0d9;
    --radius:3px;
    --module-bar:38px;
    font-size:15px;
  }
  *{box-sizing:border-box;}
  html,body{margin:0;padding:0;}
  body{
    background:var(--paper);
    color:var(--ink);
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    -webkit-font-smoothing:antialiased;
  }
  .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Consolas,"Liberation Mono",Menlo,monospace;}

  /* --module-bar tall less than the viewport: the bar above #app is
     outside it, and 100vh here would push every page into a scrollbar
     it does not need. */
  #app{display:flex; min-height:calc(100vh - var(--module-bar));}
  nav#side{width:190px; flex:none; background:var(--ink); color:#dfe4ea; display:flex; flex-direction:column;
      position:relative; z-index:30; transition:width .16s ease;}
  /* The z-index is load-bearing, not decoration. The arrow hangs 11px past the
     sidebar's right edge, over whatever the page has put there, and a screen
     whose own toolbar sat at `z-index:20` painted over it — the arrow could
     not be clicked at all. Found by trying to click it. (That screen was the
     Assembly Instructions Builder, removed 14 September 2026; the rule stays,
     because the next screen with a toolbar will do the same thing.) */

  /* ---- the sidebar folded away -------------------------------------------
     Bruno: "Can we have the left side vertical menu tucked away? This would be
     useful for the Assembly Instructions Builder in particular, but also in
     some other busy work spaces." (The Builder is gone as of 14 September
     2026. The busy work spaces are not.)

     Folded it is a rail: the company mark stays, everything else goes. Not
     `display:none` on the nav — the mark is how you know which app you are in,
     and a rail that keeps it reads as folded rather than as broken. Which is
     what his reference picture shows.

     `html.side-folded` rather than a class on the nav itself, because the
     class is set by an inline script before the first paint. Anything set
     later paints the open sidebar and then folds it, which is a flinch on
     every single page load. */
  html.side-folded nav#side{width:46px;}
  html.side-folded nav#side .brand{padding:18px 0 14px; justify-content:center;}
  html.side-folded nav#side .brand div{display:none;}
  /* Everything but the mark and the arrow, whatever a module has put in its
     sidebar. Named by hand at first — .tabs, .footer, .whoami, #undoBtn — which
     is Shopfloor's list, and Sales' sidebar has blocks of its own that went on
     showing, overflowing a 46px rail with "Recent changes" and "Document
     settings". A rule that says what stays cannot be out of date; a rule that
     lists what goes is out of date the moment a module adds anything. */
  html.side-folded nav#side > *:not(.brand):not(#sideFold){display:none;}

  /* The arrow, and it is not a button any more.

     Bruno: "let's make the button itself a little bit more subtle … I would
     like it to look like a simple triangle of the menu background colour
     extending into the right and vice versa." And then, having lived with it:
     "it should not be shown only on mouse over, rather it has to be visible all
     the time. Also let's make grey as the background of the work space so it
     looks like it's part of it when it's extended, but colour of the background
     of the vertical menu when collapsed."

     Which is the better rule, and it is an inversion: the triangle belongs to
     whichever side it is poking into.

       extended  — `--paper`, the workspace's own grey. It reads as a notch the
                   workspace has cut into the menu, and it points left, which is
                   where the menu is about to go.
       collapsed — `--ink`, the menu's own colour. Now it is the menu reaching
                   out into the workspace, pointing right, which is where the
                   menu will come back from.

     Full opacity in both states. It was .9 rising to 1 on hover, and against a
     dark panel a dark triangle at .9 was invisible until the mouse found it —
     which is exactly what he was describing. (The dark panel belonged to the
     Assembly Builder, since removed; a triangle you have to hunt for is a
     fault on any background.)

     And it does not react to the mouse AT ALL: "please remove the mouse over
     colour change of that triangular button." So there is no :hover rule here
     and nothing to transition. The pointer cursor is the whole of the
     affordance, which is what the rest of the app does too. */
  #sideFold{position:absolute; top:16px; right:-9px; width:18px; height:34px;
      padding:0; border:0; background:none; cursor:pointer; z-index:5;
      display:flex; align-items:center; justify-content:flex-start;}
  #sideFold::after{content:""; display:block; width:0; height:0;
      border-top:9px solid transparent; border-bottom:9px solid transparent;
      /* Coloured on the RIGHT, so the apex is on the left. */
      border-right:9px solid var(--paper);}
  /* Folded: the same triangle the other way round, in the menu's colour. */
  html.side-folded #sideFold::after{border-right:0;
      border-left:9px solid var(--ink);}
  html.side-folded #sideFold{justify-content:flex-end;}

  /* Folded, the whole rail is the way back. "Let's make it possible to click
     anywhere on the menu to expand." A 9px triangle is a small target and the
     rail beside it is 46px of nothing else — so the rail is the button, and
     the triangle is what says so. */
  html.side-folded nav#side{cursor:pointer;}
  /* Not a control. Bruno: "the logo is clickable but it leads nowhere so let's
     just make the logo passive piece of graphic, not a button." It was a link
     to the module's own home, which from most screens is somewhere you already
     are — and once the folded rail became clickable, a link inside it was one
     thing that did something else. */
  nav#side .brand{padding:18px 16px 14px; border-bottom:1px solid #2c3849; display:flex; align-items:center; gap:10px;}
  nav#side .brand .logo{display:block; width:32px; height:32px; flex:none;}
  nav#side .brand b{font-size:15px; letter-spacing:.02em;}
  nav#side .brand div.tag{font-size:11px; color:#8a95a5; margin-top:2px; letter-spacing:.04em;}
  nav#side .tabs{padding:10px 0; flex:1;}
  nav#side button.tabbtn{display:block; width:100%; text-align:left; background:none; border:none; color:#b7c0cd;
    padding:10px 16px; font-size:13.5px; cursor:pointer; border-left:3px solid transparent; font-family:inherit;}
  nav#side button.tabbtn:hover{background:#222d3d; color:#fff;}
  nav#side button.tabbtn.active{background:#222d3d; color:#fff; border-left-color:var(--accent); font-weight:600;}
  nav#side .footer{padding:12px 16px; font-size:11px; color:#65707f; border-top:1px solid #2c3849;}
  nav#side #undoBtn{display:block; width:calc(100% - 32px); margin:0 16px 10px; background:#222d3d; color:#b7c0cd; border:1px solid #33415a; border-radius:6px; padding:8px 10px; font-size:12.5px; cursor:pointer; font-family:inherit; text-align:left;}
  nav#side #undoBtn:not(:disabled):hover{background:#2b3850; color:#fff;}
  nav#side #undoBtn:disabled{opacity:.4; cursor:default;}

  main{flex:1; padding:26px 34px; max-width:1180px;}
  /* …except on the screens that are a wide table rather than something to read.
     Bruno: "since collapse left hand vertical menu now leaves a lot more space,
     let's make use of it, especially in the busy work spaces such as Inventory
     and B2B Sales." It did not, and the menu was never the reason — the 1180px
     cap above ate the whole gain. Measured at 1440px: folding took the unused
     strip on the right from 70px to 214px and left the table at 1078px, exactly
     where it was. At 1920px, 694px of window was doing nothing.

     Per screen, not everywhere: 1180px is a reading measure and it is right for
     a form and for prose. Which screens is decided in core/ui.py WIDE_PAGES,
     and it arrives as `data-wide` on the module bar rather than as a class on
     `main` — because `main` is written in both shells and one of the two is not
     ours to keep a line in. See templates/_modules.html. */
  body:has(#modules[data-wide]) main{max-width:none;}

  /* The line under a live search box: how much of the list you are looking at.
     Bruno's model for this was the component picker, which shows a short list
     and no count because it caps at fifteen. A list of 142 shop orders needs
     to say how many of them are still on screen, or a filter that matched two
     rows looks like a list with two rows in it. See static/livesearch.js. */
  .ls-count{font-size:11.5px; color:var(--ink-soft); margin:-8px 0 12px;}
  h1{font-size:20px; margin:0 0 4px; letter-spacing:-0.01em;}
  .subtitle{color:var(--ink-soft); font-size:13px; margin-bottom:22px;}
  .panel{background:var(--panel); border:1px solid var(--line); border-radius:var(--radius); padding:20px; margin-bottom:18px;}
  .panel h2{font-size:14px; text-transform:uppercase; letter-spacing:.06em; color:var(--ink-soft); margin:0 0 14px; font-weight:700;}

  /* Running low, at the top of Inventory. 19 September 2026.

     `--warn` rather than `--bad`: being low on screws is something to do this
     week, not something that has gone wrong. Red here would sit on the screen
     most days and teach people that red means nothing, which is what a real
     failure then has to shout over. Only the left edge is tinted, so the panel
     still reads as one of the page's panels rather than as an alert box
     dropped on top of it.

     Only drawn when there is something in it — see the note in the template. */
  .lowstock{border-left:3px solid var(--warn); background:var(--warn-soft);}
  .lowstock h2{color:var(--ink);}
  /* The three sections on Vendors, 20 September 2026. Nothing but spacing —
     each is a `.panel`, and they are told apart by their headings and by the
     tint on the middle one. */
  .vendors-section{margin-bottom:18px;}
  /* One supplier's order. The block is the unit here: heading, the parts, and
     the same list as text — so it gets a rule above it rather than a border
     all round, which at four or five blocks would be a stack of boxes inside a
     box. */
  .order-block{margin-top:16px; padding-top:12px; border-top:1px solid #e4d9bd;}
  .order-block:first-of-type{border-top:0; padding-top:0;}
  .order-block h4{margin:0 0 6px; font-size:13.5px;}
  .order-block h4 .per{font-weight:400; margin-left:6px;}
  /* The same quiet count beside the thresholds heading inside a vendor's fold.
     `.per` is otherwise scoped to `.tree-name`, so without this the count came
     out as bold as the heading and read as part of it. */
  .tree-children h4 .per{font-weight:400; margin-left:6px;
    color:var(--ink-soft); font-size:11.5px;}
  /* The text to paste lives behind a TXT button now, in the shared `.copybox`
     dialog the run list already uses — so there are no rules for it here. The
     button sits in the block's heading; `.order-block h4` keeps it on the
     baseline of the words beside it. */
  .order-block h4 button{margin-left:8px; vertical-align:baseline;}
  /* Wrapping rows rather than a table: this is usually two or three parts and
     occasionally twenty, and a table of three rows with headings is heavier
     than the thing it is describing. */
  .lowstock-rows{display:flex; flex-direction:column; gap:2px; margin-top:12px;}
  .lowstock-row{display:flex; align-items:center; gap:8px; padding:5px 8px;
    border-radius:var(--radius); color:var(--ink); text-decoration:none;
    background:rgba(255,255,255,.55);}
  .lowstock-row:hover{background:#fff;}
  .lowstock-row .nm{font-weight:600; flex:1; min-width:0;
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
  /* The figure that matters, and then what it is measured against — the count
     bold and the level quiet, so the eye lands on "3" rather than on "of 50". */
  .lowstock-have{font-weight:700; font-variant-numeric:tabular-nums;}
  .lowstock-of{color:var(--ink-soft); font-size:12px; white-space:nowrap;}
  @media (max-width:640px){
    .lowstock-row{flex-wrap:wrap;}
    .lowstock-row .nm{flex:1 1 100%; order:3; white-space:normal;}
  }

  .row{display:flex; gap:10px; flex-wrap:wrap; align-items:center;}
  .grid{display:grid; gap:12px;}
  .grid.cols-2{grid-template-columns:1fr 1fr;}
  .grid.cols-3{grid-template-columns:1fr 1fr 1fr;}

  label{display:block; font-size:11.5px; color:var(--ink-soft); margin-bottom:4px; letter-spacing:.02em;}
  input,select,textarea{width:100%; padding:7px 9px; border:1px solid #c9c6bc; border-radius:var(--radius);
    font-size:13.5px; background:#fff; color:var(--ink); font-family:inherit;}
  /* A tick box is not a field to fill in, so it does not take a field's width.

     The rule above says `width:100%` for every input, which is right for text,
     a number or a date and wrong for the two inputs that are a fixed little
     square: the box gets stretched to the width of the form and the tick ends
     up floating in the middle of it. Bruno, 20 September 2026: *"Let's have
     these check boxes aligned to left."*

     `.checkfield input` below has said `width:auto` since the part editor was
     built, which is why most of the app looks right — but that only helps
     markup that remembered to use `.checkfield`, and the Invoicing board's
     column headers did not. Asked for by the Invoicing chat as a one-line fix
     in a file of mine rather than a workaround in one of theirs, which was the
     right call: a shared wrong default should be fixed where it is written,
     not routed around once per module.

     Radio buttons for the same reason, though the app has none today — the
     rule above catches them identically, and finding that out screen by screen
     is exactly what this is meant to prevent. */
  input[type=checkbox],input[type=radio]{width:auto;}

  input:focus,select:focus,textarea:focus{outline:2px solid var(--steel); outline-offset:0; border-color:var(--steel);}
  .field{margin-bottom:10px;}
  .checkfield{display:flex; align-items:center; gap:8px; margin-bottom:10px;}
  .checkfield input{width:auto;}
  .checkfield label{margin:0;}

  /* The three "what this part is" ticks in the part editor, 19 September 2026.
     Bruno asked for them bunched together with small icons on the left, "similar
     fashion as the Zn and bending icons".

     A box of their own rather than three loose rows: they answer one question
     between them and they are what decides the part's number, and a tinted
     block says that without a heading having to. Tighter row spacing inside it
     than the 10px `.checkfield` gives on its own, because at that spacing three
     rows read as three unrelated settings. */
  /* The grey box that gathers a handful of controls into one thing. Two of
     them in the part editor since 29 September 2026 — what the part is, and
     its recipe — so the look lives here once rather than being described
     twice. Bruno, asking for the second: *"in Edit part menu, let's gently
     highlight the Recipe section. Same grey box as with part type with three
     icons would work I think."* */
  .boxed{background:var(--paper); border:1px solid #e4e0d6;
    border-radius:var(--radius); padding:10px 12px 4px; margin-bottom:12px;}
  .whatis{background:var(--paper); border:1px solid #e4e0d6;
    border-radius:var(--radius); padding:10px 12px 4px; margin-bottom:12px;}
  .whatis .checkfield{margin-bottom:6px;}
  /* The mark takes the muted text colour, not the ink: it is a label for the
     row, not a control in it, and at full strength three of them down the left
     edge pull harder than the words they belong to. It lifts on hover with the
     row so the pair still reads as one thing. */
  .whatis-mark{color:var(--ink-soft); flex:none;}
  .whatis .checkfield:hover .whatis-mark{color:var(--ink);}
  /* The explanation after the label, on the same line where there is room and
     wrapping under it where there is not. `display:inline` rather than a block,
     so it does not push the tick out of line with its own words. */
  .whatis-note{display:inline; margin-left:6px; color:var(--ink-soft);}
  /* The threshold, indented under the ticks it belongs to and lined up with
     their labels rather than with their icons. */
  .whatis-level{margin:2px 0 8px 28px; display:flex; align-items:center;
    gap:8px; flex-wrap:wrap;}
  .whatis-level label{margin:0; white-space:nowrap;}
  .whatis-level .hint{margin:0;}
  /* The recipe, in the same box. Its own rule only for the padding at the
     foot: the list inside ends with a button rather than a field, and 4px
     under a button reads as the box being too small for it. */
  .recipebox{padding-bottom:10px;}
  .recipebox > .field{margin-bottom:0;}

  /* --- low stock alert thresholds, on the Vendors screen ----------------
     One line per watched part: what it is, a bar showing where the threshold
     sits against what is on the shelf, and the number itself as a field.
     Deliberately not a table — Bruno asked for something that is *"not another
     inventory list"*, and a row that reads left to right in one line is the
     difference between a setting and a spreadsheet. */
  .thresholds{display:flex; flex-direction:column; gap:2px;}
  .threshold-row{display:flex; align-items:center; gap:8px; padding:4px 8px;
    border-radius:var(--radius); font-size:13px;}
  .threshold-row:nth-child(odd){background:#fbfaf7;}
  .threshold-row .nm{flex:1; min-width:0; overflow:hidden;
    text-overflow:ellipsis; white-space:nowrap;}
  .threshold-have{color:var(--ink-soft); font-size:12px; white-space:nowrap;
    min-width:90px; text-align:right;}
  /* The bar. Grey when there is room, gold when the alert is on — the same
     gold the "to order" badge uses, so one colour means one thing across the
     screen. Fixed width so the numbers after it line up down the column. */
  .threshold-bar{flex:none; width:64px; height:6px; border-radius:3px;
    background:#e8e4d9; overflow:hidden;}
  .threshold-bar > span{display:block; height:100%; background:#b9b2a0;}
  .threshold-row.is-low .threshold-bar > span{background:var(--gold, #c2820f);}
  .threshold-set{display:flex; align-items:center; gap:6px; margin:0;}
  .threshold-set label{margin:0; font-size:11px; color:var(--ink-soft);
    white-space:nowrap;}
  .threshold-set input{width:78px; text-align:right; padding:3px 6px;
    font-size:12.5px;}
  /* The standard, shown as placeholder text. Chromium's own placeholder grey
     is dark enough that 27 untouched rows read as 27 numbers somebody typed,
     which is the opposite of what the empty box is saying — so it is lightened
     here until it reads as "nothing set". */
  .threshold-set input::placeholder{color:#a8a396; opacity:1;}

  /* What the part costs — a fact about the supplier, which is what a vendor's
     list is about. Quiet, because the row's loud figure is the stock. */
  .part-cost{color:var(--ink-soft); font-size:12px; white-space:nowrap;
    font-variant-numeric:tabular-nums;}
  /* Where the bar, the stock and the box would be on a part nothing watches.
     Empty on purpose: a threshold box there would be an empty field waiting to
     be filled in. It holds the column so the Order buttons line up. */
  .threshold-slot{flex:none; width:246px;}
  /* Order. The only thing in one of these rows that reacts to the mouse —
     Bruno asked for a list that is "not clickable or editable" apart from this
     one link. Sized to the row rather than to a form, so it does not stretch
     it. */
  .threshold-row > a.buy{flex:none; padding:2px 8px; font-size:11.5px;
    margin-left:2px;}

  button{cursor:pointer; border:1px solid var(--ink); background:var(--ink); color:#fff;
    padding:7px 14px; border-radius:var(--radius); font-size:13px; font-family:inherit; font-weight:600;}
  button:hover{opacity:.88;}
  button.ghost{background:#fff; color:var(--ink); border-color:#c9c6bc; font-weight:500;}
  button.danger{background:var(--bad); border-color:var(--bad);}
  button.accent{background:var(--accent); border-color:var(--accent);}
  button.sm{padding:4px 9px; font-size:12px;}
  /* A link that opens a dialog is still a link — that is what keeps the screen
     working with the script off — but it has to look like the button it stands
     in for. `a.sm.ghost` was the only one styled, so `a.sm.accent` came out as
     a plain underlined link; the shared geometry is now in one place and only
     the colours differ, which is how `button` and `button.accent` already
     relate to each other a few lines above. */
  a.sm.ghost, a.sm.accent{display:inline-block; cursor:pointer;
    border:1px solid #c9c6bc; background:#fff; color:var(--ink);
    padding:4px 9px; border-radius:var(--radius); font-size:12px;
    font-weight:500; text-decoration:none; font-family:inherit;}
  a.sm.accent{background:var(--accent); border-color:var(--accent); color:#fff;
    font-weight:600;}
  a.sm.ghost:hover, a.sm.accent:hover{opacity:.88;}
  button:disabled{opacity:.4; cursor:not-allowed;}

  table{width:100%; border-collapse:collapse; font-size:13px;}
  th{text-align:left; font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--ink-soft);
     border-bottom:2px solid var(--ink); padding:8px 8px; font-weight:700;}
  td{padding:8px; border-bottom:1px solid var(--line); vertical-align:middle;}
  tr:hover td{background:#faf9f6;}
  .num{text-align:right; font-variant-numeric:tabular-nums;}

  .badge{display:inline-block; padding:2px 6px; border-radius:20px; font-size:10px; font-weight:700; letter-spacing:.01em; white-space:nowrap;}
  .badge.ok{background:var(--ok-soft); color:var(--ok);}
  .badge.bad{background:var(--bad-soft); color:var(--bad);}
  .badge.steel{background:var(--steel-soft); color:var(--steel);}
  .badge.accent{background:var(--accent-soft); color:var(--accent);}
  .badge.purple{background:var(--purple-soft); color:var(--purple);}
  .badge.teal{background:var(--teal-soft); color:var(--teal);}
  .badge.magenta{background:var(--magenta-soft); color:var(--magenta);}
  .badge.brown{background:var(--brown-soft); color:var(--brown);}
  .badge.olive{background:var(--olive-soft); color:var(--olive);}
  .badge.indigo{background:var(--indigo-soft); color:var(--indigo);}
  .badge.coral{background:var(--coral-soft); color:var(--coral);}
  .badge.cyan{background:var(--cyan-soft); color:var(--cyan);}
  .badge.slate{background:var(--slate-soft); color:var(--slate);}
  .badge.gold{background:var(--gold-soft); color:var(--gold);}
  .badge.muted{background:#ececea; color:#6b6b66;}
  .dot{display:inline-block; width:9px; height:9px; border-radius:50%; flex:none;}

  .empty{color:#8f8b80; font-size:13px; padding:18px 4px; text-align:center;}
  .hint{font-size:12px; color:var(--ink-soft); margin-top:6px;}

  .modal-bg{position:fixed; inset:0; background:rgba(20,24,32,.45); display:flex; align-items:flex-start; justify-content:center; padding:40px 20px; overflow:auto; z-index:50;}
  /* On a short screen the 40 px above and below a dialog is the difference
     between reaching the Save button and having to scroll for it. Every dialog
     benefits; the bending one, which is the tallest in the app, benefits most. */
  @media (max-height:760px){ .modal-bg{padding:16px 20px;} }
  .modal{background:#fff; border-radius:var(--radius); padding:22px; width:100%; max-width:560px; border:1px solid var(--line);}
  .modal.wide{max-width:720px;}
  .modal h3{margin:0 0 14px; font-size:15px;}
  .modal-actions{display:flex; justify-content:flex-end; gap:8px; margin-top:16px;}
  .x{cursor:pointer; float:right; color:var(--ink-soft); font-size:18px; line-height:1;}

  .toast{position:fixed; bottom:20px; right:20px; background:var(--ink); color:#fff; padding:10px 16px; border-radius:var(--radius); font-size:13px; z-index:100; box-shadow:0 4px 14px rgba(0,0,0,.2);}
  .comp-line{display:grid; grid-template-columns:1fr 90px 30px; gap:8px; margin-bottom:8px; align-items:center;}

  /* ---- inventory tree ---- */
  .tree-row{display:flex; align-items:center; gap:8px; padding:7px 4px; border-bottom:1px solid #eeece5;}
  .tree-row:hover{background:#faf9f6;}
  .tree-row.drop-hover{background:var(--ok-soft); outline:2px dashed var(--ok); outline-offset:-2px;}
  .tree-toggle{width:20px; flex:none; text-align:center; cursor:pointer; color:var(--ink-soft); user-select:none; display:inline-flex; align-items:center; justify-content:center;}
  .tree-toggle.leaf{visibility:hidden;}
  .tri{display:inline-block; font-size:13px; line-height:1; transition:transform .15s ease; transform-origin:47% 50%;}
  .tri.open{transform:rotate(90deg);}
  .tree-name{flex:1; min-width:0; display:flex; align-items:baseline; gap:8px;}
  .tree-name .nm{font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
  .id{color:#767b85; font-size:13.5px; font-weight:700;}
  .tree-name .id{flex:none;}
  .tree-name .id.shrinkable{flex:1 1 0; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
  .tree-name .per{color:var(--ink-soft); font-size:11.5px; font-family:ui-monospace,monospace; flex:none;}
  .tree-badges{display:flex; gap:6px; flex:none;}
  .tree-badges .slot{width:70px; flex:none; overflow:hidden;}
  /* Wide enough for the longest process name there actually is — "Laser
     cutting (FIBER)" — because a tag chopped off mid-word is worse than a
     narrower name column beside it. The tags themselves are set smaller and
     tighter than elsewhere in the app to pay for it: three of them in a row
     that also has to hold a count, a quantity box, Apply and Edit. */
  .tree-badges .slot.proc{width:108px;}
  .tree-badges .slot.shared{width:46px;}
  .tree-badges .badge{font-size:9px; padding:1px 5px; letter-spacing:0;
      max-width:100%; overflow:hidden; text-overflow:ellipsis;}
  /* 8 characters ("2000 kom") is the widest stock reading in the database, and
     this holds it. It was 90px, which parked the bending icon a clear 40px away
     from the number it belongs beside. */
  .tree-stock{font-family:ui-monospace,monospace; font-size:13.5px; min-width:72px; text-align:right; flex:none;}
  .tree-price{display:flex; flex-direction:column; align-items:flex-end; gap:1px; min-width:150px; flex:none;}
  .tree-price .unit{color:var(--ink-soft); font-size:11.5px; font-family:ui-monospace,monospace;}
  .tree-price .line{font-family:ui-monospace,monospace; font-size:13.5px; font-weight:700;}
  .tree-price-group{display:flex; gap:16px; flex:none;}
  .tree-price-group .tree-price{min-width:118px;}
  .tree-price-group .tree-price.col-inhouse .line{color:var(--accent);}
  .tree-price-group .tree-price.col-outsource .line{color:var(--steel);}
  .tree-price-group .tree-price.col-purchased .line{color:var(--purple);}
  /* The Outsourced column as a control. Bruno, 11 September 2026, on having
     the "If outsourced instead" section taken out of the part editor: *"let's
     have a possibility to add outsourced prices right there in the Production
     Cost Report for each individual part"*.

     The link is the cell, so the figure keeps its place and its alignment and
     nothing is added to the row — a report that grew a column of buttons would
     be trading one kind of clutter for another. It looks like the text it
     replaced until the pointer is on it. */
  .tree-price .quote-cell{display:flex; flex-direction:column;
      align-items:flex-end; gap:1px; color:inherit; text-decoration:none;
      border-radius:var(--radius); padding:1px 3px; margin:-1px -3px;}
  .tree-price .quote-cell:hover{background:var(--steel-soft);}
  .tree-price .quote-cell:hover .line{text-decoration:underline;}
  /* Where no quote has been given. The shape Bruno asked for in place of every
     empty field in the app: "let's just have a + Add or + New button and make
     that everywhere." Quiet until hovered, because most rows have no quote and
     a column of blue links would read as a column of something to do. */
  .tree-price .quote-add{font-size:11.5px; color:var(--ink-soft);
      opacity:.55;}
  .tree-row:hover .quote-add{opacity:1; color:var(--steel);}

  /* The verdict inside the quote dialog: what making it here costs, and what
     the number just typed means against that. The only reason the number is
     ever entered. */
  .quote-compare{background:var(--paper); border:1px solid var(--line);
      border-radius:var(--radius); padding:9px 11px; margin:4px 0 2px;
      font-size:12.5px;}
  .quote-compare .quote-inhouse{color:var(--ink-soft);}
  .quote-compare .quote-inhouse b{color:var(--accent);
      font-family:ui-monospace,monospace;}
  .quote-verdict{margin-top:3px; font-weight:600; min-height:16px;}
  /* Dearer is the ordinary answer and reads calm; cheaper is the one worth
     looking twice at, because it is the case that might change what the shop
     actually does. */
  .quote-verdict.dearer{color:var(--steel);}
  .quote-verdict.cheaper{color:var(--gold);}
  .quote-verdict.level{color:var(--ink-soft);}

  .tree-price-header{display:flex; gap:16px; flex:none;}
  .tree-price-header .col{min-width:118px; text-align:right; font-size:11px; font-weight:600; color:var(--ink-soft); text-transform:uppercase; letter-spacing:.03em;}
  .tree-price-header .col.col-inhouse{color:var(--accent);}
  .tree-price-header .col.col-outsource{color:var(--steel);}
  .tree-price-header .col.col-purchased{color:var(--purple);}
  /* `align-items:center`, and it matters: without it the row's children
     stretch to the tallest of them — the 31px quantity box — so the Edit link,
     being a direct child, was drawn 31px tall next to a 24px Apply button and
     sat 3.5px higher than it. Bruno: "What happened with this Edit button? It's
     crooked."

     The arrows keep stretching, because that is inside their own form. */
  .tree-actions{display:flex; gap:6px; flex:none; align-items:center;}
  .tree-children{margin-left:22px; border-left:1px dashed #c9c6bc; padding-left:4px;}
  .pick-list{max-height:220px; overflow-y:auto; border:1px solid #c9c6bc; border-radius:var(--radius); margin-top:4px;}
  .pick-row{padding:7px 9px; font-size:13.5px; cursor:pointer; border-bottom:1px solid #eeece5;}
  .pick-row:last-child{border-bottom:none;}
  .pick-row:hover{background:#faf9f6;}
  .clip{color:var(--ink-soft); flex:none; cursor:pointer; font-size:13px;}
  .procsection{border:1px solid var(--line); border-radius:var(--radius); padding:14px 14px 4px; margin-bottom:16px; background:#fbfaf7;}
  .procsection h4{margin:0 0 4px; font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--accent); font-weight:700;}
  .procsection .sechint{font-size:12px; color:var(--ink-soft); margin-bottom:12px;}

  /* ---- toggle switch ---- */
  .toggle-switch{display:inline-flex; border:1px solid #c9c6bc; border-radius:20px; overflow:hidden;}
  .toggle-switch button{border:none; background:#fff; color:var(--ink-soft); padding:7px 16px; font-size:13px; font-weight:600; border-radius:0;}
  .toggle-switch button.active{background:var(--ink); color:#fff;}
  .toggle-switch button:hover{opacity:1;}

  /* ---- simple on/off switch (e.g. "In production") ---- */
  .onoff{display:inline-flex; align-items:center; gap:7px; cursor:pointer; user-select:none; flex:none;}
  .onoff .track{position:relative; width:36px; height:20px; background:#c9c6bc; border-radius:20px; flex:none; transition:background .15s;}
  .onoff .track .knob{position:absolute; top:2px; left:2px; width:16px; height:16px; background:#fff; border-radius:50%; transition:left .15s; box-shadow:0 1px 2px rgba(0,0,0,.25);}
  .onoff.on .track{background:var(--accent);}
  .onoff.on .track .knob{left:18px;}
  .onoff .lbl{font-size:12px; color:var(--ink-soft); font-weight:600;}
  .onoff.on .lbl{color:var(--accent);}

  .procgroup{margin-bottom:20px;}
  .procgroup h3{font-size:13px; text-transform:uppercase; letter-spacing:.05em; color:var(--ink-soft); margin:0 0 8px; display:flex; align-items:center; gap:8px;}
  .procgroup .featuregroup{margin:10px 0 0 18px; padding-left:10px; border-left:2px solid var(--line);}
  .procgroup .featuregroup h4{font-size:12.5px; font-weight:700; color:var(--ink-soft); margin:0 0 6px; display:flex; align-items:center; gap:8px;}
  .task-row{display:flex; align-items:center; gap:10px; padding:9px 6px; border-bottom:1px solid var(--line); transition:background .2s;}
  .task-row.done{background:var(--ok-soft);}
  .task-row .tname{flex:1;}
  .task-row .tname .spec{color:var(--ink-soft); font-size:11.5px;}
  .task-progress{font-family:ui-monospace,monospace; font-size:12.5px; color:var(--ink-soft); min-width:110px; text-align:right;}
  .bar{height:5px; background:#e9e7e0; border-radius:3px; overflow:hidden; margin-top:3px;}
  .bar > div{height:100%; background:var(--ok);}
  .quick-complete{display:flex; gap:6px; align-items:center; flex:none;}
  /* Room for the number spinner, which Chrome draws inside the field on the
     right — over the top of a right-aligned placeholder. At 64px wide with 9px
     of padding there were 44px for "±qty" and the spinner both, so the y was
     cut in half. Bruno: "y in Qty is cut off." */
  .quick-complete input{width:86px; text-align:right; padding-right:22px;}
  .subproc-row{display:flex; flex-wrap:wrap; gap:6px; margin-top:6px; width:100%;}
  .subproc-chip{display:inline-flex; align-items:center; gap:5px; font-size:11.5px; padding:3px 9px; border-radius:12px;
    background:#ececea; color:var(--ink-soft); cursor:pointer; border:1px solid #ddd9cd; font-weight:600;}
  .subproc-chip input{width:auto; margin:0; cursor:pointer;}
  .subproc-chip.done{background:var(--ok-soft); color:var(--ok); border-color:transparent;}
  .subproc-chip.vendor{cursor:default; flex-wrap:wrap; border-radius:8px;}
  .subproc-chip.vendor input[type=number]{cursor:text;}

  textarea.copybox{font-family:ui-monospace,monospace; font-size:12.5px; min-height:140px; white-space:pre;}
  .run-date{font-size:12px; color:var(--ink-soft); min-width:110px; text-align:right;}

  /* ------------------------------------------------------------------
     Additions for the Django version. Everything above this line is the
     Shopfloor.html stylesheet verbatim, so the two apps look identical.
     ------------------------------------------------------------------ */

  /* The sidebar tabs are links here rather than buttons, so the same rules
     are repeated for <a>. */
  /* font-weight:600 because the original's tabs are <button> elements and so
     inherit the button rule above; these are links and would default to 400. */
  nav#side a.tabbtn{display:block; width:100%; text-align:left; background:none;
    border:none; color:#b7c0cd; padding:10px 16px; font-size:13.5px;
    font-weight:600; cursor:pointer; border-left:3px solid transparent;
    font-family:inherit; text-decoration:none;}
  nav#side a.tabbtn:hover{background:#222d3d; color:#fff;}
  nav#side a.tabbtn.active{background:#222d3d; color:#fff;
    border-left-color:var(--accent); font-weight:600;}
  nav#side a.tabbtn.notyet{color:#5d6673; cursor:default;}
  nav#side a.tabbtn.notyet:hover{background:none; color:#5d6673;}

  /* Rows expand and collapse in the browser instead of re-rendering, so the
     children are in the page from the start and simply hidden. */
  .hidden{display:none;}

  /* Row actions post forms where the original called JS handlers. A form is a
     block element by default, which would leave its buttons spaced by
     whitespace instead of the row's own 6px gap — a couple of pixels out on
     every indented row. Making it a matching flex container puts the geometry
     back exactly where the original has it. */
  form.inline{display:inline-flex; gap:6px; align-items:center; margin:0;}
  .tree-actions form{display:flex; gap:6px; margin:0;}
  /* The reorder arrows are direct children of .tree-actions in the original,
     which has no align-items, so they stretch to the row's content height
     (31px, matching the qty input). Wrapping them in a form would shrink them
     to 24px without this. */
  .tree-actions > form{align-items:stretch;}
  /* Inside .quick-complete the original centres its children, so the form
     standing in for it must too. */
  .quick-complete form{align-items:center;}

  /* Attachment icons are links here (the original downloaded via JS), so the
     link underline has to be turned off to match. */
  a.clip{color:var(--ink-soft); text-decoration:none;}

  /* Django's messages framework, styled like the original's toasts. */
  .messages{margin-bottom:14px;}
  .msg{padding:9px 12px; border-radius:var(--radius); margin-bottom:6px;
    font-size:13px; border:1px solid;}
  .msg.success{color:var(--ok); background:var(--ok-soft); border-color:#bcdcc7;}
  .msg.error{color:var(--bad); background:var(--bad-soft); border-color:#edc4c7;}
  .msg.warning{color:var(--gold); background:var(--gold-soft); border-color:#e8d6a8;}
  .msg.info{color:var(--steel); background:var(--steel-soft); border-color:#c4d3de;}

  /* Banner marking a screen that is read-only until its actions are ported. */
  .notyet-note{border:1px solid #e8d6a8; background:var(--gold-soft);
    color:#6a5308; padding:9px 12px; border-radius:var(--radius);
    font-size:12.5px; margin-bottom:18px;}
  .notyet-note a{color:#6a5308;}

  /* An error message stays on screen until dismissed, so it gets its own
     colour rather than the plain dark toast used for confirmations. */
  .toast.error{background:var(--bad);}

  /* ------------------------------------------------------------------ *
   * The module bar. New — the HTML app was one module, so it had no
   * equivalent, and this is the only block in this file that is not a copy
   * of its stylesheet.
   *
   * Steel rather than the sidebar's near-black: it is already this app's
   * second structural colour (badges, drawing previews, the brand tag), so
   * the bar reads as part of the same scheme while being clearly a level
   * above the dark sidebar under it rather than more of the same mass. The
   * active module is marked the way an active sidebar tab is — white, seated
   * on a darker shade, with the accent colour on the edge nearest the
   * content.
   * ------------------------------------------------------------------ */
  #modules{height:var(--module-bar); background:var(--steel);
    border-bottom:1px solid #2f4a63; display:flex; align-items:stretch;}
  #modules .modtabs{display:flex; align-items:stretch;}
  #modules .modtab{display:flex; align-items:center; padding:0 18px;
    color:#cfdde9; font-size:12.5px; font-weight:600; letter-spacing:.06em;
    text-transform:uppercase; text-decoration:none;
    border-bottom:2px solid transparent;}
  #modules a.modtab:hover{color:#fff; background:#33506b;}
  #modules .modtab.active{color:#fff; background:#33506b;
    border-bottom-color:var(--accent);}
  /* A module with no app installed and no stand-in page to visit. */
  #modules .modtab.notyet{color:#8ba2b8; cursor:default;}

  /* A sidebar with no workspaces in it yet, in the same muted voice as the
     tabs it stands in for. */
  nav#side .sidenote{padding:12px 16px; color:#8a95a5; font-size:12.5px;
    line-height:1.5;}

  /* Who is signed in, at the bottom of the sidebar above the footer note. */
  nav#side .whoami{margin:0; padding:10px 16px; border-top:1px solid #2c3849;
    display:flex; align-items:center; justify-content:space-between; gap:8px;}
  nav#side .whoami div{font-size:12.5px; color:#8a95a5; min-width:0;
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
  nav#side .whoami button{background:none; border:1px solid #33415a;
    color:#b7c0cd; padding:4px 9px; font-size:12px; flex:none;}
  nav#side .whoami button:hover{background:#222d3d; color:#fff;}

  /* ------------------------------------------------------------------
     The bending dialog: a copy of the TP10s press-brake controller.

     Kept at the end of the file and prefixed `bend-`, because the rest of
     this stylesheet is a faithful copy of the original app's and nothing
     here belongs to it. Every colour is sampled from the photograph of the
     controller, which is why they are literals rather than the variables
     used everywhere else: they are that machine's colours, not this app's,
     and if the app is restyled these must not follow it.
     ------------------------------------------------------------------ */
  /* Narrow, now that the empty middle column is gone: the label, the reading
     and its correction are all the width there is anything to put. Not as
     narrow as it briefly was — at 520 px the top line wrapped on a 13-inch
     screen, and the extra line was enough to push the Save button below the
     fold. */
  .bendmodal{max-width:580px; position:relative;}
  /* Out of the flow, not floated: with no die the band above it is not
     rendered at all, and a floated × then landed on the STEP line, on top of
     the thickness reading. */
  .bendmodal > .x{position:absolute; top:10px; right:12px; float:none;}
  /* Print, at the foot, to the left of + Add bend — where Bruno asked for it:
     "can you please move the Print icon to the bottom? Perhaps to the left of
     the +Add bend button?" It began in the corner beside the ×, which put a
     control that produces something in among the two that dismiss the dialog.

     It carries `sm ghost` like the buttons beside it, so it is one of that row
     rather than an icon dropped into it, and only the glyph is inside — the row
     is already three controls wide. */
  .bendmodal .bend-nav .bend-print{display:inline-flex; align-items:center;
      justify-content:center; width:30px; padding:0; color:var(--ink-soft);}
  .bendmodal .bend-nav .bend-print:hover{color:var(--accent); opacity:1;}
  .bendmodal .bend-nav .bend-print svg{width:15px; height:15px; display:block;
      fill:none; stroke:currentColor; stroke-width:1.7;
      stroke-linecap:round; stroke-linejoin:round;}
  /* While the PDF is being made. The server runs a headless browser over the
     sheet, which takes about a second, and a download tells the page nothing
     when it starts — so the icon says so itself and `bendmodal.js` stops it
     when the file lands. A pulse rather than a spinner: it is 15px of glyph,
     and a spinner at that size is a smudge. */
  .bendmodal .bend-nav .bend-print.busy{color:var(--accent);
      pointer-events:none; animation:bendprint-wait 1.05s ease-in-out infinite;}
  @keyframes bendprint-wait{0%{opacity:1;} 50%{opacity:.3;} 100%{opacity:1;}}
  @media (prefers-reduced-motion:reduce){
    /* Still says "working", without the movement. */
    .bendmodal .bend-nav .bend-print.busy{animation:none; opacity:.45;}
  }
  /* Three pictures across the top — part, punch, die — each with its one spec
     printed above it, centred on its own drawing. It was one tall panel with a
     headline, a drawing and a name stacked in it, which stood 190 px high, more
     than the whole controller screen it sits above. */
  .bendmodal .bend-band{background:#fefefe; border:1px solid #dfe6ec;
      border-radius:4px; padding:6px 12px 8px; margin-bottom:8px;
      display:flex; align-items:flex-end; gap:14px;}
  /* An author `display` beats the `hidden` attribute, which is only UA-level
     display:none — so the band went on showing as an empty framed strip after
     it was told to hide. Anything given a display here needs this line. */
  .bendmodal .bend-band[hidden]{display:none;}
  .bendmodal .bend-tile{flex:1; min-width:0; display:flex;
      flex-direction:column; align-items:center; gap:2px;}
  .bendmodal .bend-tilehead{font-size:22px; line-height:1.15; font-weight:700;
      color:#33348e; letter-spacing:.5px; min-height:25px; text-align:center;
      overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
      max-width:100%;}
  /* Only the part's tile has one. "Let's also have the full part name displayed
     here" — under the id, in the palette's secondary figure colour and at a
     third of the id's size, so the loud thing on the tile is still the number
     you match against the job sheet. The band's tiles bottom-align, so the line
     grows the band upwards and the three drawings stay level with each other.

     Nowrap and an ellipsis, with the whole name on `title`: bent parts are
     named "Cross brace" and "Swivel plate" and fit easily, but the field takes
     300 characters and a wrapping name would shove this tile's drawing out of
     line with the other two.

     `min-height` is what makes the empty ones in the punch and die tiles hold
     their line, and holding it is the point: without it those two tiles are a
     line shorter and their headlines sit below the part's. */
  .bendmodal .bend-tilename{font-size:12px; line-height:1.2; color:#5a5b93;
      text-align:center; min-height:15px; max-width:100%; overflow:hidden;
      text-overflow:ellipsis; white-space:nowrap;}
  .bendmodal .bend-tiledraw{height:66px; width:100%; display:flex;
      align-items:center; justify-content:center;}
  .bendmodal .bend-tiledraw svg{height:66px; width:auto; max-width:100%;}
  /* A DXF preview is drawn in hairlines, which all but vanish at this size.
     `non-scaling-stroke` makes the width mean CSS pixels rather than user
     units, so one number works whatever scale the drawing was fitted to. */
  .bendmodal .bend-tiledraw svg path{stroke-width:2;
      vector-effect:non-scaling-stroke;}

  /* Sprite glyphs (core/_bend_icons.html). One class per size, because these
     appear at four sizes and a style attribute on each would be four more
     places to keep in step. */
  /* text-transform:none is load-bearing, and finding out why cost several
     rounds. The ANGLE glyph's alpha is a font glyph inside an <svg><text>, and
     CSS text-transform inherits straight into SVG text — so sitting inside
     `.bend-rowlabel`, which is uppercased, the α was being drawn as Α, Greek
     capital alpha, which is a Latin "A" to look at. The glyph was correct; the
     label above it was rewriting the character. letter-spacing leaks the same
     way and nudges a centred glyph off centre. */
  .bi{display:inline-block; vertical-align:middle; flex:none;
      text-transform:none; letter-spacing:normal;}
  .bi-18{width:18px; height:18px;}
  .bi-20{width:20px; height:20px;}
  .bi-22{width:22px; height:22px;}
  .bi-26{width:26px; height:26px;}
  /* The two dimension marks — thickness and bend width — are the same plate
     drawn long, and only their arrows differ. */
  .bi-wide{width:70px; height:23px;}
  .bi-30{width:30px; height:30px;}
  .bi-32{width:32px; height:32px;}
  .bi-44{width:44px; height:44px;}

  .bendmodal .bend-top{display:flex; align-items:center; gap:12px;
      padding:0 26px 10px 4px; flex-wrap:nowrap;}
  /* Pushes bend width and thickness to the right-hand end, where Bruno wants
     them and where the controller reads them. */
  .bendmodal .bend-topgap{flex:1; min-width:8px;}
  .bendmodal .bend-topvalue{font-size:15px; font-weight:600; color:#33348e;}
  /* Bend width is typed into, so it is an input that looks like the value
     beside it until you put the caret in it. */
  .bendmodal .bend-topinput{cursor:text;}
  .bendmodal .bend-topinput input{font-size:15px; font-weight:600;
      color:#33348e; width:5.4ch; text-align:right; border:1px solid transparent;
      border-bottom:1px dashed #c9d4dd; border-radius:0; padding:0 2px;
      height:auto; background:transparent;}
  .bendmodal .bend-topinput:hover input{border-bottom-color:#469dd4;}
  .bendmodal .bend-topinput input:focus{outline:none; background:#f6fbfe;
      border-bottom-style:solid; border-bottom-color:#469dd4;}
  .bendmodal .bend-topinput input[readonly]{border-bottom-color:transparent;}
  .bendmodal .bend-topinput input::-webkit-outer-spin-button,
  .bendmodal .bend-topinput input::-webkit-inner-spin-button{
      -webkit-appearance:none; margin:0;}
  .bendmodal .bend-topinput input{-moz-appearance:textfield;
      appearance:textfield;}
  /* Sampled off the controller: rgb(109,145,169). It was a flat grey, which
     was the one bit of the top line that did not match. */
  /* text-indent pays back the letter-space that `letter-spacing` adds after
     the final P: without it the word sits a pixel left of centre in its box,
     which is exactly the sort of thing you see without being able to name. */
  .bendmodal .bend-chip{background:#6d91a9; color:#fff; font-size:13px;
      font-weight:700; letter-spacing:1px; text-indent:1px; border-radius:3px;
      display:inline-flex; align-items:center; justify-content:center;
      height:28px; padding:0 11px; flex:none;}
  .bendmodal .bend-steplabel{color:#33348e; font-size:28px; font-weight:500;
      font-variant-numeric:tabular-nums; line-height:1;}
  .bendmodal .bend-topitem{color:#33348e; font-size:15px; font-weight:500;
      display:flex; align-items:center; gap:5px;
      font-variant-numeric:tabular-nums;}
  .bendmodal .bend-topicon{color:#adadad; font-size:15px;}

  /* The three tools sit together in the middle of the screen, as a set —
     material, punch, die is the order they are chosen in — and COMPENSATE is
     taken out of the flow and parked over its own column of numbers, whose left
     edge is at 76.3% of the row (the value grid below is 0.7/1.1/1.1/0.9). */
  .bendmodal .bend-tools{display:flex; align-items:flex-end;
      padding:2px 4px 10px;}
  /* An empty pad the same width as the correction column, so the three tools
     are centred on the *screen* while COMPENSATE still sits over its own column
     of numbers. Reserved space rather than absolute positioning: a `<details>`
     shrink-wraps to something surprising in a flex row, and the die was landing
     on top of the word. */
  .bendmodal .bend-toolset{flex:1; display:flex; justify-content:center;
      align-items:flex-end; gap:18px;}
  .bendmodal .bend-compensate{flex:0 0 150px;}
  /* Equal slots, stated rather than inferred: material, punch and die read as
     a set of three, and a fixed width is also what keeps a wide die drawing
     from pushing its neighbours around. */
  .bendmodal .bend-tool{flex:0 0 92px; text-align:center;}
  /* The tooling is the thing being chosen, so it is drawn big enough to
     recognise across a workshop bench. The uploaded drawing goes in the same
     box, so it grows with it. */
  .bendmodal .bend-glyph{color:#2f308a; height:58px; display:flex;
      align-items:center; justify-content:center;}
  /* height, not max-height: a die's drawing is wide and shallow, so capping
     the height alone left it at a third of the box while the punch beside it
     filled it. Both fill it now, and max-width keeps the wide one inside its
     column. */
  .bendmodal .bend-glyph svg{height:56px; width:auto; max-width:100%;}
  .bendmodal .bend-toollabel{font-size:11px; color:#adadad; font-weight:600;
      text-transform:uppercase; letter-spacing:.5px;}
  .bendmodal .bend-compensate{font-size:11px; color:#adadad; text-align:center;
      text-transform:uppercase; letter-spacing:.5px; font-weight:600;
      display:flex; flex-direction:column; align-items:center; gap:2px;}
  .bendmodal .bend-compglyph{color:#2f308a; width:44px; height:16px;}

  /* A tool picker: the glyph is the control. <details> so it opens with no
     script at all, and the list floats rather than pushing the row apart. */
  .bendmodal .bend-picker{position:relative;}
  .bendmodal .bend-picker > summary{list-style:none; cursor:pointer;
      border-radius:4px; padding:2px 0;}
  .bendmodal .bend-picker > summary::-webkit-details-marker{display:none;}
  .bendmodal .bend-picker > summary:hover{background:#f2f7fb;}
  .bendmodal .bend-picker[open] > summary{background:#eaf3fa;}
  .bendmodal .bend-picker[data-bend-locked] > summary{cursor:default;}
  .bendmodal .bend-picker[data-bend-locked] > summary:hover{background:none;}
  .bendmodal .bend-picklist{position:absolute; z-index:5; top:100%; left:50%;
      transform:translateX(-50%); min-width:230px; max-height:260px;
      overflow:auto; background:#fff; border:1px solid var(--line);
      border-radius:4px; box-shadow:0 6px 18px rgba(26,35,50,.16);
      padding:4px; text-align:left;}
  .bendmodal .bend-pick{display:flex; align-items:center; gap:8px;
      padding:5px 7px; border-radius:3px; cursor:pointer; font-size:13px;}
  .bendmodal .bend-pick:hover{background:#f2f7fb;}
  .bendmodal .bend-pick input{width:auto; margin:0; flex:none;}
  .bendmodal .bend-pickthumb{width:42px; height:26px; flex:none;
      display:flex; align-items:center; justify-content:center;}
  .bendmodal .bend-pickthumb svg{max-width:100%; max-height:100%;}
  .bendmodal .bend-pickname{color:var(--ink);}

  .bendmodal .bend-rows{padding:6px 4px 2px;}
  /* Four columns: the label, the big value, an empty gutter where the
     controller shows the live machine position, and the correction. The
     gutter is what keeps the correction under its own heading. */
  .bendmodal .bend-row{display:grid;
      grid-template-columns:100px 1fr 150px; align-items:center;
      gap:8px; margin-bottom:10px;}
  /* Glyph above the word, which is how the controller sets these two out.
     Side by side it read as a button with a caption. */
  .bendmodal .bend-rowlabel{font-size:11px; color:#adadad; font-weight:600;
      text-transform:uppercase; letter-spacing:.5px; display:flex;
      flex-direction:column; align-items:center; gap:1px;}
  /* Big — as big as they are on the machine, where these two are graphics
     rather than icons: the alpha-and-V is a seventh of the screen's width. At
     32 px square they were a quarter of that. */
  .bendmodal .bend-rowglyph{color:#2f308a; width:76px; height:44px;}
  .bendmodal .bend-matglyph{width:56px;}
  /* The dimension row's label is a button: the icon IS the switch between in
     size and flange length. It has to look like the two labels either side of
     it, so everything a button brings is taken back off, and only the cursor
     and a little lift on hover say it can be pressed. */
  /* `color` is deliberately NOT reset here: `.bend-rowlabel` sets the grey the
     other two row labels use, and `color:inherit` on the button overrode it —
     the label came out black beside ANGLE's grey. */
  .bendmodal button.bend-dimtoggle{background:none; border:0; padding:0;
      margin:0; font:inherit; cursor:pointer;}
  .bendmodal button.bend-dimtoggle:hover .bend-rowglyph{color:#469dd4;}
  /* `opacity:1` because `button:disabled{opacity:.4}` is app-wide, and this
     button is a **label**: IN SIZE / FLANGE says which of the two measurements
     the bend is programmed in, which is information, not an affordance. Faded
     to 40% it read as switched off — Bruno, of the printed sheet: "why is the
     In-size icon (or the L-flange) greyed out in the pdf though?" It was greyed
     in every read-only dialog too, on screen, since the day the toggle was
     built; the print is only where it got noticed, because ANGLE is a `<div>`
     right above it at full strength and the pair are read together.
     `.bend-sidebtn:disabled` already resets it for the same reason. */
  .bendmodal button.bend-dimtoggle:disabled{cursor:default; opacity:1;}
  .bendmodal button.bend-dimtoggle:disabled:hover .bend-rowglyph{color:#2f308a;}
  .bendmodal button.bend-dimtoggle > span{display:flex;
      flex-direction:column; align-items:center; gap:1px;}
  /* The missing-value dot. Absolutely positioned in every case, because the
     value grid, the tools row and the band were measured against Bruno's
     controller screenshot column by column — a dot that took part in the
     layout would move the numbers it is reporting on.

     `hidden` is honoured explicitly: an author `display` beats the attribute,
     and .bend-dot sets one. That trap cost a round on the band already. */
  /* The thickness box, for a part with no sheet-thickness feature to read one
     from. Sized and coloured like the read-only value beside it so the row does
     not change shape depending on which kind of part is open. */
  .bendmodal .bend-topask{width:5ch; padding:1px 2px; font-size:15px;
      font-weight:500; color:#33348e; border:1px solid #d8d8e4;
      border-radius:3px; background:#fbfbfd; text-align:right;
      font-variant-numeric:tabular-nums;}
  .bendmodal .bend-topask:focus{outline:2px solid var(--steel);}
  /* The spinner is hidden here for the same reason it is hidden on every other
     number box in this dialog — and it was the one that got missed. With the
     arrows drawn, the box measured 35px of room for 40px of content and cut
     the last digit off a right-aligned value: Bruno sent a screenshot of 1.5
     with the 5 half gone, 9 October 2026. The arrows are no use on a screen
     that copies a machine controller anyway; you type the thickness. */
  .bendmodal .bend-topask::-webkit-outer-spin-button,
  .bendmodal .bend-topask::-webkit-inner-spin-button{
      -webkit-appearance:none; margin:0;}
  .bendmodal .bend-topask{-moz-appearance:textfield; appearance:textfield;}

  /* Column headings above a group of rows. Added because Bruno had to ask what
     two of the columns in EXTRA PRICED STEPS were — a bare number box with no
     label is a guess, and the answer ("price per unit" and "order in the
     list") was only ever in the code. */
  .comp-head{margin-bottom:2px;}
  .comp-head > div{font-size:10.5px; text-transform:uppercase;
      letter-spacing:.04em; color:var(--ink-soft); font-weight:700;
      padding:0 2px;}

  /* A production run's in-house step, as an action rather than a tick box.
     Green once done, and still pressable to un-mark. */
  /* Steel and filled, so a step you finish reads as its own kind of control:
     not the orange of Send and Apply, which move stock, and not the white of
     Bending settings, which opens something. Green once it is done, which is
     the app's colour for finished everywhere else. */
  .stepbtn{border:1px solid var(--steel); background:var(--steel); color:#fff;
      font-weight:600; font-family:inherit; font-size:11.5px;
      padding:3px 9px; border-radius:20px; white-space:nowrap;}
  .stepbtn:hover{background:#2f4a63; border-color:#2f4a63;}
  .stepbtn.done:hover{background:#cfe6da;}
  .stepbtn.done{background:var(--ok-soft); color:var(--ok);
      border-color:#bcd9c9;}
  /* The Done button's rules were here for one day. Bruno, 2 September 2026:
     "nothing is entered, only tracked … Remove all Done buttons." Nothing in a
     run posts any more, so `.sm.done`, `.task-row.dismissed` and `.groupdone`
     have no markup left to style. */
  /* **Every tag is a reminder now, in-house and outsourced alike.** No button,
     nothing to press, so nothing that looks pressable. Bruno, 1 September:
     "Let's still have the tags, but only as a reminder for the sub-processes";
     2 September, for the vendor one: "make the Galvanising tab without the
     ability to send and return parts, the icon with number of parts at the
     vendor stays as well as the writing".

     The bending mark inside an in-house tag is the one thing left that opens
     anything — the press-brake numbers — and keeps its own pointer. */
  .subproc-chip.reminder{cursor:default; padding:3px 10px 3px 6px;}
  .subproc-chip.reminder a.subproc-mark{cursor:pointer;}
  /* The vendor tag no longer wraps, because there is nothing left to wrap: the
     qty box and its two buttons are gone and what remains is a mark, a count
     and a sentence. */
  .subproc-chip.vendor.reminder{flex-wrap:nowrap;}
  a.sm.ghost{display:inline-block; cursor:pointer; border:1px solid #c9c6bc;
    background:#fff; color:var(--ink); padding:4px 9px;
    border-radius:var(--radius); font-size:12px; font-weight:500;
    text-decoration:none; font-family:inherit;}
  a.sm.ghost:hover{opacity:.88;}
  /* The bending link beside those steps: the Inventory row's glyph and a
     label, so one mark means one thing across the app. */
  /* The in-house step chip — the same shape as the outsourced one beside it.
     Bruno: "let's have the buttons Bending Settings and Bending Done combined
     into a field like icon Zn Galvanising -> Vendor and button Galvanising is."
     So mark, name, button, in one rounded field, and the separate "Bending
     settings" button is gone. */
  .subproc-chip.inhouse{gap:8px; padding:4px 9px 4px 6px; border-radius:8px;
      cursor:default;}
  .subproc-mark{display:inline-flex; align-items:center; color:var(--steel);
      line-height:0;}
  /* A link when there is something behind it, a mark when there is not — done,
     or waiting on an earlier step. Same glyph either way, so the row does not
     change shape as steps are ticked. */
  a.subproc-mark{cursor:pointer;}
  a.subproc-mark:hover{color:var(--accent);}
  span.subproc-mark{cursor:default;}

  /* A price box with its € inside it.
     Bruno: "let's have the euro sign inside the field, not on the side. Let's
     have that everywhere in the module where a price is required." Drawn on the
     box's own left padding rather than as a sibling, so it cannot drift away
     from the field it belongs to, and pointer-events:none so clicking the sign
     puts the caret in the box. */
  .moneybox{position:relative; display:inline-flex; align-items:center;}
  .moneybox::before{content:"\20AC"; position:absolute; left:9px;
      color:var(--ink-soft); font-weight:600; font-size:12.5px;
      pointer-events:none;}
  .moneybox input{padding-left:22px;}
  /* Greyed while "No quantity" is ticked — a step you tick is not a step you
     price. readonly rather than disabled, because a disabled field posts
     nothing at all and the server would read that as "no answer". */
  .moneybox input[readonly]{background:#f1efe9; color:var(--ink-soft);
      cursor:not-allowed;}
  .moneybox:has(input[readonly])::before{opacity:.45;}

  /* The one slot on a sub-process row that asks two different questions: the
     plate for a step that goes out to a vendor, the controller for one done
     here. Fixed width, so the columns line up down the section whichever of
     the two a row is showing — otherwise a plate row and a controller row
     disagree about where every box after them starts and the whole section
     reads as crooked, which is a complaint Bruno has made about three other
     screens already.

     The width is on this wrapper and never on the two spans inside it: an
     author `display` beats the `hidden` attribute, and the hidden one would go
     on holding its space. */
  .sub-slot{display:inline-flex; width:118px; flex:0 0 118px;}
  .sub-slot > span{width:100%;}

  /* "Thread tapping first", where the Send box would be. Steel like the
     at-vendor badge: this is the order of work, not a fault. */
  .subproc-chip.vendor .waitfor{color:var(--steel); font-weight:600;
      white-space:nowrap; font-style:italic;}
  .stepbtn[disabled]{opacity:.45; cursor:not-allowed;}
  .stepbtn[disabled]:hover{background:var(--steel); border-color:var(--steel);}
  .stepbtn.done[disabled]:hover{background:var(--ok-soft);}

  /* The tickbox behind a row's ×.
     A formset counts its rows and reads each row's id, so a saved row cannot
     just leave the page — × ticks this and hides the row, which looks like the
     original's splice and means the same thing on save. It is in the page
     because it has to be posted; it is never something to click. */
  [data-row-delete]{display:none;}

  /* The vendor step inside a run: Zn icon, what it is, what is out there now,
     one wide Send box, and the done button.

     The Send box was 56px with the word "send" inside it as a placeholder,
     which drew as "sen". It is 72px and says "qty" now, with the verb on the
     button — the same shape as the Apply box at the end of the same row, and
     wide enough for four figures. */
  .subproc-chip.vendor{gap:8px; padding:4px 9px 4px 6px;}
  .subproc-chip.vendor .subproc-zn{display:inline-flex; align-items:center;
      color:var(--steel); line-height:0;}
  /* Not a link in a run: the row's own Send box and done button are the
     control, so the mark is a mark, and what is out rides in its corner as the
     same badge the Inventory row uses (.tree-subicon / .tree-subcount). */
  .subproc-chip.vendor .subproc-zn{cursor:default;}
  .subproc-chip.vendor .sendform{display:inline-flex; gap:5px;
      align-items:center; margin:0;}
  .subproc-chip.vendor .sendform input[type=number]{width:72px;}

  /* A step that cannot be done yet reads as ONE thing.
     Bruno, on a row where thread tapping was still open: "the Bending icon is
     greyed out, text Bending seems to be not greyed and then the button is …
     For Galvanising the icon Zn isn't greyed out which it should, text Vijčane
     pumpe isn't either and only the button is."

     He is right, and the cause was three separate decisions: the button faded
     itself with [disabled], the in-house mark had an opacity of its own, and
     the Zn mark was painted var(--steel) unconditionally so nothing could tone
     it down. So the state now lives on the chip and everything inside takes its
     colour from there — one rule, both kinds of chip, mark and words and button
     together.

     Colour rather than opacity on the chip itself, so the disabled button can
     keep its own fade without the two multiplying into something unreadable. */
  /* The role grid's one control per screen: No access / Can open / Can change.
     Wide enough that the longest of the three does not truncate. */
  select.accesslevel{width:auto; min-width:132px; font-size:12px;
      padding:4px 6px;}

  .subproc-chip.blocked{background:#f2f2ef; border-color:#e4e1d8;
      color:#a7a49a;}
  .subproc-chip.blocked .subproc-zn,
  .subproc-chip.blocked .subproc-mark{color:inherit;}
  /* And the same idea for a step that is finished: the mark joins the green
     rather than staying steel-blue in a green field. */
  .subproc-chip.done .subproc-zn,
  .subproc-chip.done .subproc-mark{color:inherit; opacity:.75;}

  /* Walking a batch of one sheet thickness, at the foot of the bend dialog and
     only when it was opened from a production run. */
  .bendmodal .bend-walk{display:flex; align-items:center; gap:12px;
      justify-content:space-between; margin:10px 0 2px; padding-top:10px;
      border-top:1px solid var(--line);}
  .bendmodal .bend-walkat{font-size:11.5px; color:var(--ink-soft);
      letter-spacing:.02em;}

  /* The vendor send/receive dialog. One row per outsourced step: what it is on
     the left, the two quantity boxes on the right. Its own rules rather than
     borrowing .tree-row, which is measured for the inventory list's columns. */
  .holdrow{display:flex; align-items:center; justify-content:space-between;
      gap:14px; flex-wrap:wrap; padding:11px 2px;
      border-bottom:1px solid var(--line);}
  .holdrow:last-of-type{border-bottom:none;}
  .holdrow-what{display:flex; flex-direction:column; align-items:flex-start;
      gap:3px;}
  .holdrow-do{display:flex; gap:10px; align-items:center;}
  .holdform{display:flex; gap:6px; align-items:center;}
  .holdform input[type=number]{width:70px; text-align:right;}

  /* A zero-sized marker in the flow, with the circle drawn by ::before
     outside it. In flow so it lands beside the value it belongs to however
     wide that value is; zero-sized so it cannot move anything. The 4px of flex
     gap it would otherwise add is cancelled by the negative margin, which
     keeps a dotted cell and an undotted one exactly the same width. */
  .bend-dot{position:relative; display:block; width:0; height:0;
      align-self:center; margin:0;}
  .bend-dot::before{content:''; position:absolute; width:7px; height:7px;
      border-radius:50%; background:#d8232a;}
  .bend-dot[hidden]{display:none;}
  /* Inside the field's own top-right corner, so there is no doubt which value
     it belongs to. "Just past the unit" was tried first and read as though it
     belonged to the compensation column on the right: the input is 182px wide,
     so the digits are a long way from its right-hand edge.

     The offset is exact rather than eyeballed. The marker sits immediately
     after the unit, the unit is a fixed 26px (.bend-unit above) and the cell's
     flex gap is 4px, so the input's right edge is exactly 30px to the left. */
  .bendmodal .bend-cell .bend-dot{margin-left:-4px;}
  .bendmodal .bend-cell .bend-dot::before{left:-43px; top:-21px;}
  .bendmodal .bend-topitem .bend-dot{margin-left:-5px;}
  .bendmodal .bend-topitem .bend-dot::before{left:3px; top:-11px;}
  .bendmodal .bend-picker > summary .bend-dot{position:absolute;
      right:5px; top:5px;}
  .bendmodal .bend-picker > summary .bend-dot::before{left:0; top:0;}

  .bendmodal .bend-cell{display:flex; align-items:baseline; gap:4px;
      justify-content:center; position:relative;}
  /* The dimension row holds two cells for one column — the in size and the
     flange length — and shows one at a time.
     
     Both rules here are load-bearing, and the first is the trap this file
     already warns about twice: an author `display` beats the `hidden`
     attribute, which is only UA-level `display:none`. `.bend-cell{display:flex}`
     made `hidden` a no-op, so the row drew BOTH numbers side by side and
     pushed the compensation onto a line of its own. Invisible in the markup,
     obvious in a screenshot.

     The second is because the row places its cells with :first-of-type and
     :last-of-type, and those count DOM position rather than what is showing —
     so the cell that shares column 2 has to say so itself. */
  .bendmodal [data-bend-dim-when][hidden]{display:none;}
  .bendmodal .bend-cell[data-bend-dim-when]{grid-column:2;}
  .bendmodal .bend-topitem{position:relative;}
  .bendmodal .bend-cell:first-of-type{grid-column:2;}
  .bendmodal .bend-cell:last-of-type{grid-column:3;}
  /* One note per bend, on exactly the grid the bend rows use — 100px / 1fr /
     150px — so its label lines up under ANGLE and IN SIZE, the field starts
     where the numbers do, and SIDE sits in the same column as the two
     compensation fields above it. Bruno: "let's have Side A/B vertically
     aligned with compensate parameter fields." It was `auto` before, so the
     button's width decided where it sat and it lined up with nothing.

     `align-items:start` because the note is a textarea now and can be several
     lines tall; SIDE belongs at the top of the cell, beside the note's first
     line, not floating in the middle of it. */
  .bendmodal .bend-noterow{display:grid;
      grid-template-columns:100px 1fr 150px; align-items:start; gap:8px;
      margin-top:2px;}
  .bendmodal .bend-notelabel{font-size:11px; color:#adadad; font-weight:600;
      text-transform:uppercase; letter-spacing:.5px; text-align:center;}
  /* The note is a textarea now, so these carry over from the old input: same
     face, same underline, same focus. `resize:vertical` and a min-height rather
     than a fixed one — bendmodal.js grows it to fit what is typed, and a person
     can still drag it taller. */
  .bendmodal textarea.bend-note{font-size:13px; color:var(--ink);
      font-family:inherit; line-height:1.45; width:100%; display:block;
      min-height:56px; resize:vertical; overflow:hidden;
      background:#f7f9fd; border:1px solid transparent; border-radius:3px;
      border-bottom:2px solid #d5dfec; padding:5px 8px;}
  .bendmodal textarea.bend-note:hover{border-bottom-color:#93a9c4;}
  .bendmodal textarea.bend-note:focus{background:#fff; outline:none;
      border-bottom-color:#469dd4;}
  .bendmodal textarea.bend-note[readonly]{background:transparent;
      border-bottom-color:transparent; resize:none;}
  .bendmodal textarea.bend-note::placeholder{color:#c3cddb;}

  /* Which side is bent. The value is in the <select>; the button in front of it
     is the toggle. `html.js` is set inline in base.html before the first paint,
     so exactly one of the two is ever visible — the button where the script is
     running, the select where it is not. A hidden select still posts, which is
     the whole reason this works. */
  .bendmodal .bend-side{flex:none; display:inline-flex;
      align-items:center; justify-content:center; justify-self:center;
      margin-top:2px;}
  .bendmodal .bend-sideselect{width:auto; font-size:12px; padding:4px 6px;}
  .bendmodal .bend-sidebtn{display:none;}
  html.js .bendmodal .bend-sideselect{display:none;}
  html.js .bendmodal .bend-sidebtn{display:inline-flex; align-items:center;
      justify-content:center; min-width:78px; padding:6px 10px; font-size:11px;
      font-weight:700; letter-spacing:.6px; border-radius:20px;
      border:1px solid #6d91a9; background:#eaf1f6; color:#3c6480;}
  /* Side B is the underside off the laser — a little dross, some smoke. It gets
     the warmer colour, so a programme that flips the part shows it at a glance
     rather than on being read. */
  html.js .bendmodal .bend-sidebtn[data-side="B"]{border-color:#c2882b;
      background:#fbf2e2; color:#8a5c12;}
  html.js .bendmodal .bend-sidebtn:disabled{cursor:default; opacity:1;}
  /* Back beside the numbers, at Bruno's ask. The controller leaves them off,
     but the controller is not the thing anyone types into.

     A fixed width, because the cell is centred: "mm" is wider than "°", and
     without this the ANGLE row's box and the IN SIZE row's box below it sat at
     different places on the line. */
  .bendmodal .bend-unit{font-size:13px; color:#adadad; flex:none; width:26px;
      text-align:left;}
  /* Sized to their content, not to the column, so the unit sits against the
     number the way it does on the controller rather than drifting off to the
     right of an input that filled the space. Wide enough for "-180.0". */
  .bendmodal input.bend-value{font-size:42px; font-weight:400; color:#33348e;
      font-variant-numeric:tabular-nums; letter-spacing:.02em;
      border:1px solid transparent; border-radius:0; padding:0 7px;
      height:auto; width:7.8ch; min-width:0; flex:none; text-align:right;}
  .bendmodal input.bend-comp{font-size:28px; font-weight:400; color:#5a5b93;
      font-variant-numeric:tabular-nums; letter-spacing:.02em;
      border:1px solid transparent; border-radius:0; padding:0 7px;
      height:auto; width:6ch; min-width:0; flex:none; text-align:right;}
  /* These have to look like somewhere you type. Matching the controller
     exactly means numbers on bare white, and that went too far: with the
     programme empty there was nothing on the screen to aim at. So a very quiet
     field — a pale ground and a rule under it — which reads as a box up close
     and disappears into the layout from a step back. Read-only (a production
     run) keeps the bare-white version, because there is nothing to type
     there. */
  .bendmodal input.bend-value, .bendmodal input.bend-comp{
      background:#f7f9fd; border-radius:3px;
      border-bottom:2px solid #d5dfec;}
  .bendmodal input.bend-value:hover, .bendmodal input.bend-comp:hover{
      border-bottom-color:#93a9c4;}
  .bendmodal input.bend-value:focus, .bendmodal input.bend-comp:focus{
      background:#fff; border-bottom-color:#469dd4;}
  .bendmodal input.bend-value[readonly], .bendmodal input.bend-comp[readonly]{
      background:transparent; border-bottom-color:transparent;}
  .bendmodal input.bend-value::placeholder,
  .bendmodal input.bend-comp::placeholder{color:#c3cddb;}
  .bendmodal .bend-topinput input{background:#f4f7fb; border-radius:2px;
      padding:0 6px;}
  .bendmodal .bend-topinput input[readonly]{background:transparent;}
  /* No spinners. The controller has numerals, and a pair of tiny grey arrows
     beside each one is the clearest possible sign that this is a web form
     pretending — which is the one thing this screen must not look like. */
  .bendmodal input.bend-value::-webkit-outer-spin-button,
  .bendmodal input.bend-value::-webkit-inner-spin-button,
  .bendmodal input.bend-comp::-webkit-outer-spin-button,
  .bendmodal input.bend-comp::-webkit-inner-spin-button{
      -webkit-appearance:none; margin:0;}
  .bendmodal input.bend-value, .bendmodal input.bend-comp{
      -moz-appearance:textfield; appearance:textfield;}
  .bendmodal input.bend-value:focus, .bendmodal input.bend-comp:focus{
      outline:none; border-bottom-color:#469dd4; background:#f6fbfe;}
  .bendmodal input.bend-value[readonly], .bendmodal input.bend-comp[readonly]{
      border-bottom-color:transparent;}

  .bendmodal .bend-nav{display:flex; align-items:center; gap:8px;
      padding:8px 4px 2px; border-top:1px solid #eef2f5; margin-top:8px;}
  .bendmodal .bend-arrow{background:#eef2f5; border:1px solid #dfe6ec;
      color:#2f308a; border-radius:3px; padding:3px 10px; cursor:pointer;
      font-size:12px;}
  .bendmodal .bend-arrow:disabled{opacity:.4; cursor:default;}
  .bendmodal .bend-strip{display:flex; gap:4px;}
  /* padding:0 is the fix: `button` sets 7px 14px globally, and with
     box-sizing:border-box a 28 px box has no room left for the digit, which then
     draws off-centre against a content box squeezed to nothing. Flex centring
     rather than the default text centring, so it cannot happen again. */
  .bendmodal .bend-pip{width:28px; height:28px; border-radius:3px; padding:0;
      line-height:1; display:inline-flex; align-items:center;
      justify-content:center; border:1px solid #dfe6ec; background:#fff;
      color:#33348e; font-size:13px; font-weight:700; cursor:pointer;}
  /* Outlined rather than filled red. A solid red button is the heaviest thing
     on a screen whose whole job is to be four numbers, and it was sitting next
     to the one you press most. */
  .bendmodal .bend-remove{color:var(--bad); border-color:#e3c8c2;}
  .bendmodal .bend-remove:hover{background:var(--bad-soft);}
  .bendmodal .bend-pip.on{background:#469dd4; border-color:#469dd4; color:#fff;}

  .bendmodal .bend-foot{display:flex; gap:10px; flex-wrap:wrap;
      border-top:1px solid #eef2f5; margin-top:8px; padding-top:8px;}
  .bendmodal .bend-foot .field{margin-bottom:0;}
  .bendmodal .modal-actions{margin-top:12px;}

  /* The bending icon's slot in an inventory row. Fixed width and always
     present, so the quantity box, Apply and Edit line up down the column
     whether or not the part on that row is bent. */
  /* Pushed to the right of its own slot, so the gap sits between the icon and
     the process tag rather than between the icon and the count it belongs to.
     Widening the slot moves the tags left, not the icon right: everything from
     here to the end of the row is flex:none, so the row is measured from its
     right edge. */
  /* Room for two marks now that galvanising has one: bending, then Zn hard
     against the count. A part with one of the two shows it alone and the slot
     keeps its width, which is the whole point of the slot. */
  .tree-bend{width:58px; flex:none; display:flex; align-items:center;
      justify-content:flex-end; gap:1px;}
  .tree-bend a{color:var(--steel); display:flex; align-items:center;
      text-decoration:none; padding:2px; border-radius:3px;}
  .tree-bend a:hover{color:var(--accent); background:var(--accent-soft);}
  /* The count at the vendor, when there is one. Anchored to the icon rather
     than taking space in the row, so a part with stock out does not shift the
     column. */
  .tree-subicon{position:relative;}
  /* Steel, not red: red is the missing-value dot's colour throughout the bend
     dialog, and stock sitting at the vendor is information rather than a fault.
     Kept inside the icon's own corner so it cannot crowd the stock column. */
  .tree-subcount{position:absolute; top:-1px; right:-1px; min-width:10px;
      padding:0 2px; border-radius:6px; background:var(--steel);
      color:#fff; font-size:8px; line-height:10px; text-align:center;
      font-weight:700; pointer-events:none;}

  /* The bending-tools library. Rows of inputs rather than a table: a table
     cell cannot be a flex row of nine controls without fighting it, and the
     row has to stay one line — see the nowrap note below. It was also once
     true that each row was its own form; it is one form with one Save now. */
  /* Wider than an editor: nine columns of a tool's specs. `.modal.wide` is
     two classes, so this needs both to beat it. */
  .modal.wide.bendtools{max-width:900px;}
  /* The box, the heading and the hint are `.procsection`, shared with every
     other editor. What is left here is only what this group needs on top of
     it. */
  .bendtools-group{padding-bottom:10px;}

  /* The tools table is a `table.sheet` — see the sheet block at the foot of
     this file. Two things it needs that the Sales sheets do not:

     - **A min-width of its own.** The shared rule reserves 1066px for thirteen
       columns of boxes and vendor codes. This table has eight columns inside a
       900px dialog, so it fits as it is, and inheriting that floor would have
       made a dialog scroll sideways for no reason.
     - **A white ground.** A sheet on a page sits on `.panel`; here it sits in
       a `.procsection`, which is cream, and the field that appears under the
       pointer is `var(--panel)`. Without this the row you are on looks like a
       different colour of paper from the rest of the table. */
  table.sheet.sheet-tools{min-width:0;}
  .sheet-tools tbody tr:hover td{background:transparent;}
  .sheet-tools td{padding:3px 2px;}
  /* The drawing cell keeps its own height, which is the square's. */
  .sheet-tools td:first-child{padding:4px 6px 4px 0; width:74px;}
  /* A row removed by its × is hidden, not detached: the formset counts its
     rows and reads each one's id, so the row stays in the form with its DELETE
     ticked. Same rule as every other formset row in the app — and `!important`
     because a `<tr>`'s own `display:table-row` from the UA sheet is what the
     bare `[hidden]` rule loses to. */
  .bendtools-row[hidden]{display:none !important;}
  /* Why a row was refused, under the name it belongs to. It cannot be a row of
     its own: × hides `[data-formset-row]`, and a second row would be left
     behind under a tool that is no longer there. */
  .bendtools-why{margin:2px 0 1px;}
  /* The drawing is the file picker: a label with the input laid over it,
     invisible but still there for the keyboard (hence :focus-within). */
  .bendtools-thumb{position:relative; width:74px; height:34px; flex:none;
      display:flex; align-items:center; justify-content:center;
      border:1px solid var(--line); border-radius:3px; background:#fff;
      box-sizing:border-box; cursor:pointer; overflow:hidden;}
  .bendtools-thumb:hover{border-color:var(--accent);}
  .bendtools-thumb:focus-within{border-color:var(--accent);
      box-shadow:0 0 0 2px var(--accent-soft);}
  .bendtools-thumb input[type=file]{position:absolute; inset:0; width:100%;
      height:100%; opacity:0; cursor:pointer;}
  /* The drawing fills the square. A plain <span> around it has no size of its
     own, so `max-width:100%` on the svg resolved against nothing and the
     profile collapsed to a point — a square that looked empty on every tool
     that had a drawing. */
  .bendtools-drawn{display:flex; align-items:center; justify-content:center;
      width:100%; height:100%; overflow:hidden;}
  .bendtools-thumb svg{max-width:100%; max-height:100%;}
  .bendtools-nodraw, .bendtools-picked{font-size:10px;
      color:var(--ink-soft); max-width:68px; overflow:hidden;
      text-overflow:ellipsis; white-space:nowrap;}
  .bendtools-empty{border-style:dashed;}
  /* The square says one of two things: the drawing it has, or the name of the
     file it is holding and has not saved yet. dialog.js sets [data-holding]
     when a file is chosen, and this is what swaps them — so the same square
     carries a row through drawing → chosen name → new drawing without ever
     changing size. */
  .bendtools-picked{display:none;}
  .bendtools-thumb[data-holding]{border-style:solid;
      border-color:var(--accent);}
  .bendtools-thumb[data-holding] .bendtools-drawn{display:none;}
  .bendtools-thumb[data-holding] .bendtools-picked{display:block;
      color:var(--accent);}
  /* `.bendtools-said` used to be here: a message shown inside the dialog,
     because a save answered by re-rendering the fragment into it and the toast
     machinery — which fires when #main is swapped — never saw it. Saving
     closes the dialog and refreshes the page behind it now, like every other
     editor, so the ordinary toast is what says what happened. */

  /* "Were these actually built?" — asked over the inventory list. Three
     answers is one more than a dialog usually has, so they are allowed to wrap
     rather than run off the edge of a narrow window. */
  .build-ask-lines{margin-top:10px;}
  .build-ask-actions{flex-wrap:wrap;}

  /* ------------------------------------------------------------------
     The Sales module, merged 28 August 2026 from its own working copy.

     Its own rules, appended rather than woven in: two chats work on this file
     now, and a block with a boundary can be edited from one side without the
     other having to re-read the whole file. Everything above this line belongs
     to Shopfloor and core.

     What did NOT come across: the Sales copy's older versions of `.badge.muted`,
     `.tree-badges`, `.hidden`, `#modules` and `.modtab`. Those are files this
     side has changed since the branch point, and the ERP's version is the one
     that stands — the same rule as the data.
     ------------------------------------------------------------------ */
  /* A shipped order is done with. Dimmed rather than merely grey-texted,
     because the badges and boxes inside a row carry their own colours and
     would otherwise stay as loud as a row that still needs work. Hover brings
     it back, so it is quiet without being hard to use. */
  tr.done{opacity:.5;}
  tr.done:hover{opacity:1;}
  /* An unpaid order is not finished — it is waiting on money — so it stands
     back less than a shipped one and keeps a red edge down its first cell.
     "Quiet" and "forgotten" must not look the same. */
  tr.owing{opacity:.74;}
  tr.owing:hover{opacity:1;}
  tr.owing td:first-child{box-shadow:inset 3px 0 0 var(--bad);}
  /* Yellow: work that is ready to be done, as against red for work that
     cannot start yet. Used for "Build pallet(s)". */
  .badge.warm{background:#fdf1cf; color:#7d5e0b;}

  /* A box that reads as the tag it replaces. Status and payment are both set
     from the list, and both were carrying the same six lines of inline style;
     one class means they cannot drift apart. The colours are the badge
     colours, on purpose — the same fact should not change appearance because
     it became editable. */
  select.tag{width:auto; min-width:130px; padding:3px 8px; font-size:12px;
    font-weight:700; letter-spacing:.01em; border-radius:20px;
    border:1px solid transparent; background:#ececea; color:#6b6b66;}
  select.tag.ok{background:var(--ok-soft); color:var(--ok);}
  select.tag.bad{background:var(--bad-soft); color:var(--bad);}
  select.tag.warm{background:#fdf1cf; color:#7d5e0b;}
  select.tag.accent{background:var(--accent-soft); color:var(--accent);}
  select.tag.steel{background:var(--steel-soft); color:var(--steel);}
  select.tag.teal{background:var(--teal-soft); color:var(--teal);}
  select.tag.slate{background:var(--slate-soft); color:var(--slate);}
  select.tag.muted{background:#ececea; color:#6b6b66;}

  /* FIRA's own yellow, colour-picked from their logo. Three screens had the
     same four declarations written out by hand; a button that opens FIRA
     should look the same everywhere it appears. */
  button.fira{background:var(--fira); border-color:var(--fira); color:#3d2b15;
    font-weight:600;}
  .panel.fira, .card.fira{border-color:var(--fira); background:#fffaea;}
  .note.fira{border:1px solid var(--fira); background:#fffaea;
    border-radius:var(--radius); padding:11px 14px; margin-bottom:14px;
    font-size:12.5px; line-height:1.7;}

  /* The paragraph under a dialog's heading that says what the dialog is for.
     Fifteen dialogs had written it out by hand in four different sizes, so the
     same element was visibly different from one dialog to the next. */
  .lede{font-size:12.5px; color:var(--ink-soft); line-height:1.7;
    margin-bottom:14px;}

  /* A small capitalised caption over a block inside a dialog. Four dialogs had
     written this out by hand. */
  .caption{font-size:12px; text-transform:uppercase; letter-spacing:.04em;
    color:var(--ink-soft); margin-bottom:8px;}

  /* -------------------------------------------------------------------------
     A table you edit like a spreadsheet.

     Moved here from `static/sales.css` on 2 September 2026, unchanged. Sales
     built it for the boxes and price grids; Bruno then asked for the bending
     tools library to look the same — *"I wanted the tool library to look more
     like Price lists under Sales module or Product Weight & Dimensions"* — and
     the way to make two screens look the same is one set of rules, not two.
     What stayed in `sales.css` is only what the price grid alone needs: a
     heading that is a name rather than a label, and its pressable rows.

     `erp.css` is loaded by both shells; `sales.css` is loaded after it, so
     Sales can still override any of this.
     ------------------------------------------------------------------------- */
  /* A unit written inside the field it belongs to. Bruno asked for € on the price
     grid and kg in Weight & size, and a column heading that says "Weight (kg)" is
     not the same thing: by the time you are typing, the heading has scrolled away
     and the number in front of you says nothing about what it measures. The suffix
     sits inside the box, greyed, and the input is padded so a long number never
     runs underneath it. */
  .unitfield{position:relative; display:inline-block;}
  .unitfield > input{padding-right:24px; text-align:right;}
  .unitfield::after{content:attr(data-unit); position:absolute; right:8px;
    top:50%; transform:translateY(-50%); font-size:11px; line-height:1;
    color:var(--ink-soft); pointer-events:none;}

  /* ---------------------------------------------------------------------------
     A table you edit like a spreadsheet.

     The Boxes table earned this. Its first version stacked a code, a name and a
     description into one cell and a supplier, a code and a spec into another —
     Bruno's word was "scrambled", and he was right: rows of different heights
     with three type sizes in them read as a mess however good the data is. The
     rule here is one value per cell, one line per cell, every input filling its
     column so the edges line up down the page.
     --------------------------------------------------------------------------- */
  /* `width` as well as `min-width`: fixed table layout is ignored when the
     table's width is `auto`, which is why the header widths were being treated as
     a suggestion and every column came out squeezed. */
  table.sheet{table-layout:fixed; width:100%; min-width:1066px;}
  table.sheet td{padding:3px 3px; vertical-align:middle;}

  /* Tight padding inside the fields. A bordered input needs room so the text does
     not touch the line; a flat one does not, and every pixel of it was coming out
     of the characters you can actually read. */
  table.sheet td input{padding:4px 6px; font-size:12.5px;}
  /* …but not on the right of a field that carries its unit inside it, or the
     number is drawn underneath the mm. That is what tightening the padding did
     the first time: `1200` and `mm` on top of each other. */
  table.sheet td .unitfield > input{padding-right:21px;}
  /* The unit itself, smaller and closer in, because in a 60px column the space it
     reserves is space the number needed. `1200` under `mm` was the first attempt
     at this. */
  table.sheet .unitfield::after{right:4px; font-size:10px;}
  /* Headings wrap. "Vendor code" is wider than its column, and `nowrap` does not
     make a column wider — it makes the words run over the next heading, which is
     what "Vendor code" was doing to "Length". Bottom-aligned so a two-line
     heading still sits on the same baseline as its neighbours. */
  table.sheet th{white-space:normal; vertical-align:bottom; line-height:1.25;}
  table.sheet td > input,
  table.sheet td > .unitfield{width:100%; display:block;}
  table.sheet .unitfield > input{width:100%;}

  /* **The fields are flat until you touch them.**

     Bruno noticed that the two price cells Inventory owns — plain text, no box —
     looked better than the eleven outlined inputs beside them, and asked for that
     everywhere. He is right. A table with an outline round every cell reads as a
     wall of form, and this is a table looked at far more often than it is typed
     into. The border arrives on hover and on focus, which is when it means
     something: "you can change this" and "you are changing this".

     Flat also buys back about 8px of horizontal room per column, which is most of
     why the text was being cut off. */
  table.sheet td > input,
  table.sheet td .unitfield > input{background:transparent; border-color:transparent;
    transition:border-color .12s ease, background .12s ease;}
  table.sheet tbody tr:hover td > input,
  table.sheet tbody tr:hover td .unitfield > input{border-color:var(--line);
    background:var(--panel);}
  /* Focus has to beat hover, and it was not: you cannot click a cell without
     the pointer being over its row, so the hover rule above — one class more
     specific — was painting the border line-grey at the very moment the field
     became the one you were typing in. Found by measuring rather than by
     looking, which is the only way this kind of thing is ever found: both
     borders are a pale grey line and the wrong one is not obviously wrong. */
  table.sheet tbody tr:hover td .unitfield > input:focus,
  table.sheet tbody tr:hover td > input:focus,
  table.sheet td .unitfield > input:focus,
  table.sheet td > input:focus{border-color:var(--accent);
    background:var(--panel);}

  /* Except the ones Inventory owns, which stay flat even on hover — there is
     nothing to change here, and offering a box that throws away what you type is
     worse than no box. */
  /* …and the same specificity trap on the other side. A field the row does not
     own — a price a derived list works out, a supplier Inventory keeps — must
     stay flat even while the row is hovered, and the hover rule was winning here
     too, so every read-only cell grew a border promising an edit that could not
     happen. Clicking one still focuses it, browsers being what they are, so the
     focus ring is turned off as well: nothing about it should suggest typing. */
  table.sheet input[readonly],
  table.sheet tbody tr:hover td .unitfield > input[readonly],
  table.sheet tbody tr:hover td > input[readonly],
  table.sheet tbody tr:hover input[readonly]{background:transparent;
    border-color:transparent; color:var(--ink-soft); cursor:default;}
  table.sheet input[readonly]:focus{outline:none; border-color:transparent;}

  /* The number spinners. Useful on a weight you nudge, noise on a hundred cells
     you type into — and they eat 14px of a 62px column. */
  table.sheet input[type=number]::-webkit-inner-spin-button,
  table.sheet input[type=number]::-webkit-outer-spin-button{
    -webkit-appearance:none; margin:0;}
  table.sheet input[type=number]{-moz-appearance:textfield;}

  /* Cells with nothing to type into.

     The products table is mostly read: the product is Shopfloor's, and the box,
     its size and its volume are the boxes table's. Only the weight is this
     table's own, and it is the only field on the row — which is the point, and
     is why the read-only cells are set as plain text rather than as boxes that
     refuse to be typed in. `.key` is the row's identity, in full ink; `.borrowed`
     is a value that belongs to another table, in the softer grey the rest of the
     app uses for figures it did not decide. */
  table.sheet td.key{font-weight:600; white-space:nowrap; overflow:hidden;
    text-overflow:ellipsis; padding-left:7px;}
  table.sheet td.borrowed{color:var(--ink-soft); font-size:12.5px;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    padding-left:7px;}

  /* "Something is happening, wait." — `static/busy.js`, 25 October 2026.
     Bruno: *"I'd like to be aware that something's going on in the background
     and just to wait, like mouse turning into something that lets you know
     that this is the case."*

     `progress`, not `wait`: the old page is still there and still scrolls
     while the next one is fetched, so "working" is true and "blocked" is not.

     The `*` and the `!important` are both load-bearing, and are why the nine
     places that used to set `document.body.style.cursor` could never work
     properly. A cursor is inherited, so **any element with one of its own
     beats it** — and every button, link and tab in this stylesheet sets
     `cursor:pointer`. The button you just pressed was the one element
     guaranteed to be under the pointer and the one element that went on
     showing a hand. */
  html.busy, html.busy *{cursor:progress !important;}
