/* app.css -- the planner surface: grids, the 3D viewer chrome, item grid, chat,
 * result panel.
 *
 * Loaded last, and the media queries sit at the end of this file on purpose. Media
 * queries carry no extra specificity, so when several match, the last one wins --
 * the order must run wide to narrow. Putting the responsive block earlier is what
 * silently disabled the whole mobile layout during the mockup work.
 */
.page-shell {
  min-height: calc(100vh - 70px);
}
.breadcrumbs {
  height: 42px;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 0 24px;
  color: var(--muted);
  font-size: 13px;
  position: sticky;
  top: var(--header-h);
  z-index: 15;
  background: var(--bg);
}
.breadcrumbs .current {
  color: var(--text);
}
.workspace {
  padding: 0;
}
/* One rail width for both modes. They were 460 and 380: the conversation was given
   the wider one back when it was the whole of its rail. It now shares the column with
   a folded form, and more to the point the two modes are the same page with different
   things in the rail -- so a reader moving between them should not watch the plan
   change size. Merged rather than matched, because two numbers that must agree are
   two numbers that will not. */
.ai-grid,
.manual-grid {
  display: grid;
  grid-template-columns: 380px 1fr;
  gap: 14px;
}
.left-stack {
  display: grid;
  gap: 12px;
  align-content: start;
}
.reason-list {
  padding: 0;
  margin: 0;
  list-style: none;
}
.reason-row {
  min-height: 38px;
  display: grid;
  grid-template-columns: 22px 1fr auto;
  gap: 8px;
  align-items: center;
  border-bottom: 1px solid var(--line);
  color: var(--text-2);
  font-size: 12px;
}
.reason-row:last-child {
  border-bottom: 0;
}
.reason-row .value {
  color: var(--text);
  text-align: right;
}
.rules {
  margin: 6px 0 4px 20px;
  color: var(--text);
}
.rules li {
  margin: 4px 0;
}
.reason-footer {
  padding-top: 14px;
  display: flex;
  justify-content: space-between;
  color: var(--muted);
  font-size: 12px;
}
.metric-green {
  color: var(--green) !important;
  font-weight: 750;
}
.viewer-card {
  overflow: hidden;
}
.viewer-toolbar-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--muted);
  font-size: 12px;
}
.viewer-area {
  height: auto;
  position: relative;
  /* Its own ground, not a panel colour: in dark the scene has to sit on something
     deeper than the cards around it or the load stops reading as lit. */
  background: radial-gradient(circle at 50% 35%, var(--viewer-bg-1) 0, var(--viewer-bg-2) 46%, var(--viewer-bg-2) 100%);
  overflow: hidden;
  aspect-ratio: 2.38;
  min-height: 236px;
  max-height: 56vh;
}
.viewer-area.manual {
  height: auto;
}
.viewer-canvas {
  width: 100%;
  height: 100%;
  display: block;
}
.tool-rail {
  position: absolute;
  z-index: 3;
  left: 12px;
  top: 18px;
  background: var(--float-bg);
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
  box-shadow: var(--shadow);
}
.tool-rail button {
  width: 62px;
  height: 62px;
  border: 0;
  border-bottom: 1px solid var(--line);
  background: transparent;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  color: var(--text-2);
  font-size: 10px;
}
.tool-rail button:last-child {
  border-bottom: 0;
}
.tool-rail button:hover {
  background: var(--primary-soft);
  color: var(--primary);
}
.float-panel {
  position: absolute;
  z-index: 3;
  right: 14px;
  top: 16px;
  width: 186px;
  background: var(--float-bg);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 13px;
  box-shadow: var(--shadow);
}
.float-panel.legend {
  top: 232px;
}
.float-title {
  font-weight: 720;
  margin-bottom: 11px;
  display: flex;
  justify-content: space-between;
}
.layer-row, .legend-row {
  display: grid;
  grid-template-columns: 18px 13px 1fr auto;
  gap: 7px;
  align-items: center;
  min-height: 27px;
  font-size: 11px;
  color: var(--text);
}
.legend-row {
  grid-template-columns: 13px 1fr auto;
}
.viewer-help {
  position: absolute;
  left: auto;
  bottom: 12px;
  z-index: 3;
  background: var(--float-bg-soft);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 6px 10px;
  color: var(--muted);
  font-size: 11px;
  right: 14px;
}
/* The slice-and-drag controls, docked top-left of the canvas. dat.GUI paints its own
   panel inside; all this owns is the button and whether the panel is shown. */
.filter-dock {
  position: absolute;
  z-index: 4;
  left: 12px;
  top: 14px;
  /* The dock is what the panel's height is measured against, so the panel can never
     run past the bottom of the viewer however many rows dat.GUI adds. */
  max-height: calc(100% - 28px);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.filter-toggle {
  flex: none;
}
/* Nothing to slice yet. The dock stays in the tree for dat.GUI's sake; only the
   control that offers to open it goes away. */
.filter-dock.idle .filter-toggle {
  display: none;
}
.filter-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 31px;
  padding: 0 10px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--float-bg);
  box-shadow: var(--shadow);
  color: var(--text-2);
  font: inherit;
  font-size: 11px;
  cursor: pointer;
}
.filter-toggle:hover {
  background: var(--primary-soft);
  color: var(--primary);
}
.filter-toggle:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}
.filter-dock.open .filter-toggle {
  background: var(--primary-soft);
  color: var(--primary);
}
/* Slice and Drag sit side by side; the panel opens underneath both. */
.dock-row {
  display: flex;
  gap: 6px;
  flex: none;
}
/* `.filter-dock.open .filter-toggle` also matches this button -- same class, inside
   the same dock -- so the open panel would tint it as though drag were on. It has to
   restate its own resting colours at a weight that rule cannot reach. */
.drag-toggle,
.filter-dock.open .drag-toggle {
  background: var(--float-bg);
  color: var(--text-2);
}
/* Loud on purpose, and the only accent-filled thing over the canvas: while this is on,
   a click means "pick this box up" rather than "turn the scene", and that is not a
   state to leave a visitor guessing at. */
.drag-toggle.on,
.drag-toggle.on:hover,
.filter-dock.open .drag-toggle.on {
  background: var(--accent-fill-1);
  border-color: var(--accent-fill-2);
  color: var(--on-accent);
}
/* Hidden without leaving layout. The node is never removed -- dat.GUI appends into
   it once at renderer init and holds the reference -- and `display:none` is equally
   out, because dat.GUI measures its own list height when it builds, and a list built
   inside a display:none parent measures zero and opens as a 7px strip. visibility
   and a clipped height hide it while it still has a box to measure. */
.filter-dock:not(.open) #visual-filter {
  visibility: hidden;
  max-height: 0;
  overflow: hidden;
  pointer-events: none;
}
.filter-dock #visual-filter {
  margin-top: 6px;
  min-height: 0;
}
.view-switch {
  position: absolute;
  z-index: 3;
  right: 14px;
  /* Anchored near the top, not 168px down. The mockup's offset assumed a tall
     canvas; once the result panel takes its 300px the viewer is short enough that a
     fixed offset pushed the centre-of-gravity toggle outside the card -- at desktop
     width as well as on a phone. Measured from the top it fits at every height the
     viewer can take. */
  top: 14px;
  width: 78px;
  background: var(--float-bg);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 6px;
  box-shadow: var(--shadow);
}
.view-switch button {
  width: 100%;
  height: 31px;
  border: 0;
  background: transparent;
  border-radius: 6px;
  color: var(--text-2);
  font-size: 11px;
}
.view-switch button.active, .view-switch button:hover {
  background: var(--primary-soft);
  color: var(--primary);
}
.canvas-options {
  position: absolute;
  z-index: 3;
  left: 12px;
  bottom: 10px;
  display: flex;
  gap: 18px;
  background: var(--float-bg-soft);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 7px 10px;
  font-size: 11px;
  color: var(--text-2);
}
.canvas-options label {
  display: flex;
  align-items: center;
  gap: 6px;
}
.result-strip {
  display: grid;
  grid-template-columns: repeat(4,minmax(150px,1fr));
  gap: 12px;
  padding: 14px 0 0;
}
.stat-card {
  min-height: 170px;
  padding: 14px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.stat-card h4 {
  margin: 0 0 15px;
  font-size: 13px;
  margin-bottom: 12px;
}
.big-number {
  font-size: 32px;
  font-weight: 760;
  letter-spacing: -.5px;
}
.stat-sub {
  color: var(--muted);
  font-size: 12px;
  margin-top: 8px;
  line-height: 1.7;
  white-space: nowrap;
}
.donut {
  --value: 92.6deg;
  width: 90px;
  height: 90px;
  border-radius: 50%;
  background: conic-gradient(var(--green) 0 333deg, var(--surface-3) 333deg);
  position: relative;
  display: grid;
  place-items: center;
  margin: 0 auto 12px;
}
.donut::after {
  content: "";
  position: absolute;
  inset: 11px;
  border-radius: 50%;
  background: var(--surface);
}
.donut .center {
  position: relative;
  z-index: 1;
  text-align: center;
  font-weight: 780;
  font-size: 18px;
}
.donut .center small {
  display: block;
  font-size: 9px;
  color: var(--muted);
  font-weight: 600;
}
.reco-card {
  min-height: 195px;
}
.reco-item {
  display: grid;
  grid-template-columns: 24px 1fr auto;
  gap: 10px;
  align-items: start;
  padding: 14px 15px;
  border-bottom: 1px solid var(--line);
  font-size: 12px;
}
.reco-item:last-child {
  border-bottom: 0;
}
.reco-icon.success {
  color: var(--green);
}
.reco-icon.warning {
  color: var(--amber);
}
.reco-footer {
  text-align: center;
  padding: 13px;
  color: var(--primary);
  font-weight: 650;
  font-size: 12px;
}
.app-footer {
  margin-top: 14px;
  min-height: 64px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px 10px 18px;
}
.footer-meta {
  display: flex;
  align-items: center;
  gap: 20px;
  color: var(--text-2);
  font-size: 12px;
}
.footer-meta .divider {
  width: 1px;
  height: 24px;
  background: var(--line);
}
.autosaved {
  color: var(--green);
  display: flex;
  align-items: center;
  gap: 5px;
}
.split-button {
  display: flex;
}
.split-button .btn:first-child {
  border-radius: 8px 0 0 8px;
}
.split-button .btn:last-child {
  border-radius: 0 8px 8px 0;
  padding: 0 12px;
  border-left: 1px solid rgba(255,255,255,.25);
}
.manual-panel {
  overflow: hidden;
}
.manual-scroll {
  padding: 0 16px 14px;
}
.form-section {
  padding: 15px 0 16px;
  border-bottom: 1px solid var(--line);
}
.form-section:last-of-type {
  border-bottom: 0;
}
.section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 14px;
}
.section-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 720;
}
.step {
  width: 21px;
  height: 21px;
  border-radius: 50%;
  border: 1.5px solid var(--primary);
  color: var(--primary);
  display: grid;
  place-items: center;
  font-size: 11px;
  font-weight: 750;
}
.constraint-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 10px;
}
.constraint-line {
  display: grid;
  grid-template-columns: 132px 1fr;
  gap: 9px;
  align-items: center;
  font-size: 11px;
  color: var(--text-2);
}
.objectives {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px;
}
.objective {
  height: 34px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  font-size: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  color: var(--text-2);
}
.objective.active {
  color: var(--primary);
  border-color: var(--focus-ring);
  background: var(--primary-soft);
  font-weight: 720;
}
.form-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  padding-top: 13px;
}
.manual-lower {
  display: grid;
  grid-template-columns: minmax(560px, 1fr) 390px;
  gap: 12px;
  margin-top: 12px;
}
.item-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 11px;
}
.item-table th {
  text-align: left;
  color: var(--muted);
  font-weight: 650;
  padding: 10px 8px;
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
}
.item-table td {
  padding: 9px 8px;
  border-bottom: 1px solid var(--line);
  color: var(--text);
}
.item-table tr:last-child td {
  border-bottom: 0;
}
.table-footer {
  display: flex;
  gap: 18px;
  align-items: center;
  padding: 10px 12px;
  color: var(--text-2);
  font-size: 11px;
  border-top: 1px solid var(--line);
}
/* The ring above the figures rather than beside them.
   Side by side, the list had 152px for a label, a gap and a value -- so "Weight
   600 / 28620 kg · 2%" and a container's three interior dimensions both broke
   across lines, and the numbers a freight person is actually reading were the
   ones that arrived in pieces. Stacked, every row has the full card width and
   fits on one line, at the cost of about 40px of height in a panel that had it
   to spare. */
.summary-body {
  display: grid;
  gap: 12px;
  padding: 14px;
  justify-items: center;
}
.summary-list {
  width: 100%;
}
.summary-body .donut {
  width: 90px;
  height: 90px;
  background: conic-gradient(var(--green) 0 305deg, var(--surface-3) 305deg);
}
.summary-list {
  display: grid;
  gap: 11px;
  font-size: 11px;
}
.summary-line {
  display: flex;
  justify-content: space-between;
  /* space-between puts no space between anything once the value is wide enough to
     fill the row -- which is exactly what a container's three interior dimensions
     do, printing "Interior1203.2 x 235 x 269.24 cm". The gap is the floor. */
  gap: 12px;
  color: var(--text-2);
}
/* The figures carry the panel, so they are the one thing in it that is coloured.
   Labels stay muted and the ring stays green: the accent marks what to read, not
   whether the news is good. */
.summary-line strong {
  color: var(--primary);
  text-align: right;
}
/* An aside inside a value -- "no limit set" beside a weight -- keeps the muted
   `.summary-note` colour it declares for itself: a rule that matches an element
   beats a colour inherited from its parent, so the accent above stops at the span. */
.validation {
  margin-top: 12px;
}
.validation-body {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 18px;
  padding: 10px 14px 14px;
  font-size: 11px;
  color: var(--text-2);
}
.note-line {
  display: flex;
  gap: 7px;
  margin-top: 8px;
}
.note-line.warning {
  color: var(--amber-ink);
}
.note-line.info {
  color: var(--text-2);
}
.left-stack {
  display: block;
  flex-direction: column;
  gap: 0;
}
.chat-head {
  flex: none;
  padding: 13px 16px;
  border-bottom: 1px solid var(--work-line);
}
.chat-head .card-title {
  font-size: 15px;
}
.chat-log {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 13px;
}
.msg {
  font-size: 13.5px;
  line-height: 1.55;
}
.msg.user {
  align-self: flex-end;
  max-width: 88%;
  background: var(--primary);
  color: var(--on-accent);
  border-radius: 13px 13px 3px 13px;
  padding: 10px 13px;
}
/* The answer, on a ground of its own -- see --chat-said. It used to be filled with
   --surface, the colour of the panel it is drawn on, which left one hairline doing
   all the work of saying where an answer started and ended. */
.msg.bot {
  align-self: flex-start;
  max-width: 94%;
  background: var(--chat-said);
  border: 1px solid var(--chat-said-line);
  border-radius: 13px 13px 13px 3px;
  padding: 11px 14px;
  color: var(--text);
}
/* Paragraphs inside an answer. The formatter emits one <p> per line, and without
   this they inherit the document's margin and the bubble grows a gap at each end. */
.msg p {
  margin: 0;
}
.msg p + p {
  margin-top: 8px;
}
.msg strong {
  font-weight: 700;
}
/* The log scrolls inside the panel rather than growing the whole rail, so the box
   you type into stays where you left it however long the conversation gets. The rail
   still scrolls; this just stops the composer being pushed off the bottom of it. */
.chat-log {
  max-height: 46vh;
}
/* Empty, the panel has to say what it is for. A blank box with a Send button teaches
   nobody what to type. */
.chat-opening {
  display: grid;
  gap: 10px;
  justify-items: start;
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-2);
}
.chat-opening p {
  margin: 0;
}
/* Waiting. `.working` is the same bubble as any other answer, because that is what it
   turns into -- a spinner somewhere else on the page would leave the conversation
   looking like it had stopped mid-sentence. */
.msg.bot.working {
  display: flex;
  align-items: center;
  gap: 9px;
  color: var(--text-2);
}
.chat-dots {
  display: inline-flex;
  gap: 3px;
}
.chat-dots i {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--primary);
  animation: chat-dot 1.1s ease-in-out infinite;
}
.chat-dots i:nth-child(2) { animation-delay: 0.15s; }
.chat-dots i:nth-child(3) { animation-delay: 0.3s; }
@keyframes chat-dot {
  0%, 60%, 100% { opacity: 0.25; }
  30% { opacity: 1; }
}
/* A solve can run for minutes. Somebody who has asked not to see motion should get a
   steady mark rather than nothing at all, or the page reads as hung. */
@media (prefers-reduced-motion: reduce) {
  .chat-dots i {
    animation: none;
    opacity: 0.65;
  }
}
.composer .send:disabled,
.composer .attach:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
/* Quieter than Send: attaching is a step towards asking, not the asking. */
.composer .attach {
  position: absolute;
  right: 50px;
  bottom: 8px;
  width: 36px;
  height: 36px;
  border-radius: 9px;
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--text-2);
  display: grid;
  place-items: center;
  cursor: pointer;
}
@media (hover: hover) {
  .composer .attach:hover:not(:disabled) {
    border-color: var(--primary);
    color: var(--primary);
  }
}
/* What is about to go up, named and sized. "3 files attached" is not something
   anyone can check before pressing Send. */
.attach-list {
  list-style: none;
  margin: 0 0 9px;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.attach-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 4px 4px 4px 8px;
  border: 1px solid var(--line-strong);
  border-radius: 7px;
  background: var(--surface);
  font-size: 11px;
  color: var(--text-2);
}
/* The name is the part worth reading, so it is the part that truncates -- and it
   truncates rather than wrapping, because a wrapped chip stops looking like a chip. */
.attach-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text);
}
.attach-size {
  flex: none;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.attach-drop {
  flex: none;
  width: 18px;
  height: 18px;
  border: 0;
  border-radius: 5px;
  background: transparent;
  color: var(--muted);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}
@media (hover: hover) {
  .attach-drop:hover:not(:disabled) {
    background: var(--danger-soft);
    color: var(--danger);
  }
}
.composer .form-note {
  margin: 0 0 9px;
}
/* The seam between the conversation and the form it fills in. They are different
   jobs sharing one column, so the boundary is drawn rather than guessed at. */
.left-stack>.card:first-child {
  border-bottom: 1px solid var(--work-line);
}
/* Describe mode's form, folded. The disclosure *is* the panel's header here -- a
   card-header above a summary saying much the same thing twice is how a rail gets
   long for no reason. */
.left-stack>.card.compact {
  padding: 0;
}
.form-fold {
  border: 0;
  border-radius: 0;
  background: transparent;
}
.form-fold>summary {
  padding: 13px 16px;
  background: transparent;
  font-weight: 650;
  font-size: 13px;
}
.form-fold .disclosure-body {
  border-top: 1px solid var(--work-line);
  padding: 0 16px;
}
/* The buttons stay out of the fold, so a dimension corrected in the grid can still
   be run without opening anything. */
.fold-actions {
  padding: 0 16px 14px;
}
.fold-actions .form-actions {
  margin-top: 0;
}
/* Full width, because it is alone in the rail when it appears and a half-width
   button beside nothing reads as a mistake. Secondary, because Send is this mode's
   primary action and two primaries is no primary at all. */
.btn.block {
  width: 100%;
  justify-content: center;
}
/* `?embed=true`: the plan filling the frame it was given.

   The header is removed by `:has()` rather than by a class on <body>, because the
   layout template is shared with pages that are not mine to restructure. The header
   is never *rendered* for an embed either -- this only covers chrome the layout
   itself contributes. */
body:has(.embed-shell) .app-header,
body:has(.embed-shell) .breadcrumbs {
  display: none;
}
body:has(.embed-shell) {
  min-width: 0;
}
.embed-shell {
  height: 100vh;
  display: flex;
  flex-direction: column;
}
.embed-shell>bfb-viewer,
.embed-shell .viewer-card {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
/* An embed is a picture of a load, not a workspace: no card title bar, and the
   viewer takes the whole frame rather than an aspect-ratio slice of it. */
.embed-shell .viewer-card>.card-header {
  display: none;
}
.embed-shell .viewer-area {
  flex: 1 1 auto;
  height: auto;
  max-height: none;
  aspect-ratio: auto;
}
/* Controls for a load that has been rearranged by hand. They sit over the canvas,
   bottom-left, clear of the view toggles on the right and the help line beneath. Only
   rendered when there is an edit to act on, so nothing reserves space for them. */
/* Bottom right, above the drag hint -- not bottom left, which is under the slice
   dock. That dock grows to the full height of the viewer when dat.GUI's panel is
   opened, and it covered Save layout completely: the one control that says there is
   unsaved work, hidden by a panel opened to do the work. This corner holds only the
   one-line hint, which these sit above.

   z-index above the hint as well as the canvas, so a wide "move to container" row
   overlaps the hint rather than being overlapped by it. */
.edit-controls {
  position: absolute;
  right: 14px;
  bottom: 44px;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  flex-wrap: wrap;
  max-width: calc(100% - 28px);
}
.save-layout {
  box-shadow: 0 2px 10px var(--shadow);
}
.send-to {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 8px;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: var(--surface);
  box-shadow: 0 2px 10px var(--shadow);
  font-size: 11px;
  color: var(--text-2);
}
.send-to button {
  min-width: 24px;
  height: 24px;
  padding: 0 6px;
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  background: var(--surface-2);
  color: var(--text);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
@media (hover: hover) {
  .send-to button:hover {
    border-color: var(--primary);
    color: var(--primary);
  }
}
.msg-meta {
  font-size: 10px;
  color: var(--muted);
  margin-top: 7px;
}
.understood {
  margin-top: 9px;
  border-top: 1px solid var(--line);
  padding-top: 8px;
}
.understood summary {
  cursor: pointer;
  font-size: 11px;
  font-weight: 650;
  color: var(--primary);
}
.understood dl {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 5px 10px;
  margin: 9px 0 0;
  font-size: 11px;
}
.understood dt {
  color: var(--muted);
}
.understood dd {
  margin: 0;
  text-align: right;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
/* No ground of its own: this rail already *is* the --work surface (see
   `.left-stack > .card:first-child`), so filling the dock with --work paints it the
   colour it is already sitting on. The distinction has to come from the field, one
   step above the rail -- which is the same move the manual form makes with its own
   inputs. */
.composer {
  flex: none;
  border-top: 1px solid var(--work-line);
  padding: 13px 14px 14px;
}
.composer .prompt-chips {
  margin: 0 0 9px;
}
.composer-box {
  position: relative;
}
/* Bigger than a search box, because what goes in it is a cargo list: three or four
   lines of it are visible before the box has to scroll.
   The right padding clears *both* corner controls. It has to be set here rather
   than beside the attach button, which is declared earlier in this sheet: this
   shorthand comes later and would reset a `padding-right` set up there -- which is
   exactly what it was doing, so the text ran under the attach button. */
.composer textarea {
  min-height: 136px;
  padding: 14px 90px 14px 14px;
  font-size: 14.5px;
  line-height: 1.55;
  /* A step above the rail rather than the panel colour: on dark, --surface and
     --work are four values apart and the box disappeared into its own footing. */
  background: var(--surface-2);
  /* Border colour is not set here -- the rail's own rule further down this sheet
     outweighs this selector. See `.left-stack > .card:first-child textarea`. */
  border-radius: 10px;
  box-shadow: 0 1px 3px var(--shadow-xs-c);
}
/* No label, no row, no neighbour -- so it answers the pointer like every other
   control on the surface does, rather than being the one live thing that looks
   inert until clicked. */
@media (hover: hover) {
  .composer textarea:hover:not(:disabled) {
    border-color: var(--primary);
  }
}
.composer .send {
  position: absolute;
  right: 8px;
  bottom: 8px;
  width: 36px;
  height: 36px;
  border-radius: 9px;
  border: 0;
  background: var(--primary);
  color: var(--on-accent);
  display: grid;
  place-items: center;
}
.composer .send:hover {
  filter: brightness(.95);
}
.composer .hint {
  margin-top: 8px;
}
.grid-toolbar {
  display: flex;
  gap: 7px;
  align-items: center;
}
.item-grid-wrap {
  max-height: 326px;
  overflow: auto;
  overscroll-behavior: contain;
}
.item-table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface-3);
  /* The heading and its `?` are one thing. Left to wrap, "Each (kg)" put the
     question mark on a second line and every heading in the row grew to match. */
  white-space: nowrap;
}
.item-table td.expander,.item-table th.expander {
  width: 28px;
  padding-right: 0;
}
.expand-btn {
  border: 0;
  background: transparent;
  color: var(--muted);
  width: 22px;
  height: 22px;
  border-radius: 5px;
  display: grid;
  place-items: center;
}
.expand-btn:hover {
  background: var(--primary-soft);
  color: var(--primary);
}
.expand-btn .icon {
  transition: transform .16s ease;
}
.expand-btn[aria-expanded="true"] .icon {
  transform: rotate(180deg);
}
tr.detail>td {
  background: var(--surface-3);
}
/* Three across, and capped: this row lives inside the table, so it is as wide as the
   table rather than as wide as the card -- and the table is wider than the card and
   scrolls. Five fields spread over the full width put the last of them off the right
   edge, reachable only by scrolling a row whose own controls gave no hint it was
   there. Two rows of three sit inside the visible width instead. */
.detail-grid {
  display: grid;
  grid-template-columns: repeat(3,minmax(0,1fr));
  max-width: 660px;
  gap: 9px 12px;
  padding: 4px 0 9px;
}
.detail-grid .field label {
  font-size: 9px;
}
/* Wrapped at the same width as the fields they explain, for the same reason: the
   cell they sit in is as wide as the scrolling table, so a sentence in it runs off
   the visible edge instead of wrapping. */
.item-table tr.detail .hint {
  max-width: 660px;
}
.detail-grid .field input,.detail-grid .field select {
  height: 31px;
  font-size: 11px;
}
/* Neither the group nor the labels wrap, and that pairing is the point. The chips
   deformed into ellipses because their labels could wrap: a wrapped label makes the
   column's min-content small, the table happily shrank the column to it, and a
   999px-rounded chip two lines tall is an ellipse. With the labels unwrappable the
   column's min-content is the full row of chips, so it cannot be squeezed at all --
   the table scrolls inside its wrapper instead, which is what that wrapper is for.
   Letting the group wrap instead was worse: the column then sized to one chip and
   they stacked even at full width. */
.mini-flags {
  display: flex;
  gap: 5px;
  flex-wrap: nowrap;
}
.mini-flag {
  font-size: 10px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  padding: 3px 8px;
  background: var(--surface);
  color: var(--text-2);
}
.mini-flag.on {
  background: var(--primary-soft);
  border-color: var(--focus-ring);
  color: var(--primary);
  font-weight: 650;
}
.grid-empty {
  padding: 26px 16px;
  text-align: center;
  color: var(--muted);
  font-size: 12px;
}
.card.manual-panel, .left-stack>.card:first-child {
  background: var(--work);
  border-color: var(--work-line);
  box-shadow: inset 0 1px 0 var(--inset-highlight);
}
.card.manual-panel>.card-header, .left-stack>.card:first-child>.card-header {
  background: transparent;
  border-bottom-color: var(--work-line);
}
.card.manual-panel .form-section {
  border-bottom-color: var(--work-line);
}
.card.manual-panel .field label, .left-stack>.card:first-child .label {
  color: var(--work-label);
}
.card.manual-panel .disclosure, .card.manual-panel .toggle-row {
  background: var(--surface);
  border-color: var(--work-line);
}
.card.manual-panel .disclosure>summary {
  background: var(--surface);
}
.card.manual-panel .disclosure .disclosure-body {
  border-top-color: var(--work-line);
}
.card.manual-panel .toggle-cell {
  border-right-color: var(--work-line);
}
.card.manual-panel .constraint-line select, .card.manual-panel .field input, .card.manual-panel .field select {
  border-color: var(--line-strong);
}
/* The hover state has to be restated here, at this selector's weight. The rule
   above is three classes deep and beat the plain `.field select:hover` in base.css
   on specificity, so every control in the rail sat inert under the pointer while
   the same controls elsewhere on the page responded. Kept next to the rule it is
   answering rather than with the other hover rules, because apart they drift. */
@media (hover: hover) {
  .card.manual-panel .constraint-line select:hover,
  .card.manual-panel .field input:hover,
  .card.manual-panel .field select:hover {
    border-color: var(--primary);
  }
}
/* The composer's edge, set here rather than beside the rest of the composer: this
   selector is two classes deep and beats `.composer textarea` on specificity, so
   whatever it says is what the field wears. --field-edge is a step firmer than the
   --line-strong it used to carry -- this is the one field on the page with no label
   and no row around it, so its border is all that says where it is. */
.left-stack>.card:first-child textarea {
  border-color: var(--field-edge);
}
.card.manual-panel .hint, .left-stack>.card:first-child .hint {
  color: var(--text-2);
}
.ai-grid,.manual-grid {
  gap: 0;
  align-items: start;
}
.left-stack, .manual-grid>.card.manual-panel {
  background: var(--work);
  border: 0;
  border-right: 1px solid var(--work-line);
  border-radius: 0;
  box-shadow: none;
  position: sticky;
  top: var(--chrome-h);
  height: calc(100vh - var(--chrome-h));
  overflow-y: auto;
  overscroll-behavior: contain;
}
.left-stack>.card:first-child {
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
.left-stack>.card:nth-child(2) {
  margin: 0 14px 16px;
}
.manual-grid>.card.manual-panel>.card-header {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--work);
}
.ai-grid>section,.manual-grid>section {
  padding: 0 16px 14px;
}
.left-stack>.card:nth-child(2), .viewer-card, .result-strip>.card, .manual-lower>.card, .card.validation {
  box-shadow: 0 1px 2px var(--shadow-xs-c), 0 10px 24px var(--shadow-xs-c);
}
.legend-row>:last-child {
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* One column: the plan takes the whole width of its side of the page. */
.plan-row {
  display: grid;
  grid-template-columns: minmax(0,1fr);
}
/* What you read about the load, beside the load's contents. The result sits at its
   natural height -- stretching it would leave a tall card with a paragraph at the
   top -- while the grid fills whatever height the row has. */
.work-row {
  display: grid;
  grid-template-columns: minmax(0,1fr) 300px;
  gap: 12px;
  align-items: start;
  margin-top: 12px;
}
.work-row>.lower-stack {
  margin-top: 0;
  align-self: stretch;
}
.viewer-card>.card-header {
  gap: 8px;
  padding: 12px;
  min-height: 0;
}
.pager {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  background: var(--surface);
}
.pager button {
  border: 0;
  background: transparent;
  width: 24px;
  height: 30px;
  color: var(--text-2);
  font-size: 14px;
}
.pager button:hover {
  background: var(--primary-soft);
  color: var(--primary);
}
.pager b {
  padding: 0 7px;
  font-size: 11px;
  color: var(--text);
  border-inline: 1px solid var(--line);
  height: 30px;
  display: grid;
  place-items: center;
  font-variant-numeric: tabular-nums;
}
.seg {
  display: flex;
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  background: var(--surface);
}
.seg button {
  border: 0;
  background: transparent;
  height: 30px;
  padding: 0 10px;
  font-size: 11px;
  color: var(--text-2);
}
.seg button+button {
  border-left: 1px solid var(--line);
}
.seg button.active {
  background: var(--primary-soft);
  color: var(--primary);
  font-weight: 650;
}
.view-pop {
  max-inline-size: 214px;
  padding: 10px 12px;
}
.pop-label {
  display: block;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 6px;
}
.view-pop .seg button {
  flex: 1;
  padding: 0 6px;
}
.view-pop .layer-row {
  grid-template-columns: 18px 13px 1fr;
  min-height: 26px;
}
.leftover-note {
  display: grid;
  grid-template-columns: 16px 1fr;
  gap: 8px;
  align-items: start;
  margin: 0 14px 14px;
  padding: 10px 11px;
  border-radius: 8px;
  background: var(--amber-soft);
  border: 1px solid var(--amber-line);
  color: var(--amber-ink);
  font-size: 11px;
  line-height: 1.5;
}
.leftover-note .icon {
  color: var(--amber);
  margin-top: 1px;
}
.leftover-note b {
  color: var(--amber-ink);
}
.ctn-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(258px,1fr));
  gap: 10px;
  padding: 12px;
}
.ctn {
  text-align: left;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
  padding: 15px 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.ctn:hover {
  border-color: var(--focus-ring);
}
.ctn.is-current {
  border-color: var(--primary);
  background: var(--primary-soft);
}
.ctn-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.ctn-head b {
  font-size: 13px;
  color: var(--text);
}
.ctn-head span {
  color: var(--muted);
  font-size: 11px;
}
.ctn-body {
  display: grid;
  grid-template-columns: 64px 1fr;
  gap: 12px;
  align-items: center;
}
.donut.sm {
  width: 64px;
  height: 64px;
  margin: 0;
}
.donut.sm::after {
  inset: 8px;
}
.donut.sm .center {
  font-size: 13px;
}
.ctn-lines {
  display: grid;
  gap: 5px;
  font-size: 11px;
}
.ctn-lines>div {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  color: var(--text-2);
}
.ctn-lines strong {
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.colour-key {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  margin-top: 4px;
  padding-top: 9px;
  border-top: 1px solid var(--line);
  font-size: 11px;
  color: var(--text-2);
}
.colour-key>span {
  display: flex;
  align-items: center;
  gap: 6px;
}
.colour-key b {
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.lower-stack {
  margin-top: 12px;
}
.lower-stack>.card {
  width: 100%;
}
.manual-grid>section,.ai-grid>section {
  display: flex;
  flex-direction: column;
  height: calc(100vh - var(--chrome-h));
}
.ai-grid>section>.plan-row,
.ai-grid>section>.work-row,
.manual-grid>section>.work-row {
  flex: 1 1 auto;
  min-height: 0;
}
/* min-width:0 on every flex and grid item down this column. The default is `auto`,
   which resolves to min-content -- and the item grid's min-content is a whole table.
   Without this, a table wider than its share does not scroll inside its wrapper: it
   widens the row, the row widens past the section, and the result card is carried
   off the right edge of the window. The grid decides the widths; the table scrolls
   in the space it is given. */
.ai-grid>section>*,
.manual-grid>section>*,
.work-row>*,
.ai-grid>section .lower-stack>.card,
.manual-grid>section .lower-stack>.card {
  min-width: 0;
}
.ai-grid>section .lower-stack {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.ai-grid>section .lower-stack>.card {
  flex: none;
}
.ai-grid>section .lower-stack>.card {
  flex: 1 1 auto;
  min-height: 150px;
  display: flex;
  flex-direction: column;
}
.ai-grid>section .lower-stack>.card>.card-header {
  flex: none;
}
.ai-grid>section .lower-stack>.card .item-grid-wrap {
  flex: 1 1 auto;
  min-height: 0;
  max-height: none;
}
.ai-grid>section .lower-stack>.card .table-footer {
  flex: none;
}
.ai-grid>section .lower-stack .ctn-grid {
  max-height: 210px;
  overflow: auto;
  align-content: start;
}
.manual-grid>section .lower-stack {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
}
.manual-grid>section .lower-stack>.card {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.manual-grid>section .lower-stack>.card>.card-header {
  flex: none;
}
.manual-grid>section .lower-stack .item-grid-wrap {
  flex: 1 1 auto;
  min-height: 150px;
  max-height: none;
}
.manual-grid>section .lower-stack .table-footer {
  flex: none;
}
.big-number,.item-table td,.summary-line strong,.reason-row .value {
  font-variant-numeric: tabular-nums;
}
.summary-note {
  color: var(--muted);
  font-weight: 600;
}
.next-step {
  display: grid;
  grid-template-columns: 22px 1fr auto;
  gap: 10px;
  align-items: center;
  padding: 13px 15px;
  border-bottom: 1px solid var(--line);
  font-size: 12px;
}
.next-step:last-child {
  border-bottom: 0;
}
.constraint-line select {
  width: 100%;
  height: 34px;
  border: 1px solid var(--line-strong);
  border-radius: 7px;
  background: var(--surface);
  color: var(--text);
  padding: 0 8px;
  font-size: 12px;
}

/* ── The editable item grid ─────────────────────────────────────────────────────
 * The mockup showed the grid as read-only text, so these rules are new. The cells
 * are inputs now, and a table of thirty boxed inputs reads as a form to fill in
 * rather than a list to scan -- so the input is invisible until it is hovered or
 * focused, and the row keeps the density of the text version it replaces.
 * Unlayered, like the rest of these sheets: an @layer block here would lose to
 * every unlayered rule above it, including .field input's own height.
 */
.item-table input {
  width: 100%;
  min-width: 0;
  height: 26px;
  padding: 0 6px;
  border: 1px solid var(--line);
  border-radius: 6px;
  /* Sunken against the card, so a cell reads as somewhere to type without needing a
     heavy border. These were transparent until hover, which was quieter and wrong:
     a first-time visitor cannot hover every cell to discover the grid is editable,
     and "40 x 30 x 25" in bare text looks like a result, not a form. */
  background: var(--surface-3);
  color: var(--text);
  font: inherit;
  font-variant-numeric: tabular-nums;
}
.item-table input::placeholder {
  color: var(--muted-faint);
}
.item-table input:hover {
  border-color: var(--primary);
}
.item-table input:focus-visible {
  border-color: var(--focus-ring);
  background: var(--surface);
}
/* Laid out rather than calculated. The swatch and the field used to be inline with
   the field at `calc(100% - 22px)`, which fits only until the newline between the
   two tags counts as a text node -- the row then overflows by a pixel and the field
   wraps under the swatch. Flex has no whitespace to get wrong.
 *
 * On a wrapper inside the cell, never on the cell: `display:flex` on a <td> takes it
 * out of table layout, so it stops stretching to the row's height and vertical-align
 * no longer applies to it. Two cells done that way sat 13px above the rest of the
 * row -- worse than the misalignment being fixed. */
.item-table td .cell-inline {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}
.item-table td.cell-name input {
  font-weight: 650;
  flex: 1 1 60px;
  width: auto;
}
/* Out of flow, so saying what is wrong does not move the field being corrected.
   In flow the message became a second line in the cell, the cell grew, and
   vertical-align:middle re-centred the taller block -- which lifted the name field
   nine pixels clear of every other field in the row, at the exact moment the eye
   was going there. The row makes room for the message instead of the cell doing
   it: `:has()` means only a row that actually has one pays for it. */
.item-table td .cell-inline {
  position: relative;
}
.item-table td .cell-inline .field-error {
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: 3px;
  white-space: nowrap;
}
.item-table tbody tr:has(.field-error) > td {
  padding-bottom: 26px;
}
/* L x W x H in one cell: three inputs that shrink together, with the separators
   staying put. minmax(0,1fr) rather than 1fr, or the inputs' min-content width
   would push the column wider than its share of the table. */
/* The three dimensions share one bordered control, so the row reads as name, count,
   size, weight rather than five interchangeable boxes of digits. The inputs inside
   lose their own chrome -- two nested borders would undo the grouping. */
/* Lays itself out rather than borrowing `.cell-inline`'s flex, so it can sit inside
   one as a single item alongside its error message. A wrapped dimension group breaks
   "120 x 80 x 145 cm" across two lines with the separators dangling, which is the
   arrangement this control exists to prevent. */
.dim-group {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  flex: 1 1 auto;
  min-width: 0;
  gap: 2px;
  padding: 0 7px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--surface-3);
}
.item-table td.cell-qty .cell-inline,
.item-table td.cell-weight .cell-inline,
.item-table td.cell-size .cell-inline {
  flex-wrap: nowrap;
}
.dim-group input {
  height: 24px;
  padding: 0 2px;
  border: 0;
  background: transparent;
  text-align: center;
}
.dim-group:hover {
  border-color: var(--primary);
}
.dim-group:focus-within {
  border-color: var(--focus-ring);
  background: var(--surface);
}
.cell-size .times {
  color: var(--muted);
  flex: 0 0 auto;
}
/* Which of the three is wrong still has to be visible, and the input inside the
   group has no border of its own left to colour. */
.dim-group input[aria-invalid='true'] {
  background: var(--danger-soft);
  border-radius: 4px;
  box-shadow: inset 0 0 0 1px var(--danger);
}
/* Units and the count's multiplication sign. Muted, because they are what the
   number means rather than something to read. */
.affix {
  flex: 0 0 auto;
  color: var(--muted);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.affix.lead {
  font-size: 12px;
}
.cell-qty .cell-inline,
.cell-weight .cell-inline {
  gap: 5px;
}
/* Room for the numbers people actually type, without the field's own intrinsic size
   deciding how wide the column is.
 *
 * `width` has to be a definite length, not `auto`. A text input's intrinsic width is
 * its `size` attribute -- twenty characters, about 164px -- and while `width:auto`
 * leaves that in play it is what the table measures the column against, whatever
 * min-width says. Five such fields demanded 1357px of a 1004px table. With a
 * definite width the column is sized by this rule, the fields still grow to fill
 * what they are given, and they stop shrinking at four digits: a centred value wider
 * than its box is clipped at both ends, so 1203.5 read as "203." with nothing to
 * show it had been cut. */
.cell-qty input,
.cell-weight input,
.dim-group input {
  flex: 1 1 auto;
  /* Seven characters. Six covered 1203.5 but not 3333.22, and a millimetre entry
     of a container-length dimension runs to seven easily. */
  width: 5em;
  min-width: 5em;
}
/* These two keep the standard cell padding -- 6px a side against the group's 2 --
   so the same declared width leaves them twelve pixels less to write in. A weight
   of 28620 was clipped by two. */
.cell-qty input,
.cell-weight input {
  width: 4.8em;
  min-width: 4.8em;
}
/* The flags are buttons now, not static chips. */
.mini-flag {
  font: inherit;
  font-size: 10px;
  cursor: pointer;
  line-height: 1.6;
  /* A pill is 999px-rounded, so the moment its label wraps to two lines it stops
     being a pill and becomes an ellipse. "Keep at bottom" is three words in a
     narrow column, so this is not a rare case. The label never wraps; the group
     wraps instead. */
  white-space: nowrap;
}
/* The chip is always as wide as its own bold state. Switching one on sets the
   label to 650 weight, which is wider than the resting text, so the chip used to
   grow a few pixels and shove its neighbours along the row -- the thing you just
   clicked moving out from under the pointer. This lays the bold text out with no
   height, so it decides the width in both states and nothing shifts. */
.mini-flag::after {
  content: attr(data-label);
  display: block;
  height: 0;
  overflow: hidden;
  visibility: hidden;
  font-weight: 650;
  pointer-events: none;
}
.mini-flag:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 1px;
}
.btn.icon-only {
  padding: 0 6px;
  min-width: 26px;
}

/* ── Validation ─────────────────────────────────────────────────────────────────
 * Also new: the surface being replaced put its validation in window.alert(), so
 * there was no field state to copy. Pitched to read as a correction rather than an
 * alarm -- one hairline, one line of text, no filled banner.
 */
.field-error {
  display: block;
  margin-top: 4px;
  font-size: 10px;
  line-height: 1.4;
  color: var(--danger-ink);
}
.field input[aria-invalid='true'],
.field select[aria-invalid='true'],
.item-table input[aria-invalid='true'] {
  border-color: var(--danger);
  background: var(--danger-soft);
}
.field input[aria-invalid='true']:focus-visible,
.item-table input[aria-invalid='true']:focus-visible {
  border-color: var(--danger);
  outline: 2px solid var(--danger-soft);
  outline-offset: 0;
}
.form-note {
  display: flex;
  gap: 7px;
  align-items: flex-start;
  margin-top: 12px;
  font-size: 11px;
  line-height: 1.45;
}
.form-note.error {
  color: var(--danger-ink);
}
.form-note .icon {
  flex: none;
  margin-top: 1px;
}

/* ── The sign-in sheet ──────────────────────────────────────────────────────────
 * A native <dialog>, so modality, the focus trap, Escape and ::backdrop come from
 * the browser rather than from script. Sized for one job: an address, a consent box
 * and a button.
 */
.sheet {
  width: min(420px, calc(100vw - 32px));
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow);
}
.sheet::backdrop {
  /* Deliberately not a token: ::backdrop is outside the document tree, so custom
     properties declared on :root do not reach it. */
  background: rgba(11, 18, 30, 0.55);
}
.sheet-header {
  padding: 18px 20px 0;
}
.sheet-header h2 {
  margin: 0;
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.sheet form {
  padding: 12px 20px 18px;
  display: grid;
  gap: 12px;
}
.sheet-blurb {
  margin: 0;
  color: var(--text-2);
  font-size: 12px;
  line-height: 1.5;
}
.sheet-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  margin-top: 2px;
}
.sheet-actions .link-button {
  margin-right: auto;
}
.consent {
  display: flex;
  gap: 9px;
  align-items: flex-start;
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-2);
  cursor: pointer;
}
.consent input {
  margin-top: 2px;
  flex: none;
}
.link-button {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-size: 11px;
  color: var(--primary);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.link-button:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}
/* The federated buttons, above the address field: one press against typing an
   address, waiting for mail and copying six digits back. */
.provider-list {
  display: grid;
  gap: 8px;
  justify-items: center;
}
/* Ours, next to Google's. Sized to match theirs -- 320px is what their widget
   renders at, and two sign-in buttons of different widths look like one of them is
   an afterthought. */
.provider-button {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 320px;
  max-width: 100%;
  height: 40px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  color: var(--text);
  font-weight: 600;
}
.provider-button:hover:not(:disabled) {
  background: var(--surface-2);
}
.provider-locked {
  text-align: center;
}
/* Google draws its own button into this, as an iframe. Centred rather than
   stretched: their widget takes a pixel width and ignores a percentage, so a
   full-bleed container would leave it sitting off to one side. */
.google-button {
  display: flex;
  justify-content: center;
  min-height: 40px;
}
/* "or", with a rule through it. The rules are drawn by the flex children rather
   than a background gradient, so they follow the theme's line token. */
.or-rule {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--muted);
  font-size: 11px;
}
.or-rule::before,
.or-rule::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}

/* The code field. Wide-spaced digits so a six-character code can be read back
   against the email it came from. */
.sheet .otp {
  font-size: 18px;
  letter-spacing: 0.32em;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* The two unit selects, in the container section's heading row. Small and quiet:
   they are set once and then read, not operated. */
.unit-picker {
  display: flex;
  gap: 6px;
  align-items: center;
}
.unit-picker select {
  height: 27px;
  border: 1px solid var(--line-strong);
  border-radius: 7px;
  background: var(--surface);
  color: var(--text);
  padding: 0 6px;
  font-size: 11px;
}
/* These are not in a `.field`, so the shared control hover in base.css never
   reached them and they sat inert while every other select in the rail responded. */
@media (hover: hover) {
  .unit-picker select:hover {
    border-color: var(--primary);
  }
}

@media (max-width: 1400px) {
  /* Matched to where the stacked layout takes over, at 980. At 1080 there was a
     hundred-pixel window -- and browser zoom lands you in it easily -- where the
     body was held wider than the window and the whole page scrolled sideways. */
  body { min-width: 980px; }
  .app-header { grid-template-columns: 235px 1fr 255px; }
  .nav-item { min-width: 96px; padding: 0 8px; }
  /* No rail width here. This block sits before `max-width:1480`, so anything that
     matches it matches that one too and loses -- the two rules that used to be on
     these lines never applied at any window size. The ladder lives in the blocks
     below, where it is reachable. */
  .result-strip { grid-template-columns: 135px 145px 135px 135px minmax(310px, 1fr); }
  .manual-lower { grid-template-columns: 1fr 350px; }
  /* The result drops below the item grid rather than sitting beside it. Sharing the
     row costs the table 300px, and around this width that is the difference between
     eight columns that fit and eight columns that scroll -- measured at 1300px, the
     table overflowed its wrapper by 86px. */
  .work-row { grid-template-columns: 1fr; }
}

@media (max-width:1480px){
  .app-header{grid-template-columns:224px 1fr 248px;padding:0 16px;}
  .nav-item{min-width:94px;padding:0 8px;font-size:13px;}
  .user-copy{min-width:98px;}
  .ai-grid,.manual-grid{grid-template-columns:356px 1fr;}

}

@media (max-width:1280px){
  :root{--header-h:62px;}
  .app-header{grid-template-columns:168px 1fr 62px;height:62px;padding:0 12px;}
  .page-shell{min-height:calc(100vh - 62px);}
  .brand{font-size:18px;letter-spacing:-.3px;}
  .brand-cube{margin-right:8px;}
  .brand-cube{width:28px;height:28px;border-radius:7px;}
  .nav-item{min-width:0;padding:0 7px;font-size:12px;gap:6px;}
  .nav-item .icon{width:15px;height:15px;}
  .header-actions{gap:8px;} .user-copy{display:none;}
  .icon-button{width:30px;height:30px;} .avatar{width:32px;height:32px;}
  .ai-grid,.manual-grid{grid-template-columns:336px 1fr;}

  .constraint-line{grid-template-columns:96px 1fr;}
}

@media (max-width:1180px){
  .work-row{grid-template-columns:1fr;}
  .summary-body{grid-template-columns:126px 1fr;}
}

@media (max-width:980px){
  body{min-width:360px;}
  /* minmax(0,1fr), not 1fr: a 1fr track has an automatic minimum of min-content,
     so one stubborn child drags the whole column wider than the viewport and the
     page scrolls sideways. The 3D viewer is exactly that child -- its min-height
     against its aspect-ratio implies a min-content width of over 400px. */
  .ai-grid,.manual-grid{grid-template-columns:minmax(0,1fr);}
  .left-stack,.manual-grid>.card.manual-panel{
    position: static;height:auto;max-height:none;overflow:visible;
    border-right: 0;border-bottom:1px solid var(--work-line);border-radius:0;}
  .left-stack{display:block;}
  .chat-log{max-height:320px;}
  .manual-grid>.card.manual-panel>.card-header{position:static;}
  .manual-grid>section,.ai-grid>section{height:auto;padding:12px;}
  .ai-grid>section .lower-stack,.manual-grid>section .lower-stack{height:auto;}
  .ai-grid>section .lower-stack>.card,
  .manual-grid>section .lower-stack>.card{height:auto;}
  .item-grid-wrap{max-height:440px;}
  .ai-grid>section .lower-stack .ctn-grid{max-height:none;overflow:visible;}
  .breadcrumbs{position:static;}
}

@media (max-width:768px){
  :root{--header-h:106px;}
  body{min-width:320px;}
  .app-header{grid-template-columns:1fr auto;height:auto;padding:8px 12px;row-gap:8px;}
  .brand{font-size:17px;}
  .main-nav{grid-column:1/-1;order:3;height:auto;justify-content:flex-start;
    overflow-x: auto;gap:4px;scrollbar-width:none;}
  .main-nav::-webkit-scrollbar{display:none;}
  .nav-item{min-width:0;height:36px;padding:0 10px;border-radius:8px;font-size:12px;white-space:nowrap;}
  .nav-item.active::after{display:none;}
  .header-actions{gap:6px;}
  .breadcrumbs{padding:0 12px;font-size:12px;}
  .workspace{padding:0;}

  /* width:100% makes the width definite, so the aspect-ratio only decides the
     height. Without it the desktop min-height stays in force, the ratio derives the
     *width* from that height (236 x 1.75 = 413px), and the viewer is quietly
     clipped by its card on a 390px screen. */
  .viewer-area{aspect-ratio:1.75;width:100%;min-height:200px;}
  .tool-rail,.viewer-help{display:none;}
  .viewer-card>.card-header{flex-wrap:wrap;row-gap:8px;}
  .viewer-toolbar-meta{flex-wrap:wrap;}
  /* Laid out across rather than down: on a canvas this short a stacked switch
     occludes a useful share of the load. Vertical anchoring comes from the base rule. */
  .view-switch{width:auto;display:flex;gap:4px;}
  .view-switch button{width:auto;padding:0 10px;}

  .summary-body{grid-template-columns:1fr;justify-items:center;text-align:center;}
  .summary-list{width:100%;}
  .ctn-grid,.detail-grid{grid-template-columns:1fr;}
  .form-grid.cols-4,.form-grid.cols-5{grid-template-columns:1fr 1fr;}
  .form-grid.cols-3,.form-grid.cols-2{grid-template-columns:1fr;}
  /* Length, width and height stay side by side even here: they are three short
     numbers read as one measurement, and stacking them turns one line into three. */
  .form-grid.cols-3.even{grid-template-columns:repeat(3,minmax(0,1fr));}
  .toggle-row{grid-template-columns:1fr;}
  .toggle-cell{border-right:0;border-bottom:1px solid var(--work-line);}
  .toggle-cell:last-child{border-bottom:0;}
  .constraint-line{grid-template-columns:1fr;gap:5px;}
  .form-actions{grid-template-columns:1fr;}
  .grid-toolbar{flex-wrap:wrap;}

  
  .item-grid-wrap{max-height:none;overflow:visible;}
  .item-table,.item-table tbody,.item-table tr,.item-table td{display:block;width:auto;}
  .item-table thead{display:none;}
  .item-table tr{border:1px solid var(--line);border-radius:10px;background:var(--surface);
    margin: 10px 12px;padding:10px 12px;}
  .item-table tr.detail{margin-top:-4px;border-top-left-radius:0;border-top-right-radius:0;}
  .item-table td{border:0;padding:3px 0;font-size:12px;}
  .item-table td.cell-num{display:none;}
  .item-table td.cell-name{font-size:13px;font-weight:700;color:var(--text);padding-bottom:7px;}
  .item-table td.expander{display:inline-block;float:right;padding:0;}
  .item-table td[data-l]{display:flex;justify-content:space-between;gap:12px;}
  .item-table td[data-l]::before{content:attr(data-l);color:var(--muted);font-weight:600;}
  /* Rows are cards here, not table rows, so nothing is held to a common height and
     the flags may wrap again -- three chips in a row do not fit beside their label
     on a phone, and forcing them would scroll the page sideways. */
  .mini-flags{flex-wrap:wrap;justify-content:flex-end;}
  .item-table td[data-l] .cell-inline{flex:1 1 auto;justify-content:flex-end;}
  .table-footer{flex-wrap:wrap;gap:6px 14px;}
}

/* ── The 3D canvas ──────────────────────────────────────────────────────────────
 * These rules exist to contain a reused renderer that predates this layout, and
 * they are appended last on purpose so nothing above can undo them.
 */

/* Three.js writes the canvas display size *inline* -- renderer.setSize(w, h, true)
 * -- seeded at init from window.innerWidth. An inline declaration outranks every
 * normal rule, so the canvas grows to the viewport, and because the renderer then
 * measures its own clientWidth to decide the next size, it stays there. Overriding a
 * library's inline style is the case !important is actually for. The render loop
 * re-measures every frame, so the drawing buffer follows the clamp immediately.
 */
#my-canvas {
  position: absolute;
  inset: 0;
  width: 100% !important;
  height: 100% !important;
  display: block;
}

/* ── The renderer's own control panel ──────────────────────────────────────────
 * dat.GUI, appended here by the renderer. It carries the loading-plan scrubber, the
 * horizontal/vertical/depth filters and the Drag Mode toggle -- and Drag Mode has no
 * other control anywhere, so hiding this panel makes drag-to-rearrange unreachable.
 * It is therefore themed rather than hidden.
 *
 * Restyling a third-party widget from the outside means matching its class names
 * (.dg, .cr, .property-name, .c, .slider) and beating its own injected stylesheet,
 * which is why the specificity here is higher than the rest of this file. The help
 * folder ships collapsed by the renderer, so the panel is only about six rows tall.
 */
/* Placement belongs to .filter-dock, which owns the disclosure; this rule owns only
   how the panel looks. It used to position itself, and when the dock became its
   containing block the `calc(100% - 24px)` cap started resolving against the 31px
   toggle button instead of the viewer -- a 7px panel that read as a black strip.
   Height is bounded by the dock now, so there is nothing here to get wrong. */
#visual-filter {
  width: 216px;
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: var(--shadow);
  background: var(--float-bg);
  backdrop-filter: blur(6px);
  /* Turning Drag Mode on makes the renderer expand its keyboard-shortcut folder,
     which adds ten rows and pushes the panel past the bottom of the viewer. Scroll
     inside the panel rather than letting it grow over the scene. */
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* dat.GUI writes its width inline, so this needs to outrank an inline declaration. */
#visual-filter .dg.main {
  width: 100% !important;
  margin: 0;
  text-shadow: none;
}

#visual-filter .dg,
#visual-filter .dg.main .close-button {
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 11px;
}

/* Its rows carry a coloured left border per control type; the hairline grid used
   everywhere else in this app reads better and is one less palette to maintain. */
#visual-filter .dg .cr {
  border-left: 0;
  border-bottom: 1px solid var(--line);
  background: transparent;
  line-height: 1.9;
}

#visual-filter .dg .cr:last-child {
  border-bottom: 0;
}

#visual-filter .dg .property-name {
  color: var(--text-2);
  font-weight: 600;
  width: 46%;
  overflow: hidden;
  text-overflow: ellipsis;
}

#visual-filter .dg .c {
  width: 54%;
}

#visual-filter .dg .c input[type='text'] {
  background: var(--surface-3);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 0 4px;
}

#visual-filter .dg .slider {
  background: var(--surface-3);
  border-radius: 4px;
}

#visual-filter .dg .slider-fg {
  background: var(--primary);
  border-radius: 4px;
}

#visual-filter .dg .cr.boolean input[type='checkbox'] {
  accent-color: var(--primary);
}

/* Folder titles ("Drag Placement Help") read as section headers. */
#visual-filter .dg li.title {
  background: var(--surface-3);
  color: var(--text);
  font-weight: 700;
  border-bottom: 1px solid var(--line);
}

/* The help folder's rows are static text that dat.GUI renders as string *inputs*,
   each labelled "Info". The label is noise and costs half the width, so the text
   gets the full row and stops being clipped mid-sentence. They still cannot wrap --
   an input is one line by definition -- so the type is stepped down to fit more. */
#visual-filter .dg .cr.string .property-name {
  display: none;
}

#visual-filter .dg .cr.string .c {
  width: 100%;
}

#visual-filter .dg .cr.string input[type='text'] {
  background: transparent;
  border: 0;
  color: var(--muted);
  width: 100%;
  font-size: 10px;
  text-overflow: ellipsis;
  /* Read, not edited. */
  cursor: default;
}

/* The renderer already hides the close button; this covers the reserved strip it
   otherwise leaves behind. */
#visual-filter .dg.main .close-button {
  display: none;
}

/* The panel sits where the mockup puts the tool rail, so on a narrow screen the two
   would stack on top of each other. The controls are pointer-driven and fiddly at
   that size anyway -- the pinch/drag gestures still work without them. */
@media (max-width: 768px) {
  #visual-filter {
    display: none;
  }
}

/* The cookie-preferences button, which is how anyone reopens the consent choice
   after the banner has been answered. The consent manager fixes it to the viewport
   at 60px square, ten pixels in -- which on the planner lands on the bottom-left
   corner of Reset, and Reset is a real button doing a real thing. Smaller and tucked
   fully into the corner, it clears the rail's action row at every viewport height
   the row is sticky at.

   Left alone on every other surface: this rule lives in app.css, which only the
   planner loads. */
#silktide-cookie-icon {
  width: 40px;
  height: 40px;
  left: 8px;
  bottom: 8px;
  opacity: .55;
}
#silktide-cookie-icon:hover,
#silktide-cookie-icon:focus-visible {
  opacity: 1;
}

/* ---------------------------------------------------------------------------
 * The upgrade sheet
 *
 * Same <dialog class="sheet"> chrome as sign-in, widened because it carries three
 * plans rather than one address field. The plan cards are styled here rather than
 * reused from site.css: that sheet is only loaded on the marketing shell, and
 * pulling it in for three cards would drag the whole landing page's cascade onto
 * the planner.
 * ------------------------------------------------------------------------- */
/* Unlayered, and it has to be. `.sheet` above is unlayered too, and an unlayered
   rule beats a layered one whatever the specificity -- so this sizing sat inside
   @layer bfb.component and lost to `.sheet { width: min(420px, ...) }` every time.
   Three plan cards were then laid out in a 420px box and the third was simply cut
   off at the edge of the dialog, which is exactly what it looked like. */
.upgrade-sheet {
  width: min(1000px, calc(100vw - 32px));
  max-height: min(92vh, 900px);
  overflow-y: auto;
}

@layer bfb.component {
  .upgrade-plans {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 12px;
    padding: 16px 20px 4px;
  }

  .upgrade-plans .plan {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin: 0;
    padding: 16px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--surface-2);
  }

  /* One accent, on one card. The badge and the filled button say the same thing
     twice, which is what a recommendation is. */
  .upgrade-plans .plan.featured {
    border-color: var(--primary);
  }

  .upgrade-plans .plan-badge {
    position: absolute;
    top: -9px;
    left: 16px;
    padding: 1px 8px;
    border-radius: 999px;
    background: var(--primary);
    color: var(--on-accent);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.02em;
  }

  .upgrade-plans .plan-name {
    font-size: 13px;
    font-weight: 600;
    color: var(--muted);
  }

  .upgrade-plans .plan-price {
    font-size: 26px;
    font-weight: 700;
    line-height: 1.1;
    color: var(--text);
  }

  .upgrade-plans .plan-cadence {
    margin-left: 3px;
    font-size: 13px;
    font-weight: 500;
    color: var(--muted);
  }

  .upgrade-plans .plan-blurb {
    margin: 0;
    font-size: 12px;
    line-height: 1.45;
    color: var(--muted);
  }

  .upgrade-plans .checks {
    flex: 1;
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 5px;
  }

  .upgrade-plans .checks li {
    position: relative;
    padding-left: 16px;
    font-size: 12px;
    line-height: 1.4;
    color: var(--text-2);
  }

  .upgrade-plans .checks li::before {
    content: "";
    position: absolute;
    left: 2px;
    top: 6px;
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--primary-soft);
  }

  .sheet-footnote {
    margin: 0;
    padding: 0 20px 18px;
    font-size: 11px;
    color: var(--muted-faint);
  }
}

/* One column once three cards stop fitting side by side. The sheet is the only
   place in the planner that has to work at this width -- the rest of the app pins
   min-width: 1200px -- because it can be reached from the marketing pages. */
/* The blurb is a direct child of the dialog here, not of the padded form the sign-in
   sheet wraps everything in -- so it sat flush against the left edge while the heading
   above it was indented, and the two read as belonging to different boxes. */
.upgrade-sheet .sheet-blurb {
  padding: 6px 20px 0;
  font-size: 13px;
}

/* One row of cards, one row of buttons. The three plans have different numbers of
   features, so each button landed wherever its own list happened to end and the eye
   had nothing to compare across. `margin-top: auto` inside a stretched grid cell puts
   every one of them on the same line. */
.upgrade-plans .plan > .btn {
  margin-top: auto;
}

/* Under the dismissal rather than tucked in the far corner away from it: they are one
   thought, and the sheet is a metre wide now. */
.upgrade-sheet .sheet-footnote {
  text-align: center;
  padding-top: 10px;
}

/* Dismissal, centred and in its own colour.
   Unlayered, like the sizing above and for the same reason: `.sheet-actions` and the
   `.btn` variants are unlayered too, and a layered rule here would lose to both.

   Red at rest rather than only on hover, which is against the house style for
   destructive controls -- but this is not a destructive control. It is the way past a
   dialog that stands in front of the thing somebody came to do, and it has to be
   findable at a glance or the offer becomes an obstacle. Centred under the plans for
   the same reason: tucked in the corner it read as an afterthought. */
.upgrade-sheet .sheet-actions {
  justify-content: center;
  padding: 6px 20px 0;
}

.upgrade-sheet .upgrade-dismiss,
.upgrade-sheet .upgrade-dismiss:focus-visible {
  color: var(--danger-ink);
  border-color: var(--danger);
  background: var(--danger-soft);
}

/* Filled at rest, not only under the pointer. An outline reads as the quieter of the
   buttons on the sheet, and this one is not quiet -- it is the way out of a dialog
   standing in front of the thing somebody came to do, and it has to be seen without
   being hunted for. Hover then goes the whole way to solid. */
.upgrade-sheet .upgrade-dismiss:hover {
  background: var(--danger);
  border-color: var(--danger);
  color: var(--on-accent);
}

/* Two across before one across: at tablet widths three cards are each too narrow to
   read a feature list in, but two are fine and the third simply sits below. */
@media (max-width: 1040px) {
  @layer bfb.component {
    .upgrade-plans {
      grid-template-columns: repeat(2, 1fr);
    }
  }
}

@media (max-width: 760px) {
  @layer bfb.component {
    .upgrade-plans {
      grid-template-columns: 1fr;
    }
  }

  /* Unlayered for the same reason as the rule above. */
  .upgrade-sheet {
    width: calc(100vw - 24px);
    max-height: 88vh;
  }
}

/* ---------------------------------------------------------------------------
 * Print
 *
 * The other half of export: a sheet somebody can put on a clipboard, and the
 * route to a PDF on every platform without this server rendering one. The
 * browser's own print-to-PDF is better than anything a headless renderer would
 * produce here, and it is already installed.
 *
 * What survives: the viewer (the picture is the point), the result figures, and
 * the item list. What goes: the header, the tool rail, the chat, every control,
 * and the consent and support widgets, which are the two things guaranteed to
 * be in the way.
 * ------------------------------------------------------------------------- */
@media print {
  /* Not layered. Print rules have to beat the component layer without an arms
     race in specificity, and unlayered author rules already do. */
  header.site-header,
  nav,
  .tool-rail,
  .float-panel,
  .view-toggles,
  .container-nav,
  bfb-chat,
  bfb-auth,
  bfb-upgrade,
  .result-actions,
  .manual-grid button,
  #silktide-wrapper,
  #silktide-cookie-icon,
  iframe[title*="chat" i],
  .tawk-min-container,
  #tawkchat-container {
    display: none !important;
  }

  :root {
    /* Ink on paper. The dark theme costs a cartridge and reads worse. */
    color-scheme: light;
  }

  body {
    /* The app pins 1200px so its two-pane layout holds together; paper is
       narrower than that and would otherwise clip at the right margin. */
    min-width: 0 !important;
    background: #fff !important;
    color: #000 !important;
  }

  main,
  .app-grid,
  .lower-stack {
    display: block !important;
    width: auto !important;
    max-width: none !important;
    padding: 0 !important;
    margin: 0 !important;
  }

  .card {
    border: 1px solid #ccc !important;
    box-shadow: none !important;
    background: #fff !important;
    break-inside: avoid;
    margin-bottom: 10px;
  }

  /* The canvas is a live WebGL surface; at print time it is whatever was last
     drawn, so it needs a size that fits the page rather than the viewport. */
  bfb-viewer,
  bfb-viewer canvas {
    width: 100% !important;
    height: auto !important;
    max-height: 380px !important;
    break-inside: avoid;
  }

  /* Rows split across a page break are the specific thing that makes a printed
     packing list unusable at the receiving end. */
  .manual-grid tr,
  .summary-line,
  .leftover-note {
    break-inside: avoid;
  }

  a[href]::after {
    /* Printed links are dead ends unless the address comes with them. */
    content: " (" attr(href) ")";
    font-size: 10px;
    color: #555;
  }

  /* ...except the ones that are navigation, which are gone anyway, and the
     brand, whose address is the site itself. */
  nav a[href]::after,
  .brand a[href]::after {
    content: "";
  }
}
