/**
 * Jet black, amber accent. Values taken from the Paper file, not eyeballed.
 *
 * The greys are deliberately close together and deliberately distinct: the
 * page, the rail and a panel sitting on the rail are three different blacks,
 * and collapsing them into one flattens the whole interface. Likewise the
 * three hairlines — a rail edge, a panel edge and a control edge are not the
 * same line.
 *
 * Amber is for state, not decoration: the live value, the active element, the
 * one button that starts work. The primary action is white, because on a
 * near-black surface white outranks any hue.
 */
:root {
  --bg: #0a0a0b;
  --rail: #0d0d0e;
  --bar: #0b0b0c;
  --panel: #141416;
  --panel-2: #1c1c1f;
  --line: #1c1c1f;
  --line-soft: #141416;
  --line-panel: #1f1f22;
  --line-strong: #2a2a2e;
  --segment: #151517;
  --segment-on: #2c2c30;
  --ink: #f5f5f7;
  --ink-2: #c7c7cc;
  --muted: #8a8a91;
  --muted-2: #6e6e76;
  --muted-3: #5a5a61;
  --tick: #3a3a40;
  --accent: #ff9f0a;
  --accent-soft: rgba(255, 159, 10, 0.14);
  --ok: #30d158;
  --mono: 'JetBrains Mono', ui-monospace, monospace;
  --display: 'Inter', system-ui, sans-serif;
}

/* Anything set in mono here is a number that changes, so tabular figures are
   the default rather than something each readout has to remember. */
.credit-count,
.group-value,
.cutstop-sub,
.el-mm,
.ct-dims,
.ct-zoom,
.status,
.fmt-cost strong {
  font-variant-numeric: tabular-nums;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* Form controls do not inherit the page font by default — they fall back to
   the UA's own (Arial 13.33px here). Without this, every button, input and
   select silently opts out of the type system. */
button,
input,
select,
textarea {
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
}

html,
body,
#app {
  height: 100%;
}

/* Inter for everything the user reads, mono only for numbers that change.
   The old sheet set mono on the body, which put an operator's console face on
   labels and prose that are not data — legible, but it made the whole app
   read as a terminal rather than a tool. */
body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--display);
  font-size: 13px;
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow: hidden;
}

#app {
  display: grid;
  grid-template:
    'topbar topbar' 52px
    'rail stage' 1fr
    'status status' 34px
    / 296px 1fr;
}

/* ---------- top bar ---------- */
/* Flat black. The old bar carried a diagonal candy stripe behind the brand;
   at 52px it fought the wordmark and dated the whole app. */
.topbar {
  grid-area: topbar;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 16px 0 20px;
  border-bottom: 1px solid var(--line);
  background: var(--bg);
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
}

.brand-mark {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  color: var(--ink);
}

/* The rings read as the quieter part of the mark, so they sit a step down
   from the blades rather than matching them. */
.brand-mark circle {
  stroke: var(--muted);
}

.brand h1 {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
}

/* The tagline was a third piece of text competing with the wordmark and the
   controls for a 52px bar. The product name is enough. */
.tagline {
  display: none;
}

.top-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Says the cut no longer matches the file as opened. Ghost-weight on
   purpose: it is a note about state, not a control. */
.edited-tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 30px;
  padding-right: 4px;
  font-size: 12px;
  color: var(--muted);
}

.edited-tag[hidden] { display: none; }

.edited-tag i {
  width: 5px;
  height: 5px;
  border-radius: 3px;
  background: var(--accent);
  flex-shrink: 0;
}

#btn-export-top[hidden] { display: none; }

/* Divides state (what the file is) from actions (what you can do to it). */
.topbar-sep {
  width: 1px;
  height: 20px;
  background: var(--line-strong);
  flex-shrink: 0;
}

/* ---------- buttons ----------
   Three variants, and the hierarchy is deliberate: on a near-black surface
   white outranks any hue, so the primary action is light and amber is kept
   for state. `solid` is the light fill, `ghost` the outline, and neither
   changes hue on hover — only lifts, which reads as a surface responding
   rather than a colour event. */
/* `hidden` must win over every display this file sets.
   [hidden] is only display:none in the UA sheet, so any class that sets its
   own display silently outranks it and the element stays on screen with the
   attribute still set — which is how a "Sign out" button appeared to someone
   who was not signed in, and how the top Export button would have appeared
   before an image was open. Scoped to the whole document so the next
   component to set display does not reintroduce it. */
[hidden] {
  display: none !important;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  height: 30px;
  padding: 0 12px;
  border-radius: 7px;
  font-family: var(--display);
  font-size: 13px;
  font-weight: 500;
  color: var(--ink-2);
  background: transparent;
  border: 1px solid var(--line-strong);
  cursor: pointer;
  white-space: nowrap;
  transition: background 120ms, border-color 120ms, color 120ms, opacity 120ms;
  user-select: none;
}

.btn:hover:not(:disabled) {
  border-color: #3a3a40;
  color: var(--ink);
}

.btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

.btn.solid {
  background: var(--ink);
  border-color: transparent;
  color: var(--bg);
  font-weight: 600;
  padding: 0 13px;
}

.btn.solid:hover:not(:disabled) {
  background: #fff;
  color: var(--bg);
}

/* The amber fill is reserved for the one button that starts paid work. */
.btn.accent {
  background: var(--accent);
  border-color: transparent;
  color: var(--bg);
  font-weight: 600;
  padding: 0 13px;
}

.btn.accent:hover:not(:disabled) {
  background: #ffb02e;
  color: var(--bg);
}

.btn.solid input[type='file'] {
  display: none;
}

.btn.ghost {
  background: transparent;
}

.btn.sm {
  height: 24px;
  padding: 0 8px;
  font-size: 11px;
  border-radius: 6px;
}

/* ---------- rail ---------- */
.rail {
  grid-area: rail;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--line);
  background: var(--rail);
  overflow-y: auto;
  padding: 0 0 8px;
}

/* Sections are separated by space, not by rules. Hairlines between every
   group turned the rail into a stack of boxes; the design uses one rule, at
   the export block, where the rail genuinely changes purpose. */
.group {
  padding: 18px 16px 22px;
  border-bottom: none;
}

.group h2 {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
  margin-bottom: 14px;
}

/* An accordion header, not a button that looks like one: the heading keeps
   its own type and colour, and the trigger is the full-width hit area around
   it so the whole row is clickable rather than just eleven pixels of text. */
.group h2:has(.group-toggle) {
  margin-bottom: 0;
}

.group-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  padding: 0 0 14px;
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  text-align: left;
  cursor: pointer;
}

.group-toggle:hover {
  color: var(--ink);
}

.group-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

.group-chev {
  flex: none;
  width: 14px;
  height: 14px;
  fill: currentColor;
  opacity: 0.65;
  transition: transform 160ms ease;
}

.group-toggle[aria-expanded='false'] .group-chev {
  transform: rotate(-90deg);
}

/* Collapsed sections keep their bottom padding out of the stack too, or a
   rail of closed headings sits in a column of dead space. */
.group-toggle[aria-expanded='false'] {
  padding-bottom: 0;
}

.group:has(.group-toggle[aria-expanded='false']) {
  padding-bottom: 14px;
}

.group-body {
  overflow: hidden;
}

.group-body[hidden] {
  display: none;
}

.row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 12px;
}

.row:last-child {
  margin-bottom: 0;
}

.row label {
  color: var(--muted);
}

/* A readout row is a label and a value like any other, so its label is muted
   to match. It rendered at full brightness because a bare <span> inherited
   the body colour while sibling rows used <label>. */
.row.readout > span:first-child {
  color: var(--muted);
}

/* A slider is two lines tall, so it needs more air beneath it than a
   single-line row — otherwise its handle reads as belonging to the label
   underneath. */
.row.slider {
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
  margin-bottom: 16px;
}

.row.slider label {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
}

/* Every changing number is mono and tabular. Proportional digits shift width
   as the value changes, so a slider readout wobbles while you drag it —
   tabular figures hold the column still. The design sets all readouts this
   way, and it is the reason mono is in the type system at all. */
.row.slider output,
.row.readout output {
  font-family: var(--mono);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* Aligned to the top rather than centred: a label that wraps to two lines
   should start level with its box, not float half a line below it. */
.row.check {
  justify-content: flex-start;
  align-items: flex-start;
  gap: 10px;
}

.row.check label {
  color: var(--ink);
  cursor: pointer;
  line-height: 16px;
  padding-top: 0;
}

/* Explanatory text is a step quieter than the labels it explains, and never
   competes with a value. */
.hint {
  color: var(--muted-2);
  font-size: 11px;
  line-height: 1.5;
  margin-top: 10px;
}

/* Advanced stacks five sections; a hairline between them stops the rail
   reading as one undifferentiated column of controls without boxing every
   group in its own card. */
body:not(.mode-simple) .rail > .group + .group {
  border-top: 1px solid var(--line-soft);
}

/* Fields share the control geometry used everywhere else — 30px tall, 7px
   radius — so a text field and a button read as the same family rather than
   as a styled app containing some raw HTML. */
input[type='number'],
input[type='text'],
select {
  height: 30px;
  font-family: var(--mono);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  background: var(--panel);
  border: 1px solid var(--line-panel);
  border-radius: 7px;
  padding: 0 10px;
  width: 150px;
  transition: border-color 120ms, background 120ms;
}

select {
  cursor: pointer;
  /* The UA arrow is drawn in the platform's own colour and sits badly on a
     dark field, so it is replaced with one that matches the icon set. */
  appearance: none;
  -webkit-appearance: none;
  padding-right: 28px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6' fill='none' stroke='%238a8a91' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 8px center;
  background-size: 14px 14px;
}

input[type='number'] {
  width: 80px;
}

input[type='number']:hover,
input[type='text']:hover,
select:hover {
  border-color: var(--line-strong);
}

input[type='number']:focus,
input[type='text']:focus,
select:focus {
  outline: none;
  border-color: var(--accent);
}

/* The track is 4px but the control reserves 16px so the thumb has room to
   sit inside its own row. At 3px tall the thumb overhung both edges and
   collided with the label beneath it. */
input[type='range'] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 16px;
  background: transparent;
  cursor: pointer;
}

input[type='range']::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: var(--line-strong);
}

input[type='range']::-moz-range-track {
  height: 4px;
  border-radius: 2px;
  background: var(--line-strong);
}

/* A neutral knob, not an amber one. Amber marks state in this interface —
   the live value, the active element — and a permanently amber handle on
   every slider spends that meaning on furniture. */
input[type='range']::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 14px;
  height: 14px;
  margin-top: -5px;
  background: var(--ink);
  border: 1px solid rgba(0, 0, 0, 0.25);
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}

input[type='range']::-moz-range-thumb {
  width: 14px;
  height: 14px;
  background: var(--ink);
  border: 1px solid rgba(0, 0, 0, 0.25);
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}

input[type='range']:focus-visible::-webkit-slider-thumb {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Drawn rather than accent-coloured: `accent-color` still renders the
   platform checkbox, which on a near-black panel is a bright filled square
   that does not belong to this interface. */
input[type='checkbox']:not(.switch) {
  appearance: none;
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  border: 1px solid var(--line-strong);
  border-radius: 5px;
  background: var(--panel);
  cursor: pointer;
  position: relative;
  transition: background 120ms, border-color 120ms;
}

input[type='checkbox']:not(.switch):hover { border-color: #3a3a40; }

input[type='checkbox']:not(.switch):checked {
  background: var(--accent);
  border-color: var(--accent);
}

/* The tick is drawn with two borders rotated into an L — no glyph, so it
   cannot be affected by the font stack or fail to load. */
input[type='checkbox']:not(.switch):checked::after {
  content: '';
  position: absolute;
  left: 5px;
  top: 1.5px;
  width: 4px;
  height: 8px;
  border: solid var(--bg);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

input[type='checkbox']:not(.switch):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.shape-picker {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
}

.shape {
  background: var(--bg);
  border: 1px solid var(--line);
  color: var(--muted);
  padding: 8px 0;
  cursor: pointer;
  display: grid;
  place-items: center;
  transition: border-color 120ms, color 120ms, background 120ms;
}

.shape svg {
  width: 26px;
  height: 26px;
}

.shape:hover {
  color: var(--ink);
  border-color: var(--muted);
}

.shape.active {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-soft);
}

.export-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-top: 14px;
}

.btn.export {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 9px 12px;
  font-weight: 700;
}

.btn.export span {
  font-weight: 400;
  font-size: 10px;
  color: var(--muted);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.btn.export:hover:not(:disabled) span {
  color: var(--accent);
}

.export-grid[hidden] { display: none; }

/* ---------- pinned export block ----------
   Pushes the export to the bottom of the rail however short the controls
   above it are, so its position never moves between Simple and Advanced. */
.rail-spacer {
  flex: 1 1 0;
  min-height: 0;
}

.export-block {
  display: flex;
  flex-direction: column;
  gap: 10px;
  flex-shrink: 0;
  padding: 18px 16px;
  border-top: 1px solid var(--line);
  background: var(--bar);
}

/* White, not amber: on a near-black rail a light fill is the loudest thing
   available, and this is the only button that spends a credit. */
.btn-export {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  height: 48px;
  width: 100%;
  border: 0;
  border-radius: 11px;
  background: var(--ink);
  color: var(--bg);
  font-family: var(--display);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -0.01em;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
  transition: background 120ms, opacity 120ms;
}

.btn-export:hover:not(:disabled) { background: #fff; }

.btn-export:disabled {
  opacity: 0.3;
  cursor: not-allowed;
}

.btn-export svg {
  width: 17px;
  height: 17px;
  flex-shrink: 0;
}

.export-caption {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-size: 11px;
  color: var(--muted-2);
}

.export-dot {
  width: 2px;
  height: 2px;
  border-radius: 1px;
  background: var(--tick);
  flex-shrink: 0;
}

/* ---------- stage ---------- */
.stage {
  grid-area: stage;
  position: relative;
  overflow: hidden;
  /* dark checkerboard: reads transparency without glare */
  --check: #1a1a1e;
  background-image:
    linear-gradient(45deg, var(--check) 25%, transparent 25%),
    linear-gradient(-45deg, var(--check) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--check) 75%),
    linear-gradient(-45deg, transparent 75%, var(--check) 75%);
  background-size: 24px 24px;
  background-position: 0 0, 0 12px, 12px -12px, -12px 0;
  background-color: #121215;
}

/* ---------- canvas toolbar ---------- */
.canvas-toolbar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 4;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 18px;
  border-bottom: 1px solid var(--line-soft);
  background: var(--bg);
}

.ct-file {
  display: flex;
  align-items: baseline;
  gap: 10px;
  min-width: 0;
}

#ct-name {
  font-size: 13px;
  font-weight: 500;
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ct-dims {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted-3);
  flex-shrink: 0;
}

.ct-zoom {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  height: 24px;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  color: var(--ink-2);
}

#preview {
  position: absolute;
  /* Below the toolbar, not under it: the artwork must never be clipped by
     chrome that is painted on top of it. */
  inset: 44px 0 0 0;
  width: 100%;
  height: 100%;
  cursor: grab;
  touch-action: none;
}

#preview.panning {
  cursor: grabbing;
}

#cut-path {
  stroke: var(--accent);
  stroke-width: 1.6;
  stroke-dasharray: 7 5;
  vector-effect: non-scaling-stroke;
  animation: ants 0.9s linear infinite;
}

#cut-shadow {
  stroke: rgba(236, 0, 140, 0.28);
  stroke-width: 5;
  vector-effect: non-scaling-stroke;
}

@keyframes ants {
  to {
    stroke-dashoffset: -12;
  }
}

.dropzone {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  text-align: center;
  color: var(--muted);
  /* The overlay itself stays transparent to the pointer so drags reach the
     stage underneath; only the button inside it takes clicks. */
  pointer-events: none;
  z-index: 2;
}

/* Two ways to start, side by side and weighted equally: drag, or click. */
.drop-row {
  display: flex;
  align-items: stretch;
  gap: 20px;
}

.drop-half {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 190px;
  padding: 22px 16px;
  border: 1px dashed var(--line-strong);
  border-radius: 14px;
}

.drop-or {
  align-self: center;
  font-size: 11px;
  color: var(--muted-3);
}

/* Square, icon-led, and a real button. */
.drop-open {
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 190px;
  padding: 22px 16px;
  border: 1px solid var(--line-strong);
  border-radius: 14px;
  background: var(--panel);
  color: var(--ink-2);
  font-family: var(--display);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  transition: background 140ms, border-color 140ms, color 140ms;
}

.drop-open:hover {
  background: var(--panel-2);
  border-color: #3a3a40;
  color: var(--ink);
}

.drop-open:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.drop-open svg {
  width: 34px;
  height: 34px;
  color: var(--accent);
  fill: currentColor;
}

.dropzone .drop-ico {
  width: 44px;
  height: 44px;
  color: var(--accent);
  fill: currentColor;
  margin-bottom: 6px;
}

.dropzone strong {
  color: var(--ink);
  font-family: var(--display);
  font-size: 16px;
  font-weight: 700;
}

.dropzone .sub {
  max-width: 380px;
  font-size: 11px;
  line-height: 1.6;
}

.dropzone.hidden {
  display: none;
}

#app.dragover .stage {
  outline: 2px dashed var(--accent);
  outline-offset: -8px;
}

/* One stepper, not three loose buttons: zoom out, zoom in and fit are a
   single control group, and grouping them into one bordered box stops them
   reading as three unrelated floating chips over the artwork. */
.stage-tools {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--line-panel);
  border-radius: 8px;
  background: var(--panel);
}

.stage-tools .btn {
  height: 24px;
  min-width: 26px;
  padding: 0 6px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--muted);
  font-size: 13px;
}

.stage-tools .btn:hover:not(:disabled) {
  background: var(--panel-2);
  color: var(--ink);
}

#btn-fit {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
}

/* ---------- status bar ---------- */
.status {
  grid-area: status;
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 0 18px;
  border-top: 1px solid var(--line-soft);
  background: var(--bar);
  color: var(--muted-2);
  font-family: var(--mono);
  font-size: 11px;
  white-space: nowrap;
  overflow: hidden;
}

/* The filename is the one thing here that is not a measurement, so it is the
   one thing not set in mono. Targeted by id rather than :first-child, which
   would follow whatever happens to lead the bar. */
.status #st-file {
  font-family: var(--display);
  font-weight: 500;
  color: var(--ink-2);
}

/* Says the geometry is computed and exportable — the answer to "is it done
   yet" without the user having to infer it from the numbers next to it. */
.st-ready {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-family: var(--display);
  font-weight: 500;
  color: var(--ink-2);
}

.st-ready[hidden] { display: none; }

.st-ready i {
  width: 6px;
  height: 6px;
  border-radius: 3px;
  background: var(--ok);
  flex-shrink: 0;
}

.status #st-zoom {
  margin-left: auto;
}

/* ---------- toasts ---------- */

.toasts {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: flex-end;
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: 420px;
  padding: 10px 12px;
  font-size: 13px;
  line-height: 1.45;
  color: var(--text);
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-left: 3px solid var(--muted);
  border-radius: 8px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  animation: toast-in 160ms ease-out;
}

.toast.leaving {
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 160ms ease-in, transform 160ms ease-in;
}

@keyframes toast-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

.toast-icon {
  flex: 0 0 18px;
  width: 18px;
  height: 18px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  font-size: 11px;
  font-weight: 700;
  background: rgba(255, 255, 255, 0.08);
}

.toast-text { flex: 1 1 auto; }

.toast-count {
  flex: 0 0 auto;
  font-size: 11px;
  color: var(--muted);
}

.toast-action {
  flex: 0 0 auto;
  font: inherit;
  font-size: 12px;
  color: var(--accent);
  background: none;
  border: 0;
  padding: 2px 4px;
  cursor: pointer;
  text-decoration: underline;
}

.toast-close {
  flex: 0 0 auto;
  font: inherit;
  font-size: 15px;
  line-height: 1;
  color: var(--muted);
  background: none;
  border: 0;
  padding: 0 2px;
  cursor: pointer;
}

.toast-close:hover { color: var(--text); }

.toast-success { border-left-color: #2ecc71; }
.toast-success .toast-icon { color: #2ecc71; }
.toast-error { border-left-color: var(--accent); }
.toast-error .toast-icon { color: var(--accent); }
.toast-info { border-left-color: #4aa3ff; }
.toast-info .toast-icon { color: #4aa3ff; }


/* ---------- region fix ---------- */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 10px;
}

.chip {
  font-family: var(--mono);
  font-size: 11px;
  padding: 4px 9px;
  background: var(--bg);
  border: 1px solid var(--line);
  color: var(--muted);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.chip.active {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--accent-soft);
}

.chip .x {
  opacity: 0.6;
}

.chip .x:hover {
  opacity: 1;
  color: #ff4d4d;
}

#preview.marquee-mode {
  cursor: crosshair;
}

#marquee {
  stroke: var(--accent);
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
  vector-effect: non-scaling-stroke;
}

.region-rect {
  fill: none;
  stroke: rgba(236, 0, 140, 0.45);
  stroke-width: 1;
  stroke-dasharray: 5 4;
  vector-effect: non-scaling-stroke;
}

.region-rect.active {
  stroke: var(--accent);
  stroke-width: 1.6;
}

#cut-v1 {
  stroke: #19d3f0;
  stroke-width: 1.4;
  stroke-dasharray: 3 5;
  vector-effect: non-scaling-stroke;
}

#btn-region.armed {
  border-color: var(--accent);
  color: var(--accent);
}

/* ---------------- simple / advanced ---------------- */

/* A segmented control, not folder tabs. Simple and Advanced are two views of
   one state, which a segment says and a tab stack does not. */
.mode-tabs {
  display: flex;
  gap: 0;
  margin: 14px 16px 4px;
  padding: 2px;
  background: var(--segment);
  border: 1px solid var(--line-panel);
  border-radius: 9px;
}

.mode-tab {
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 28px;
  font-family: var(--display);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 7px;
  padding: 0;
  cursor: pointer;
  transition: background 140ms, color 140ms;
}

.mode-tab:hover:not(.active) {
  color: var(--ink-2);
}

.mode-tab.active {
  color: var(--ink);
  font-weight: 600;
  background: var(--segment-on);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
}

/* Advanced controls are hidden, not duplicated, so both modes read the same
   state and cannot disagree about what the cut currently is. */
body.mode-simple .rail > .group:not(#panel-simple):not(.always-on) {
  display: none;
}

body:not(.mode-simple) #panel-simple {
  display: none;
}

/* ---------- cut style slider ----------
   The track, ticks and knob are drawn by us; the range input sits on top of
   them, transparent, and does all the input handling. That keeps native
   keyboard and assistive-tech behaviour while letting the knob look like the
   design instead of like the browser's default. */
.group-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 14px;
}

.group-head h2 {
  margin-bottom: 0;
}

.group-value {
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 500;
  color: var(--accent);
}

/* Just a container now. Base UI's Root/Control/Track own the geometry, so
   this must not impose a height or a flex context that collapses them.
   The inset is this file's only job: Base UI centres the thumb ON the track
   ends, so at the first and last stop its outer half would hang over the
   panel gutter. Insetting by half a thumb keeps both ends inside the rail. */
.cutslider {
  display: block;
  width: 100%;
  padding: 0 8px;
}

/* Geometry note, because this is easy to get subtly wrong:
   a range input centres its thumb at (thumbW / 2) from each end, so the thumb
   travels over an area inset by half a thumb at both sides — NOT the full
   track. Ticks laid out across the whole track therefore drift away from the
   stops they mark, worst at the two ends. Both are inset by the same
   --knob half-width here, so tick and thumb share one travel range. */
/* ---------- Base UI slider ----------
   Every part here is a real element the library positions, so there is no
   guessing at vendor pseudo-element box models. The thumb centres itself on
   the track because both are laid out by the same flow — the alignment bugs
   this replaced were all artefacts of drawing artwork under a native input. */
/* Root renders a bare <div> with no styling of its own, so it needs to be
   told to fill its container — otherwise the whole subtree collapses to 0x0
   and nothing below it has anything to lay out against. */
.cutslider > [role='group'],
.bui-slider-host > [role='group'] {
  display: block;
  width: 100%;
}

/* Holds a mounted slider where its hidden <input type="range"> used to sit. */
.bui-slider-host {
  display: block;
  width: 100%;
}

/* The input stays in the DOM as the value model but is never shown. */
.row.slider input[type='range'][hidden] {
  display: none;
}

.bui-slider-control {
  display: flex;
  align-items: center;
  width: 100%;
  height: 20px;
  cursor: pointer;
  touch-action: none;
}

.bui-slider-track {
  position: relative;
  width: 100%;
  height: 4px;
  border-radius: 2px;
  background: var(--line-strong);
}

/* The filled portion, from the first stop to the thumb. The hand-rolled
   version had no fill at all, so the slider never showed how far along the
   range the current setting sat. */
.bui-slider-indicator {
  position: absolute;
  top: 0;
  height: 100%;
  border-radius: 2px;
  background: var(--muted-2);
}

.bui-slider-ticks {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.bui-slider-tick {
  position: absolute;
  top: 50%;
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  border-radius: 3px;
  background: var(--tick);
  transition: background 140ms;
}

.bui-slider-tick.is-passed {
  background: var(--muted-2);
}

/* Base UI positions the thumb; translate(-50%, -50%) against top:50% is the
   only centring this needs, and it works identically in every engine. */
.bui-slider-thumb {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--ink);
  border: 1px solid rgba(0, 0, 0, 0.25);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
  outline: none;
  transition: transform 120ms;
}

.bui-slider-thumb:hover { transform: scale(1.08); }

.bui-slider-thumb:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ---------- Base UI button ----------
   Taller and softer than the topbar `.btn`: a dialog's primary action is the
   thing the whole sheet exists to get pressed, so it carries more weight than
   a control in a chrome bar. */
.bui-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  height: 38px;
  padding: 0 16px;
  border: 1px solid transparent;
  border-radius: 9px;
  font-family: var(--display);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: -0.005em;
  cursor: pointer;
  white-space: nowrap;
  user-select: none;
  transition: background 130ms ease, border-color 130ms ease,
    transform 130ms ease, opacity 130ms ease;
}

/* A press that moves is the cheapest way to make a button feel real. */
.bui-btn:active:not([data-disabled]) { transform: scale(0.98); }

.bui-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* data-disabled rather than :disabled — Base UI keeps the button focusable
   while disabled (focusableWhenDisabled), so the native pseudo-class is not
   the signal to style against. */
.bui-btn[data-disabled] {
  cursor: not-allowed;
  opacity: 0.55;
}

.bui-btn-primary {
  background: var(--ink);
  color: var(--bg);
}

.bui-btn-primary:hover:not([data-disabled]) { background: #fff; }

.bui-btn-ghost {
  background: transparent;
  border-color: var(--line-strong);
  color: var(--ink-2);
  font-weight: 500;
}

.bui-btn-ghost:hover:not([data-disabled]) {
  border-color: #3a3a40;
  color: var(--ink);
}

.bui-btn-icon {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

/* A busy button keeps its width so the dialog does not reflow under the
   cursor when the label changes to "Opening Google…". */
.bui-btn[aria-busy='true'] .bui-btn-label {
  min-width: 8.5em;
}

/* ---------- Base UI switch ----------
   A preference that takes effect later, which a switch says and a checkbox
   does not. The thumb is the library's own element, so the generic checkbox
   styling can no longer reach in and overwrite its geometry — which is
   exactly what once shrank it to a 4x8px sliver of a checkmark. */
.bui-switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  width: 42px;
  height: 25px;
  flex-shrink: 0;
  padding: 0;
  border: 0;
  border-radius: 13px;
  background: #3a3a40;
  cursor: pointer;
  transition: background 160ms;
}

.bui-switch[data-checked] {
  background: var(--accent);
}

.bui-switch:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.bui-switch-thumb {
  position: absolute;
  left: 2px;
  width: 21px;
  height: 21px;
  border-radius: 11px;
  background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
  transition: transform 160ms;
}

.bui-switch-thumb[data-checked] {
  transform: translateX(17px);
}

/* ---------- Base UI checkbox ---------- */
.bui-checkbox {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: 5px;
  background: var(--panel);
  color: var(--bg);
  cursor: pointer;
  transition: background 120ms, border-color 120ms;
}

.bui-checkbox:hover { border-color: #3a3a40; }

.bui-checkbox[data-checked] {
  background: var(--accent);
  border-color: var(--accent);
}

.bui-checkbox:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.bui-checkbox-indicator {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 11px;
  height: 11px;
}

.bui-checkbox-indicator svg {
  width: 100%;
  height: 100%;
}

/* Holds a mounted checkbox where its hidden native input used to sit. */
.bui-checkbox-host {
  display: inline-flex;
  flex-shrink: 0;
}

/* ---------- Base UI select ----------
   The popup is ours now, so it can be dark. A native select draws its list
   with the OS and opens bright white on this interface. */
.row select[hidden] { display: none; }

.bui-select-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  height: 30px;
  width: 150px;
  padding: 0 10px;
  border: 1px solid var(--line-panel);
  border-radius: 7px;
  background: var(--panel);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 12px;
  cursor: pointer;
  transition: border-color 120ms;
}

.bui-select-trigger:hover { border-color: var(--line-strong); }

.bui-select-trigger:focus-visible {
  outline: none;
  border-color: var(--accent);
}

.bui-select-value {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bui-select-icon {
  display: flex;
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  color: var(--muted);
}

.bui-select-icon svg { width: 100%; height: 100%; }

.bui-select-positioner { z-index: 95; }

.bui-select-popup {
  min-width: 180px;
  padding: 4px;
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  background: var(--panel);
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.6);
}

.bui-select-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 7px 9px;
  border-radius: 6px;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink-2);
  cursor: pointer;
  outline: none;
}

.bui-select-item[data-highlighted] {
  background: var(--panel-2);
  color: var(--ink);
}

.bui-select-item[data-selected] { color: var(--ink); }

.bui-select-item-check {
  display: flex;
  width: 13px;
  height: 13px;
  flex-shrink: 0;
  color: var(--accent);
}

.bui-select-item-check svg { width: 100%; height: 100%; }

.row.check input[type='checkbox'][hidden] {
  display: none;
}

/* The stop labels are two lines tall and sit directly above the next
   control, so the block owns the space beneath it — without this the "0 mm"
   sub-labels are overlapped by whatever row follows. */
/* Same inset as .cutslider, so each label stays under its own stop. */
.cutslider-labels {
  display: flex;
  justify-content: space-between;
  padding-top: 10px;
  margin: 0 8px 14px;
}


/* The stops are also buttons: the slider is the primary control, but a
   direct click on "Sticker" is the faster path when you know what you want. */
.cutstop {
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 64px;
  flex-shrink: 0;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  align-items: center;
}

.cutslider-labels .cutstop:first-child { align-items: flex-start; }
.cutslider-labels .cutstop:last-child { align-items: flex-end; }

.cutstop-name {
  font-size: 12px;
  font-weight: 500;
  color: var(--muted);
  transition: color 120ms;
}

.cutstop-sub {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--muted-3);
  transition: color 120ms;
}

.cutstop:hover .cutstop-name { color: var(--ink-2); }

.cutstop.active .cutstop-name {
  color: var(--ink);
  font-weight: 600;
}

.cutstop.active .cutstop-sub { color: var(--muted-2); }

#preset-hint {
  margin-top: 0;
  line-height: 1.5;
  color: var(--muted-2);
  font-size: 11px;
}

/* A second, independent decision inside the same section. The rule and the
   space above it are what say "this is not part of the slider" — without
   them the checkbox reads as a fifth cut-style option. */
.sub-setting {
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--line-soft);
}

.sub-setting-hint {
  margin-top: 8px;
  /* Indented to the label above it, so the caption clearly belongs to this
     checkbox rather than to the section. */
  padding-left: 26px;
}

/* ---------- detected elements ---------- */
#panel-elements { padding-top: 4px; }

/* Wins over the mode rules above, which set `display` on .group and would
   otherwise override the `hidden` attribute — the same trap the job banner
   hit, where the element was on screen while hidden read as true. */
#panel-elements[hidden] { display: none !important; }

/* A text link, not a button: "Detect" sits in a section header where a
   bordered control would outweigh the heading beside it. */
.link-btn {
  background: none;
  border: 0;
  padding: 0;
  font-family: var(--display);
  font-size: 11px;
  font-weight: 500;
  color: var(--accent);
  cursor: pointer;
}

.link-btn:hover { color: #ffb02e; }

/* Recessive on purpose. Reset undoes the user's work, so it must be findable
   without competing with Export — amber here would read as the thing to
   press. Muted until hover, where it turns amber to confirm it is live. */
.reset-row {
  padding-top: 4px;
}

.reset-btn {
  display: flex;
  align-items: center;
  gap: 7px;
  width: 100%;
  padding: 9px 10px;
  border: 1px solid var(--line, #232326);
  border-radius: 7px;
  color: var(--muted);
  transition: color 120ms ease, border-color 120ms ease;
}

.reset-btn svg {
  fill: currentColor;
  flex: none;
}

.reset-btn:hover {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}

.reset-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.el-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.el-row {
  display: flex;
  align-items: center;
  gap: 11px;
  height: 40px;
  padding: 0 12px;
  border: 1px solid var(--line-panel);
  border-radius: 9px;
  background: var(--panel);
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: var(--ink);
  transition: border-color 120ms, background 120ms;
}

.el-row:hover { border-color: var(--line-strong); }

.el-row.active {
  border-color: rgba(255, 159, 10, 0.5);
  background: rgba(255, 159, 10, 0.06);
}

/* The swatch is the row's only colour, and it is lit only on the selected
   element — so the list says which one the offset slider is about. */
.el-swatch {
  width: 7px;
  height: 7px;
  border-radius: 2px;
  background: var(--muted-2);
  flex-shrink: 0;
  transition: background 120ms;
}

.el-row.active .el-swatch { background: var(--accent); }

.el-name {
  flex: 1 1 0;
  min-width: 0;
  font-size: 13px;
  font-weight: 500;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.el-mm {
  width: 52px;
  flex-shrink: 0;
  text-align: right;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
}

/* ---------- credit pill ---------- */

.credit-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 30px;
  padding: 0 12px;
  border: 1px solid var(--line-strong);
  border-radius: 15px;
  background: var(--panel-2);
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: border-color 120ms, background 120ms;
}

/* A small amber dot, so the pill reads as live state at a glance without
   the number having to be coloured.
   Only when signed in: a balance is live state, an invitation to sign in is
   not, and the dot was previously doing both jobs. */
.credit-pill::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 3px;
  background: var(--accent);
  flex-shrink: 0;
}

.credit-pill.is-signed-out::before { display: none; }

/* Phosphor UserCircle, shown in the dot's place when signed out. */
.credit-avatar {
  display: none;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  fill: var(--muted);
  transition: fill 120ms;
}

.credit-pill.is-signed-out .credit-avatar { display: block; }

.credit-pill.is-signed-out:hover .credit-avatar { fill: var(--ink-2); }

/* Signed out, the pill is an invitation rather than a readout, so the label
   is not shouted in mono the way a number is. */
.credit-pill.is-signed-out .credit-count {
  font-family: var(--display);
  font-size: 13px;
  font-weight: 500;
  color: var(--ink-2);
}

.credit-pill.is-signed-out .credit-label {
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
}

.credit-pill:hover { border-color: #3a3a40; }

.credit-count {
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 500;
  color: var(--ink);
}

.credit-label {
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 11px;
  font-weight: 500;
}

/* Running low, or out, should be visible before a download fails. The dot
   carries the state; only an empty balance escalates to the whole pill,
   because that is the one case where the next click will fail. */
.credit-pill.is-low::before { background: #ffd60a; }
.credit-pill.is-empty::before { background: #ff453a; }
.credit-pill.is-empty {
  border-color: rgba(255, 69, 58, 0.4);
  background: rgba(255, 69, 58, 0.1);
}
.credit-pill.is-empty .credit-count { color: #ff453a; }

/* ---------- confirm sheet ---------- */

.sheet {
  /* A modal <dialog> is centred by the UA default margin: auto, which the
     global reset in this stylesheet zeroes out — without restoring it the
     sheet pins to the top-left corner. */
  margin: auto;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--panel);
  color: var(--ink);
  padding: 0;
  max-width: 380px;
  width: calc(100vw - 32px);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
}

.sheet::backdrop {
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(2px);
}

.sheet-body { padding: 20px; }

.sheet h3 {
  margin: 0 0 6px;
  font-size: 16px;
  letter-spacing: 0.01em;
}

.sheet-text {
  margin: 0 0 14px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted);
}

.sheet-facts {
  list-style: none;
  margin: 0 0 14px;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel-2);
  font-size: 13px;
}

.sheet-facts li {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 3px 0;
}

.sheet-facts span { color: var(--muted); }

.sheet-skip {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--muted);
  margin-bottom: 16px;
  cursor: pointer;
}

.sheet-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

/* ---------- export format dialog ---------- */
.sheet-wide { max-width: 460px; }

.fmt-head {
  margin: 18px 0 10px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
}

.fmt-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* A row per format, described rather than abbreviated: "DXF" alone asks the
   user to already know; "Plotter paths · cut only" tells them. */
.fmt {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 11px 12px;
  border: 1px solid var(--line-panel);
  border-radius: 10px;
  background: var(--panel-2);
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: var(--ink);
  transition: border-color 120ms, background 120ms;
}

.fmt:hover { border-color: var(--line-strong); }

/* The row already shows selection as an amber border and a tick; the UA focus
   ring on top of that reads as a second, contradicting selection. Keyboard
   focus still gets a ring — just ours, and only when keyboard-driven. */
.fmt:focus { outline: none; }

.fmt:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.fmt.is-on {
  border-color: var(--accent);
  background: rgba(255, 159, 10, 0.07);
}

.fmt-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 30px;
  flex-shrink: 0;
  border-radius: 7px;
  background: #2a2a2e;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  color: var(--ink-2);
}

.fmt.is-on .fmt-badge {
  background: rgba(255, 159, 10, 0.18);
  color: var(--accent);
}

.fmt-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1 1 0;
  min-width: 0;
}

.fmt-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

.fmt-default {
  padding: 1px 5px;
  border-radius: 4px;
  background: rgba(255, 159, 10, 0.16);
  color: var(--accent);
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.06em;
}

.fmt-detail {
  font-size: 11px;
  color: var(--muted);
}

.fmt-check {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  color: var(--accent);
  opacity: 0;
  transition: opacity 120ms;
}

.fmt-check svg { width: 100%; height: 100%; }

.fmt.is-on .fmt-check { opacity: 1; }

/* Cost, balance and what is left after — on the same screen as the choice,
   so the price is never a surprise discovered after the click. */
.fmt-cost {
  display: flex;
  gap: 0;
  margin: 16px 0;
  border-top: 1px solid var(--line-panel);
  border-bottom: 1px solid var(--line-panel);
  padding: 12px 0;
}

.fmt-cost > div {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.fmt-cost > div + div {
  border-left: 1px solid var(--line-panel);
  padding-left: 14px;
}

.fmt-cost span {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
}

.fmt-cost strong {
  font-family: var(--mono);
  font-size: 17px;
  font-weight: 500;
  color: var(--ink);
}

.fmt-cost .is-accent { color: var(--accent); }

.fmt-save {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  margin-bottom: 18px;
  cursor: pointer;
}

.fmt-save-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.fmt-save-text strong {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

.fmt-save-text span {
  font-size: 11px;
  color: var(--muted);
}


.btn-sub {
  font-family: var(--mono);
  font-size: 11px;
  opacity: 0.6;
  margin-left: 2px;
}

/* ---------- on-canvas job status ---------- */

/* `hidden` is only a UA default of display:none, so any author `display` rule
   beats it — without this the banner is on screen from page load announcing
   that a file is being prepared before the user has opened one. */
.job[hidden] { display: none; }

.job {
  position: absolute;
  top: 18px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 16px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: rgba(20, 20, 23, 0.92);
  backdrop-filter: blur(6px);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
  font-size: 13px;
  white-space: nowrap;
  animation: job-in 160ms ease-out;
}

@keyframes job-in {
  from { opacity: 0; transform: translate(-50%, -6px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}

/* Holds the cut mark (src/ui/cutmark.ts). It is a bare slot: the indicator
   is the brand mark cutting, and a ring drawn around it would read as a
   second, competing spinner. */
.job-spinner {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  color: var(--ink);
}

.job-sub {
  color: var(--muted);
  font-size: 12px;
}

/* Finished states stop cutting — blades still working after the work is done
   is a lie about what the app is doing. The mark's own stop() cancels the
   animation; these just recolour the parked blades to carry the outcome. */
.job.is-done .job-spinner { color: #2ecc71; }

.job.is-error .job-spinner { color: var(--accent); }

.job.is-done .job-sub,
.job.is-error .job-sub { display: none; }

/* Full-surface loading (src/ui/loading.ts), for waits that block the whole
   workspace rather than annotating a working one. Kept out of the layout
   until it is needed: visibility alone would leave it intercepting clicks. */
/* ---------- startup splash ----------
   Sits above the loading overlay and resolves into it: same mark, same size,
   same place, so the wordmark fades and the wait is already there. */
.splash {
  position: fixed;
  inset: 0;
  z-index: 95;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  /* Decoration over a live app: it must never eat a click meant for the
     studio underneath, and any real interaction dismisses it early. */
  pointer-events: none;
  transition: opacity var(--fade, 420ms) ease-out;
}

.splash.is-leaving { opacity: 0; }

/* The WebGPU flare (src/vendor/flare). It draws its own mark and wordmark, so
   when it comes up the DOM copies are hidden rather than removed — if the GPU
   path fails at any point, or the device is lost, the CSS splash underneath
   is still there and still correct. */
/* Laid out from the start, but invisible: the renderer measures the canvas
   with getBoundingClientRect() during init, and `display: none` would hand it
   a 0x0 surface. Opacity keeps the box real while the CSS splash shows. */
.splash-gpu {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  touch-action: none;
  transition: opacity 200ms ease-out;
}

.splash.is-gpu .splash-gpu { opacity: 1; }
.splash.is-gpu .splash-box { visibility: hidden; }

/* Mark beside the type, matching the GPU lockup: the mark belongs to the
   first line rather than sitting above the pair. */
.splash-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--splash-gap, 26px);
  color: var(--ink);
}

/* Large, uppercase, and hollow. Outlined glyphs are the point: an unlit
   outline is nearly invisible on black, so the light passing through does not
   merely brighten the letters — it is the only thing that draws them. */
.splash-word {
  position: relative;
  font-size: var(--splash-font, 76px);
  font-weight: 700;
  letter-spacing: var(--splash-track, 0.16em);
  /* The tracking adds a trailing gap; this keeps the word optically centred. */
  text-indent: var(--splash-track, 0.16em);
  text-transform: uppercase;
  white-space: nowrap;
}

/* The resting outline. Painted with -webkit-text-stroke rather than a shadow
   so the counters stay open and the weight is even on every edge. */
.splash-word-base {
  color: transparent;
  -webkit-text-fill-color: transparent;
  -webkit-text-stroke: var(--splash-stroke, 1.25px)
    rgba(245, 245, 247, var(--splash-rest, 0.3));
}

/* The flare. background-clip: text confines a moving gradient to the glyphs,
   so the light reads as travelling along the letters rather than as a shape
   sliding over them — the one detail that separates this from a shine.
   Stroked too, so the band lights the outline rather than filling it in. */
.splash-word-flare {
  position: absolute;
  inset: 0;
  background-image: linear-gradient(
    100deg,
    transparent 0,
    transparent calc(50% - var(--band, 22%)),
    #fff 50%,
    transparent calc(50% + var(--band, 22%)),
    transparent 100%
  );
  background-size: 300% 100%;
  background-position: 150% 0;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  -webkit-text-stroke: var(--splash-stroke, 1.25px) transparent;
}

.splash.is-sweeping .splash-word-flare {
  animation: splash-sweep var(--sweep, 1100ms) cubic-bezier(0.3, 0, 0.2, 1) forwards;
}

/* Ends at -150% rather than 0: the band leaves the far edge instead of
   parking mid-word. */
@keyframes splash-sweep {
  from { background-position: 150% 0; }
  to   { background-position: -150% 0; }
}

/* ---------- sheen variant ----------
   The alternate splash: the same lockup, but the band simply repeats rather
   than crossing once and handing over to a ray-marched render. Cheaper on
   every axis — no GPU, no shader compile, nothing to download — and on a
   machine without WebGPU it is what runs anyway, so this is that path made
   deliberate rather than a consolation prize.

   `infinite` with a pause between passes: a band that restarts the instant it
   leaves reads as a barber pole. The gap is bought with a keyframe that holds
   the band off-screen for the back half of each cycle. */
.splash.is-sheen .splash-word-flare {
  animation: splash-sheen var(--sheen, 2600ms) ease-in-out infinite;
}

/* The pause sits at the FRONT, not the back.
   Holding first means the splash opens on the resting lockup, the band
   crosses, and the fade follows immediately — with the wait at the end, a
   single pass spent its last seconds on an empty word while the user waited
   for the splash to go away. Looped, the two read identically; played once,
   only this order works. */
@keyframes splash-sheen {
  0%   { background-position: 150% 0; }
  /* Same value at both stops, so the band holds off-screen rather than
     easing across the word early. */
  35%  { background-position: 150% 0; }
  100% { background-position: -150% 0; }
}

/* Outfit for this variant, at a light weight.
   Its geometric, near-circular O and single-storey construction are the point
   — the letterforms carry themselves without needing an outline to give them
   shape. The flare keeps Inter because its rim pass lights edges and rewards
   the extra detail. */
.splash.is-sheen .splash-word {
  font-family: 'Outfit', var(--display);
  font-weight: 300;
  /* Outfit runs wider than Inter at the same tracking, so it is pulled in to
     keep the lockup the same overall width. */
  letter-spacing: calc(var(--splash-track, 0.16em) * 0.8);
  text-indent: calc(var(--splash-track, 0.16em) * 0.8);
}

/* FILLED, not stroked.
   -webkit-text-stroke centres its line on the glyph path, so at 1.25px on a
   light face the inner and outer edges resolve as two separate lines — the
   doubled strokes that showed inside T, L, E and U. A translucent fill has
   one edge by definition, and the sheen reads the same way over it: the band
   raises the letters from dim to bright rather than tracing their outline. */
.splash.is-sheen .splash-word-base {
  -webkit-text-stroke: 0;
  color: rgba(245, 245, 247, var(--splash-rest, 0.3));
  -webkit-text-fill-color: rgba(245, 245, 247, var(--splash-rest, 0.3));
}

.splash.is-sheen .splash-word-flare {
  -webkit-text-stroke: 0;
}

/* The mark keeps cutting under the sheen, which is the whole reason this
   variant still reads as Cutline rather than as a generic shimmer. */
.splash.is-sheen .splash-box {
  gap: var(--splash-gap, 26px);
}

@media (prefers-reduced-motion: reduce) {
  .splash-word-flare { animation: none; }
}

.loading {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: none;
  align-items: center;
  justify-content: center;
  background: rgba(13, 13, 15, 0.82);
  backdrop-filter: blur(8px);
}

.loading.is-on {
  display: flex;
  animation: loading-in 180ms ease-out;
}

@keyframes loading-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.loading-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  color: var(--ink);
}

.loading-text {
  font-size: 13px;
  color: var(--muted);
  letter-spacing: 0.01em;
}

.sheet-note {
  font-size: 12px;
  opacity: 0.75;
  border-left: 2px solid var(--line);
  padding-left: 10px;
}

/* ---------- credit packs ---------- */

/* Four packs now that pay-as-you-go exists, and auto-fit rather than a fixed
   count so adding a fifth does not silently produce a cramped row. */
.pack-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
  margin-bottom: 14px;
}

/* ---- plan header ---- */

.credits-status {
  margin-bottom: 16px;
}

.credits-status-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 8px;
}

.credits-big {
  font-family: var(--mono);
  font-size: 26px;
  font-weight: 500;
  color: var(--ink);
  line-height: 1;
}

.credits-unit {
  font-size: 12px;
  color: var(--muted);
  margin-left: 6px;
}

.plan-badge {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 3px 9px;
}

.plan-badge.is-paid {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

.credits-sub {
  font-size: 11px;
  color: var(--muted);
  margin: 8px 0 0;
}

/* ---- Base UI meter ---- */

.bui-meter {
  display: block;
  width: 100%;
}

.bui-meter-track {
  display: block;
  height: 6px;
  border-radius: 999px;
  background: var(--line);
  overflow: hidden;
}

.bui-meter-indicator {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--accent);
  transition: width 260ms ease;
}

/* Running out is worth seeing before the download fails rather than after. */
.bui-meter.is-low .bui-meter-indicator {
  background: #ff6b6b;
}

/* ---- subscription offer ---- */

.plan-offer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding: 13px 14px;
  margin-bottom: 14px;
  text-align: left;
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: 9px;
  color: var(--ink);
  font-family: var(--display);
  cursor: pointer;
  transition: border-color 140ms ease, background 140ms ease;
}

.plan-offer:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-color: color-mix(in srgb, var(--accent) 60%, transparent);
}

.plan-offer:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.plan-offer-main {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.plan-offer-name {
  font-size: 13px;
  font-weight: 600;
}

.plan-offer-credits {
  font-size: 11px;
  color: var(--muted);
}

.plan-offer-price {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  flex: none;
}

.plan-offer-price > span:first-child {
  font-family: var(--mono);
  font-size: 15px;
  font-weight: 500;
  color: var(--accent);
}

.plan-offer-per {
  font-size: 10px;
  color: var(--muted);
}

.pack-heading {
  font-size: 11px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  margin: 0 0 8px;
}

.pack {
  display: flex;
  flex-direction: column;
  gap: 2px;
  align-items: center;
  padding: 12px 8px;
  font: inherit;
  color: var(--text);
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  cursor: pointer;
}

.pack:hover:not(:disabled) { border-color: var(--accent); }
.pack:disabled { opacity: 0.45; cursor: not-allowed; }

.pack-credits { font-size: 18px; font-weight: 700; }
.pack-price { font-size: 13px; }
.pack-per { font-size: 10px; color: var(--muted); }

/* ---------------- onboarding tour ---------------- */

/* The tour points at live controls, so its own layer must not swallow the
   clicks that reach them — only the scrim and the bubble take pointer
   events. */
.tour {
  position: fixed;
  inset: 0;
  z-index: 9000;
  pointer-events: none;
}

/* Purely visual. It dims the app but never intercepts a click: the whole
   point of anchoring to live controls is that the user can touch the thing
   being described while being told about it, and a blocking scrim makes the
   product unusable for as long as the tour is open. Advance with Next or the
   arrow keys. */
.tour-scrim {
  position: absolute;
  inset: 0;
  background: rgba(6, 6, 8, 0.72);
  pointer-events: none;
}

/* A ring on the target rather than a hole punched through the scrim: a mask
   would have to track the target through every scroll and resize, and drifts
   out of register the moment it misses one. */
.tour-ring {
  position: absolute;
  border: 2px solid var(--accent);
  border-radius: 9px;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
  transition: left 180ms ease, top 180ms ease, width 180ms ease, height 180ms ease;
  pointer-events: none;
}

.tour-bubble {
  position: absolute;
  width: 290px;
  max-width: calc(100vw - 24px);
  padding: 15px 16px 13px;
  background: var(--rail);
  border: 1px solid var(--line-panel);
  border-radius: 11px;
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.55);
  pointer-events: auto;
}

.tour-count {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--muted);
  margin: 0 0 6px;
}

.tour-title {
  font-family: var(--display);
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 6px;
}

.tour-body {
  font-size: 12px;
  line-height: 1.55;
  color: var(--ink-2);
  margin: 0 0 13px;
}

.tour-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.tour-nav {
  display: flex;
  gap: 6px;
}

.tour-actions button {
  font-family: var(--display);
  font-size: 12px;
  font-weight: 500;
  padding: 6px 12px;
  border-radius: 7px;
  border: 1px solid transparent;
  cursor: pointer;
  background: none;
  color: var(--muted);
}

.tour-skip:hover,
.tour-back:hover:not(:disabled) {
  color: var(--ink);
}

.tour-back {
  border-color: var(--line);
}

.tour-back:disabled {
  opacity: 0.4;
  cursor: default;
}

.tour-next {
  background: var(--accent);
  color: #17110a;
  font-weight: 600;
}

.tour-next:hover {
  background: #ffb02e;
}

.tour-actions button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* An icon-only button in the top bar keeps the square proportions the worded
   buttons would otherwise stretch. */
.btn.icon-only {
  padding: 0;
  width: 30px;
  justify-content: center;
}

.btn.icon-only svg {
  fill: currentColor;
}

/* The tile that was clicked, while the browser navigates to checkout. The
   others are merely disabled; this one says which purchase is underway. */
.pack.is-busy {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  opacity: 1;
}

.pack.is-busy .pack-per {
  color: var(--accent);
}

/* ---------------- tracing indicator ---------------- */

/* Switching a cut style re-traces the whole outline, which on a large image
   is over a second of synchronous work. Without this the studio simply sat
   there and the click looked ignored.

   The transition delay is the point: a fast trace finishes before the
   indicator is ever visible, so quick edits stay silent and only the slow
   ones — the ones that actually need explaining — announce themselves. */
#stage {
  position: relative;
}

#stage::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background-image: linear-gradient(90deg, transparent, var(--accent), transparent);
  background-size: 40% 100%;
  background-repeat: no-repeat;
  opacity: 0;
  pointer-events: none;
  z-index: 5;
  transition: opacity 90ms ease;
}

body.is-tracing #stage::before {
  opacity: 1;
  animation: cutline-trace 900ms linear infinite;
  /* Held back so a 20ms retrace never flickers a bar onto the screen. */
  transition: opacity 90ms ease 240ms;
}

@keyframes cutline-trace {
  from { background-position: -40% 0; }
  to { background-position: 140% 0; }
}

/* The existing line dims while it is being replaced, so what is on screen
   reads as stale rather than current. */
body.is-tracing #cut-path,
body.is-tracing #cut-shadow {
  opacity: 0.4;
  transition: opacity 120ms ease 240ms;
}

body.is-tracing #st-time {
  color: var(--accent);
}
