:root {
  --bg: #f4f5f7;
  --surface: #ffffff;
  --surface-2: #fafbfc;
  --line: #e3e5e9;
  --line-strong: #d0d4da;
  --text: #16181d;
  --muted: #6b7280;
  --accent: #2f7d4f;
  --accent-soft: #e7f3ec;
  --danger: #d4383d;
  --stage: #4c4c50;
  --stage-2: #2b2b2e;
  --grid: #e8eaee;
  --panel-fill: #ffffff;
  --dim: #2f7d4f;
  --shadow: 0 1px 2px rgba(16, 18, 21, .06), 0 8px 24px rgba(16, 18, 21, .06);
  --radius: 10px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #16181c;
    --surface: #1e2126;
    --surface-2: #23262c;
    --line: #2e323a;
    --line-strong: #3a3f48;
    --text: #e8eaed;
    --muted: #9aa1ad;
    --accent: #4ade80;
    --accent-soft: #1c2f24;
    --danger: #f87171;
    --stage: #4c4c50;
    --stage-2: #2b2b2e;
    --grid: #262a31;
    --panel-fill: #1a1d21;
    --dim: #4ade80;
    --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 8px 24px rgba(0, 0, 0, .35);
  }
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}

#app { display: flex; flex-direction: column; height: 100%; }

/* ---------- top bar ---------- */
.topbar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 12px;
  padding: 8px 14px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

.brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
.logo { color: var(--accent); font-size: 18px; }

.name-input {
  font: 600 15px/1.2 inherit;
  color: var(--text);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 4px 6px;
  max-width: 230px;
}
.name-input:hover { border-color: var(--line); }
.name-input:focus { outline: none; border-color: var(--accent); background: var(--surface-2); }

.status { color: var(--muted); font-size: 12px; white-space: nowrap; }
.status.busy { color: var(--accent); }

.viewtoggle {
  display: inline-flex;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 3px;
}
.vt {
  border: 0; background: transparent; color: var(--muted);
  font: 600 13px/1 inherit; padding: 7px 16px; border-radius: 6px; cursor: pointer;
  /* "Cut file" is two words and the tab strip is in a grid column that gives
     it exactly the room it asks for. Without this it wraps onto two lines at
     any laptop width and pushes the whole top bar taller. */
  white-space: nowrap;
}
.vt[aria-selected="true"] { background: var(--surface); color: var(--text); box-shadow: var(--shadow); }

.actions { display: flex; align-items: center; gap: 8px; justify-content: flex-end; }

button { font-family: inherit; }

.icon-btn {
  width: 32px; height: 32px; display: grid; place-items: center;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--text); border-radius: 8px; cursor: pointer; font-size: 15px;
}
.icon-btn:hover:not(:disabled) { background: var(--surface-2); border-color: var(--line-strong); }
.icon-btn:disabled { opacity: .35; cursor: default; }

.primary {
  border: 0; background: var(--accent); color: #fff;
  font: 600 13px/1 inherit; padding: 9px 16px; border-radius: 8px; cursor: pointer;
}
@media (prefers-color-scheme: dark) { .primary { color: #0b1410; } }
.primary:hover { filter: brightness(1.06); }

.ghost {
  border: 1px solid var(--line-strong); background: var(--surface); color: var(--text);
  font: 600 13px/1 inherit; padding: 9px 14px; border-radius: 8px; cursor: pointer;
}
.ghost:hover { background: var(--surface-2); }

/* ---------- layout ---------- */
.layout { flex: 1; display: flex; min-height: 0; }

.stage {
  position: relative; flex: 1; min-width: 0;
  background: radial-gradient(125% 105% at 28% 18%, var(--stage) 0%, var(--stage-2) 78%);
}
/* The preview backdrop is the viewer's choice, independent of the UI theme. */
.stage[data-backdrop="light"] {
  --stage: #ffffff;
  --stage-2: #dde1e8;
  --dim-ink: #23272e;
  --dim-halo: rgba(255, 255, 255, .75);
  --dim-soft: rgba(35, 39, 46, .45);
  --board: #e6e9ef;
  --board-2: #ccd2dc;
  --board-line: #3a4048;
}
.stage[data-backdrop="dark"] {
  --dim-ink: #ffffff;
  --dim-halo: rgba(0, 0, 0, .55);
  --dim-soft: rgba(255, 255, 255, .45);
  --board: #7c828c;
  --board-2: #626873;
  --board-line: #eceef2;
}

.backdrop-pick {
  position: absolute; top: 14px; left: 14px; z-index: 5;
  display: flex; gap: 3px; padding: 4px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 10px; box-shadow: var(--shadow);
}
.backdrop-pick button {
  width: 26px; height: 26px; padding: 0; cursor: pointer;
  border: 1.5px solid var(--line-strong); border-radius: 7px;
}
.backdrop-pick button:hover { border-color: var(--muted); }
.backdrop-pick button[aria-pressed="true"] {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-soft);
}
.stage-pane { position: absolute; inset: 0; }
.stage-pane[hidden] { display: none; }

.stage-hint {
  position: absolute; left: 50%; bottom: 96px; transform: translateX(-50%);
  background: rgba(20, 22, 26, .82); color: #fff; font-size: 12px;
  padding: 6px 12px; border-radius: 999px; pointer-events: none;
  opacity: 0; transition: opacity .2s;
}
.stage-hint.show { opacity: 1; }

/* ---------- inspector ---------- */
.inspector {
  width: 296px; flex: none; overflow-y: auto;
  background: var(--surface); border-left: 1px solid var(--line);
  padding: 12px 14px 40px;
}
.insp-title { font: 600 13px/1 inherit; color: var(--muted); margin: 4px 0 12px; letter-spacing: .02em; }

.group { border: 1px solid var(--line); border-radius: var(--radius); margin-bottom: 10px; overflow: hidden; }
.group > summary {
  list-style: none; cursor: pointer; padding: 11px 12px;
  font: 600 13px/1 inherit; display: flex; justify-content: space-between; align-items: center;
  background: var(--surface-2);
}
.group > summary::-webkit-details-marker { display: none; }
.group > summary::after { content: "⌄"; color: var(--muted); transition: transform .15s; }
.group[open] > summary::after { transform: rotate(180deg); }
.group-body { padding: 12px; display: grid; gap: 12px; }

.field { display: grid; gap: 6px; }
.field > label { font-size: 12px; color: var(--muted); }
.row { display: flex; gap: 8px; align-items: center; }
.row > * { min-width: 0; }

input[type="text"], input[type="number"], select, textarea {
  width: 100%; padding: 7px 9px; font: inherit; font-size: 13px;
  color: var(--text); background: var(--surface);
  border: 1px solid var(--line-strong); border-radius: 7px;
}
input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent-soft); border-color: var(--accent); }
textarea { resize: vertical; min-height: 58px; }

input[type="range"] { width: 100%; accent-color: var(--accent); }

.num { display: flex; align-items: center; gap: 6px; }
.num input { text-align: right; }
.num .unit { font-size: 11px; color: var(--muted); }

.seg { display: flex; border: 1px solid var(--line-strong); border-radius: 8px; overflow: hidden; }
.seg button {
  flex: 1; border: 0; background: var(--surface); color: var(--muted);
  font: 500 12px/1 inherit; padding: 8px 4px; cursor: pointer; white-space: nowrap;
}
.seg button + button { border-left: 1px solid var(--line-strong); }
.seg button[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 600; }

.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.card {
  border: 1.5px solid var(--line-strong); border-radius: 10px; background: var(--surface);
  padding: 6px 5px 7px; cursor: pointer; text-align: center;
  font: 500 10.5px/1.25 inherit; color: var(--muted);
  min-width: 0;
}
.card .art {
  display: block; border-radius: 7px; background: var(--surface-2);
  padding: 3px 2px; margin-bottom: 7px;
}
.card svg { display: block; width: 100%; height: 46px; }
.card:hover { border-color: var(--muted); }
.card[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
.card[aria-pressed="true"] .art { background: var(--accent-soft); }

/* A preset is named, not pictured, and three of the names are long enough that
   three columns would break each of them onto four lines. */
.presets { grid-template-columns: 1fr 1fr; }
.presets .card {
  padding: 9px 8px; min-height: 44px; display: grid; place-items: center;
  line-height: 1.3;
}

.swatch { width: 14px; height: 14px; border-radius: 4px; border: 1px solid rgba(0,0,0,.18); flex: none; }

.hint, .muted { color: var(--muted); font-size: 12px; }
.hint { margin: -4px 0 0; }
.warn { color: var(--danger); font-size: 12px; }

.tick-labels { display: flex; justify-content: space-between; font-size: 10px; color: var(--muted); }

.stat { display: flex; justify-content: space-between; font-size: 12px; color: var(--muted); }
.stat b { color: var(--text); font-weight: 600; }

.link { background: none; border: 0; color: var(--accent); font: inherit; font-size: 12px; cursor: pointer; padding: 0; text-decoration: underline; }

/* ---------- dialogs ---------- */
.dlg { border: 0; border-radius: 14px; padding: 0; background: var(--surface); color: var(--text); box-shadow: var(--shadow); max-width: 480px; width: calc(100% - 32px); }
.dlg::backdrop { background: rgba(10, 12, 15, .5); }
.dlg-body { padding: 20px; display: grid; gap: 14px; }
.dlg h2 { margin: 0; font-size: 17px; }
.dlg-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
.check { display: flex; gap: 8px; align-items: center; font-size: 13px; color: var(--muted); }
.layer-key { display: flex; gap: 14px; font-size: 12px; color: var(--muted); }
.layer-key i { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 5px; }
.note { font-size: 12px; color: var(--accent); margin: 0; }
.steps { margin: 0; padding-left: 20px; display: grid; gap: 8px; font-size: 13px; color: var(--muted); }
.steps li { color: var(--text); }
.steps li span { color: var(--muted); }

@media (max-width: 900px) {
  .inspector { width: 250px; }
  .topbar { grid-template-columns: auto 1fr auto; }
  .name-input { max-width: 130px; }
}

.text-area {
  width: 100%; min-height: 92px; resize: vertical;
  border: 1px solid var(--line-strong); border-radius: 8px;
  padding: 8px 9px; font: 13px/1.45 inherit; color: var(--text);
  background: var(--surface);
}
.text-area:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

.faces-list { display: flex; flex-direction: column; gap: 5px; }
.faces-list { display: flex; flex-direction: column; gap: 4px; }
.face-opt {
  display: flex; align-items: center; gap: 8px; min-height: 42px;
  border: 1px solid var(--line); background: var(--surface);
  border-radius: 8px; padding: 6px 10px; cursor: pointer; text-align: left;
}
.face-opt:hover { background: var(--surface-2); }
.face-opt[aria-pressed="true"] {
  border-color: var(--accent); background: var(--accent-soft);
}
.face-opt .thumb { flex: 1; min-width: 0; display: block; }
.face-opt svg { display: block; width: 100%; height: 26px; }
.thumb-line path { fill: none; stroke: var(--text); stroke-width: .7; }
.thumb-fill path { fill: var(--text); stroke: none; fill-rule: nonzero; }
.face-opt[aria-pressed="true"] .thumb-line path { stroke: var(--accent); }
.face-opt[aria-pressed="true"] .thumb-fill path { fill: var(--accent); }

.pill {
  flex: none; font-size: 10px; font-weight: 700; padding: 2px 6px;
  border-radius: 999px; background: var(--surface-2); color: var(--muted);
}
.pill.warn { background: #fdecec; color: var(--danger); }
@media (prefers-color-scheme: dark) { .pill.warn { background: #3a1f21; } }

.stat b.bad { color: var(--danger); }

.midbar { display: flex; justify-content: center; }




/* The warnings sit on the drawing, not in the settings panel. They are about
   the piece, and the piece is what you are looking at - a note about a 1.4 mm
   neck three scroll-lengths down the inspector is a note nobody reads. Not
   clickable, so they never get in the way of the preview underneath. */
.warnings {
  position: absolute; left: 14px; right: 14px; bottom: 14px; z-index: 4;
  display: flex; flex-direction: column; gap: 6px; pointer-events: none;
}
.warnings[hidden] { display: none; }
.warn-box {
  background: var(--surface); color: var(--text);
  border: 1px solid var(--danger); border-left-width: 4px;
  border-radius: 8px; padding: 8px 12px; font-size: 12.5px; line-height: 1.45;
  box-shadow: var(--shadow);
}

/* ---------- the keychain itself ---------- */
/* Two views, two vocabularies. In the Piece view the board is painted in the
   colour of the sheet that was chosen, so the split ring, the engraving and
   the holes all read against a real material. In the Cut file view nothing is
   painted at all: it is the file, in the layer colours the laser will be
   given, so what is on screen is what the machine is being told to do. */
.preview { position: absolute; inset: 0; width: 100%; height: 100%; }

.board {
  fill: var(--board);
  stroke: var(--board-line);
  stroke-linejoin: round;
}
/* The split ring is drawn first and the board over it, so the half threaded
   through the hole is hidden. It exists to put the hole at the scale of the
   thing that goes in it, which no number in the inspector can do. */
.splitring { stroke: color-mix(in srgb, var(--dim-ink) 55%, transparent); }
/* Part of the cut file, not part of the keychain - the middle of an o on a
   plate with the name cut through it. Ghosted, because that is exactly what it
   is: material that will be there on the bed and gone from the finished piece. */
.dropout {
  fill: color-mix(in srgb, var(--board) 55%, transparent);
  stroke: var(--board-line);
  stroke-dasharray: 1.6 1.4;
  opacity: .6;
}
/* Engraving on the board. A burn is soft and never quite black - a laser
   darkens the surface, it does not print on it - and which way it goes is set
   in JavaScript from the colour of the sheet, not from the backdrop. See
   burnFor() in view.js: on a light board the mark is dark, on a dark one the
   beam frosts the surface and the mark is pale. The fallback here is the dark
   one, for the board colours that never reach the script. */
.burn { stroke: var(--burn, rgba(42, 33, 24, .74)); }
.burn-fill { fill: var(--burn, rgba(42, 33, 24, .74)); }
/* Where it will snap. The inspector gives the number; this says where, which
   is the half that tells you which control to reach for. */
.weak { fill: none; stroke: var(--danger); opacity: .9; }

/* The cut file view, in the three layer colours the export writes. */
.cut { stroke: #ff0000; fill: none; }
.engrave { stroke: #0000ff; fill: none; }
.engrave-fill { fill: #000000; }
.stage[data-backdrop="dark"] .engrave-fill { fill: #dfe3ea; }

/* ---- saved designs ------------------------------------------------------ */
/* A list of things you made, not a control panel: the name leads, the date
   trails behind it quietly, and the delete stays out of the way until the row
   is under the pointer. */
.row-actions { display: flex; gap: 6px; }
.row-actions .wide { flex: 1; }

.design-list {
  list-style: none; margin: 2px 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 4px;
}
.design {
  display: flex; align-items: center; gap: 4px;
  border: 1px solid var(--line); border-radius: 9px; background: var(--surface);
}
.design:hover { border-color: var(--line-strong); }
.design.open { border-color: var(--accent); background: var(--accent-soft); }

.design-open {
  flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px;
  border: 0; background: transparent; cursor: pointer;
  padding: 7px 4px 7px 9px; text-align: left; font: inherit; color: inherit;
}
.design-name {
  font-size: 12.5px; font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.design-when { font-size: 11px; color: var(--muted); }

.design .icon-btn { opacity: 0; margin-right: 4px; }
.design:hover .icon-btn, .design .icon-btn:focus-visible { opacity: 1; }

.warn-line {
  margin: 0; font-size: 12px; color: var(--danger);
}

/* ---- save dialog -------------------------------------------------------- */
.field-label { font-size: 12px; font-weight: 600; color: var(--muted); }
.name-field {
  width: 100%; padding: 9px 11px; font: inherit; font-size: 14px;
  border: 1.5px solid var(--line-strong); border-radius: 9px;
  background: var(--surface); color: var(--text);
}
.name-field:focus { outline: none; border-color: var(--accent); }

/* The gallery can hold a lot of rows; the dialog scrolls rather than growing
   past the bottom of the screen. */
#filesDlg .design-list { max-height: 46vh; overflow-y: auto; }

/* ---- three actions, three weights --------------------------------------- */
/* Export, Save and the gallery are not equal, so they must not look equal.
   Export is the finish line and keeps the filled accent. Save is the same
   family one step down - clearly related, clearly not the main event. The
   gallery is navigation rather than an action, so it stays quietest of all.
   One hue throughout: a second colour here would be three shouting at once. */
.soft {
  border: 1px solid var(--accent); background: var(--accent-soft);
  color: var(--accent);
  font: 600 13px/1 inherit; padding: 9px 14px; border-radius: 8px; cursor: pointer;
}
.soft:hover { filter: brightness(0.97); }

.quiet { display: inline-flex; align-items: center; gap: 6px; }
.quiet svg { width: 14px; height: 14px; opacity: 0.75; }

/* ---------- phone ---------- */
/* The side-by-side layout stops working well below 640px: the stage collapses
   to a sliver and the top bar's three columns run off the edge. So stack it -
   preview on top at the full width of the screen, settings in a scrolling
   panel underneath - and give the top bar two rows so nothing collides.
   Nothing here touches the tablet (900px) or desktop layout. */
@media (max-width: 640px) {
  /* top bar: brand and view tabs share row one, the buttons take row two. */
  .topbar {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "brand   mid"
      "actions actions";
    gap: 6px 8px;
    padding: 6px 10px;
  }
  .brand { grid-area: brand; gap: 6px; }
  .midbar, .topbar > .viewtoggle { grid-area: mid; }
  .actions { grid-area: actions; flex-wrap: wrap; row-gap: 6px; gap: 6px; }

  .logo { font-size: 16px; }
  /* 16px, not 14. Safari zooms the whole page when a field under 16px takes
     focus, and the layout that was just fitted into 375px is then wider than
     the screen. This field is written with no type attribute, so the
     input[type=text] rule that carries 16px never reached it. */
  .name-input { flex: 1 1 auto; min-width: 0; max-width: none; font-size: 16px; }
  /* The autosave indicator - Saving... / Saved. Dropped on a phone for room,
     and that is a real loss rather than a duplicate of the Save button: Save
     only opens the dialog and never reports autosave state. The top bar is
     already two rows tall here, so the room wins. */
  .status { display: none; }

  .vt { padding: 9px 11px; font-size: 12px; }

  /* The size readout is information, not a button; it gets its own quiet line
     rather than fighting the buttons for horizontal room. */
  .actions > #sizeReadout { flex: 1 0 100%; order: -1; font-size: 11.5px; }

  .icon-btn { width: 38px; height: 38px; font-size: 16px; }
  .primary, .ghost, .soft { padding: 11px 14px; }

  /* Icon only - the label "Reka bentuk saya" is the widest thing in the row
     and the folder icon carries the same meaning. */
  #filesBtn {
    width: 38px; height: 38px; padding: 0;
    justify-content: center; font-size: 0;
  }
  #filesBtn svg { width: 18px; height: 18px; }

  /* ---- stacked layout ---- */
  .layout { flex-direction: column; }

  .stage {
    flex: 5 1 0;
    min-height: 240px;
    /* dimension arrows are drawn outside the drawing box; on a phone the
       settings panel is directly below them, so clip. */
    overflow: hidden;
  }

  .inspector {
    flex: 4 1 0;
    width: auto;
    min-height: 0;
    border-left: 0;
    border-top: 1px solid var(--line);
    padding: 10px 12px 28px;
    -webkit-overflow-scrolling: touch;
  }
  .insp-title { margin: 2px 0 8px; }

  /* ---- finger-sized controls ---- */
  .group > summary { padding: 14px 12px; }
  input[type="text"], input[type="number"], select, textarea {
    /* 16px also stops iOS zooming the page when a field takes focus. */
    font-size: 16px; padding: 10px 11px;
  }
  .seg button { padding: 12px 4px; font-size: 13px; }
  .cat-tabs button { padding: 9px 13px; }
  .face-opt { min-height: 48px; }
  input[type="range"] { height: 28px; }

  /* The dimension callouts cluster along the bottom and right of the drawing
     once it fills a phone-width stage, so the hint moves clear of them. */
  .stage-hint {
    top: 12px; bottom: auto; max-width: calc(100% - 110px);
    text-align: center; font-size: 11px; padding: 5px 10px;
  }

  /* ---- dialogs ---- */
  /* A row of four or five export buttons is wider than the screen, so the row
     wraps into pairs instead of pushing the primary action off the edge. */
  .dlg-body { padding: 16px; gap: 12px; }
  .dlg-actions { flex-wrap: wrap; gap: 8px; }
  .dlg-actions button { flex: 1 1 42%; min-width: 0; }
  .dlg-actions .ghost.wide { padding: 11px 12px; }
  .layer-key { flex-wrap: wrap; gap: 6px 14px; }
  #filesDlg .design-list { max-height: 40vh; }
}


/* Two panes in the one stage, one shown at a time. The 3D canvas keeps its
   size while hidden rather than being torn down and rebuilt on every tab
   press: WebGL context creation is the slow part, and a tab that takes half
   a second to come back reads as a tool that is thinking. */
.stage-pane[hidden] { display: none !important; }
#stage3d { position: absolute; inset: 0; }
#stage3d canvas { display: block; width: 100%; height: 100%; }
