/* ProjectDiary — "Nocturne" design system.
   Tokens/components below are ported from the design handoff at
   design/design_handoff_projectdiary_redesign/design_files/nocturne-styles.css
   (kept byte-for-byte where possible); the app-specific rules further down
   (sidebar, item rows, project groups, tiles, dashboard groups, forms glue)
   are additions needed to actually assemble the app's pages from them. */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');

:root {
  --color-bg: #161826;
  --color-surface: #232532;
  --color-text: #e9e9ed;
  --color-accent: #9184d9;
  --color-accent-2: #a7a1db;
  --color-divider: color-mix(in srgb, #e9e9ed 16%, transparent);

  --color-neutral-100: #f3f5fe;
  --color-neutral-200: #e4e7f5;
  --color-neutral-300: #cfd3e5;
  --color-neutral-400: #b2b6ca;
  --color-neutral-500: #9397ab;
  --color-neutral-600: #75798c;
  --color-neutral-700: #595d6c;
  --color-neutral-800: #3f424d;
  --color-neutral-900: #292b31;

  --color-accent-100: #f5f4ff;
  --color-accent-200: #e7e5fe;
  --color-accent-300: #d2cefd;
  --color-accent-400: #b5abfc;
  --color-accent-500: #968ae0;
  --color-accent-600: #796cbf;
  --color-accent-700: #5d5294;
  --color-accent-800: #423a6a;
  --color-accent-900: #2b2741;

  --font-heading: "Inter", system-ui, sans-serif;
  --font-heading-weight: 500;
  --font-body: "Inter", system-ui, sans-serif;

  /* Fluid spacing: every padding/margin/gap value in this file (including
     the --space-N scale below) is written as calc(N * var(--space-unit)),
     where N is the original authored px number with the unit stripped off --
     the same technique the font-size rule above uses for text (rem instead
     of a raw px), kept as a separate variable so spacing and text can be
     tuned independently.
     --space-unit is "how many px is 1 spacing-unit right now" -- it's a
     length, not a bare ratio, because CSS calc() can't divide two lengths
     into a unitless number; multiplying a unitless N by this length is the
     valid way to get the same fluid effect. It scales linearly between
     0.7px at a 480px-wide window and 1.15px at 1600px+, so:
       - clamp()'s first argument is a hard floor -- however narrow the
         window gets, spacing never drops below 70% of its authored size.
       - it grows slightly (up to 115%) on spacious windows, then stops.
     (0.5071px + 0.0402vw is the linear interpolation between those two
     points, expressed directly in CSS length units -- no calc() needed.) */
  --space-unit: clamp(0.7px, 0.5071px + 0.0402vw, 1.15px);

  --space-1: calc(2.8 * var(--space-unit));
  --space-2: calc(5.6 * var(--space-unit));
  --space-3: calc(8.4 * var(--space-unit));
  --space-4: calc(11.2 * var(--space-unit));
  --space-6: calc(16.8 * var(--space-unit));
  --space-8: calc(22.4 * var(--space-unit));

  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 14px;

  --shadow-sm: 0 0 0 1px #3f424d;
  --shadow-md: 0 0 0 1px #595d6c, 0 6px 18px rgba(0,0,0,0.55);
  --shadow-lg: 0 0 0 1px #9397ab, 0 16px 40px rgba(0,0,0,0.65);

  /* Priority is a fixed, meaning-bearing color scale (kept out of the
     neutral/accent ramps above on purpose -- see shared.js PRIORITY_VISUAL,
     which is the single source of truth these values are duplicated from
     for the few places CSS needs them directly). */
  --danger: #ff9f9c;
  --warning: rgb(255, 165, 0);
  --success: #8fdb9f;
  /* Distinct from --danger on purpose -- --danger's soft rose is shared with
     the Dashboard's btn-danger/overdue-stat styling, which this change isn't
     meant to touch. --warning has no other consumer yet, so it was safe to
     repoint in place above; this one needed its own token to avoid a side
     effect on unrelated UI. Pure red (not the softer priority-matched tones
     used elsewhere) since the History overview table's red vs. orange were
     too close in hue to tell apart at a glance. */
  --urgency-overdue: rgb(255, 0, 0);
}

*, *::before, *::after { box-sizing: border-box; }

/* Every font-size above is authored in rem (converted 1:1 from the design
   handoff's px values), so this one line scales all of them together --
   130% of the browser's own root size, which also means it still respects
   a user's own browser zoom/accessibility font setting instead of a fixed
   px override. */
html { font-size: 130%; }

/* "Dynamic 보기" -- an opt-in toggle in the sidebar (see shared.js renderNav()
   and js/view-init.js, which applies this class before first paint so a
   page load with it already on doesn't flash at the default scale first).
   At the 1180px reference width (the app's designed content width, see
   `main > .wrap` further down) both curves equal their static-rule values
   above with no visible jump -- font is exactly 130%, spacing is ~1x --
   and instead of stopping there like the static rules do, both keep
   growing continuously as the window widens past it. Capped at a
   generous-but-real 300% / 2.3077x rather than literally unbounded:
   several things stay fixed size regardless of this scale (the sidebar's
   232px floor, 1180px content width, button/icon/input dimensions), so
   truly infinite growth would eventually overflow or look disproportionate
   on an ultra-wide/high-DPI display. Both growth curves are deliberately
   tuned to reach that same 2.3077x (300/130) ratio at the same viewport
   width, so text and spacing grow in visual lockstep instead of drifting
   apart -- spacing's static rule was tuned independently for its own,
   lower 1.15x cap, so this growth slope is intentionally different from it.

   font-size's floor is a plain clamp() because its floor value (130%) and
   its growth-start reference (also 130%, at 1180px) are the same number --
   clamping is enough to make it flat for every width below 1180px, with no
   separate segment needed. --space-unit's floor (0.7px) and its
   growth-start reference (1px, at 1180px) are *different* numbers, so a
   plain clamp() on the growth line would only approach 0.7px asymptotically
   as width shrinks (never actually reaching it above 0px width) instead of
   plateauing -- a real gap from the "always maintain a floor" requirement
   the static rule satisfies cleanly at 480px. The nested max()/min() below
   inserts the missing middle segment: flat at the 0.7px floor through
   480px (matching the static rule's own floor width, for a consistent feel
   switching Dynamic 보기 on/off), ramping up to the 1px/1180px growth
   anchor, THEN switching (via min(), since that ramp's slope is steeper
   than the growth segment's -- min() is what correctly "hands off" between
   two connected segments where slope decreases at the join) to the
   shallower, font-coordinated growth line above. */
html.dynamic-view {
  font-size: clamp(130%, calc(130% + 0.005245902 * (100vw - 1180px)), 300%);
}
.dynamic-view {
  --space-unit: clamp(
    0.7px,
    min(
      max(0.7px, calc(0.7px + 0.0004286 * (100vw - 480px))),
      calc(1px + 0.00025213 * (100vw - 1180px))
    ),
    2.3077px
  );
}

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 0.9375rem;
  line-height: 1.55;
  font-weight: 400;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);
}
h1 { font-size: 2.625rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.5625rem; }
h4 { font-size: 1.25rem; }
h5 { font-size: 1rem; }
h6 { font-size: 0.8125rem; letter-spacing: 0.08em; text-transform: uppercase; }
p { margin: 0 0 var(--space-3); }
a { color: var(--color-accent); text-underline-offset: 3px; }
.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }
:focus { outline: none; }
:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: var(--color-neutral-800); border-radius: 8px; }

/* — rules — */
.hr {
  height: 1px; border: 0; margin: var(--space-4) 0;
  background: linear-gradient(to right,
    transparent, var(--color-divider) 48px,
    var(--color-divider) calc(100% - 48px), transparent);
}

/* — buttons — */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: calc(6 * var(--space-unit));
  cursor: pointer; text-decoration: none;
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 0.875rem; line-height: 1.2; color: var(--color-text);
  background: transparent; border: 1px solid transparent;
  padding: var(--space-2) calc(var(--space-3) * 1.2);
  border-radius: var(--radius-md);
}
.btn svg { display: block; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }
.btn-primary:hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }
.btn-primary:active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }
.btn-secondary { border-color: var(--color-divider); }
.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }
.btn-danger { color: var(--danger); }
.btn-danger:hover { background: color-mix(in srgb, var(--danger) 12%, transparent); }
.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }
.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }
.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }
.btn-icon { width: 30px; height: 30px; padding: 0; }
/* Delete/trash buttons only -- each gets its own button box sized to its
   own iconSvg('trash', N) size, rather than falling back to .btn-icon's
   plain 30x30 default, so every trash icon keeps the same ~12px-of-
   breathing-room proportion regardless of whether that particular icon is
   above or below 30px. Each class here also carries .btn-icon in its
   markup, so this needs to win that cascade tie -- same single-class
   specificity, but declared after .btn-icon above, so source order
   decides. Sized independently per icon (not one shared box for all of
   them) so a compact table row's delete button doesn't inherit the same
   footprint as a spacious card's -- not the unrelated pencil-edit buttons
   that also use .btn-icon elsewhere (e.g. the Dashboard), which are
   intentionally left untouched. */
.danger-delete-entry, .task-delete { width: 28.8px; height: 28.8px; }
.delete-project { width: 43.2px; height: 43.2px; }
.delete-milestone, .remove-row { width: 27.6px; height: 27.6px; }
.btn-block { width: 100%; margin-top: var(--space-2); }
.btn-sm { font-size: 0.75rem; padding: calc(4 * var(--space-unit)) calc(10 * var(--space-unit)); }
button { font-family: inherit; }

/* — forms — */
.field { margin-bottom: var(--space-4); }
.field > label {
  display: block; font-size: 0.75rem; margin-bottom: calc(5 * var(--space-unit));
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}
.input, select, input[type=text], input[type=date], input[type=number], textarea {
  width: 100%; min-height: 36px; padding: calc(6 * var(--space-unit)) calc(10 * var(--space-unit)); font: inherit;
  font-size: 0.875rem; color: var(--color-text); caret-color: var(--color-accent);
  background: var(--color-surface);
  border: 1px solid var(--color-divider); border-radius: var(--radius-md);
}
/* Cards/grid cells can get very wide once .wrap fills the browser in Dynamic
   보기 -- without this, a short field (project name, priority dropdown, a
   date picker) would stretch to match instead of staying a sensible reading
   width. Scoped to .dynamic-view specifically: some fields (e.g. Manage's
   설명/상태 in public/manage.html) aren't inside a .grid-2, so in the
   default 1180px-capped layout they already stretch across most of the
   card on purpose -- an unscoped version of this rule would shrink them
   there too, changing the existing static-mode look for something nobody
   asked to change. `ch` scales with the field's own font-size, so the cap
   itself grows right along with Dynamic 보기's font growth (still ~42
   characters' worth of width, just at the larger size) rather than staying
   pinned to a fixed px value that would feel increasingly cramped.
   `<textarea>` is deliberately excluded -- the Input page's raw-text box
   and inline-edit content boxes are multi-line editing surfaces that
   should keep using the extra width, not get capped like a single-line
   field. Anything with its own inline `style="width:..."` (taxonomy table
   cells, the search box) is unaffected: `max-width` only ever shrinks a
   width that would otherwise be larger, and those inline widths are
   already well under 42ch. */
.dynamic-view .input,
.dynamic-view select,
.dynamic-view input[type=text],
.dynamic-view input[type=date],
.dynamic-view input[type=number] { max-width: 42ch; }
select { color-scheme: dark; }
input[type=date]::-webkit-calendar-picker-indicator { filter: invert(1); }
.input:hover, select:hover, input:hover, textarea:hover { border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }
.input:focus-visible, select:focus-visible, input:focus-visible, textarea:focus-visible { border-color: var(--color-accent); outline-offset: 0; }
textarea { min-height: 90px; resize: vertical; }
textarea.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.84375rem; line-height: 1.6; }

input[type=checkbox] {
  width: 18px; height: 18px;
  accent-color: var(--color-accent);
  cursor: pointer;
  flex-shrink: 0;
}

.seg { display: inline-flex; overflow: hidden; border: 1px solid var(--color-divider); border-radius: var(--radius-md); }
.seg-opt { display: inline-flex; align-items: center; gap: calc(6 * var(--space-unit)); padding: calc(7 * var(--space-unit)) calc(12 * var(--space-unit)); font-size: 0.8125rem; cursor: pointer; }
.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }
.seg-opt input { position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none; }
.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }
.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }

details > summary { cursor: pointer; font-size: 0.8125rem; color: var(--color-accent); user-select: none; list-style: none; }
details > summary::-webkit-details-marker { display: none; }

/* — cards — */
.card { display: flex; flex-direction: column; gap: var(--space-2); padding: var(--space-3); border-radius: var(--radius-md); background: var(--color-surface); }
.card-kicker { font-size: 0.625rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }
.card-title { font-family: var(--font-heading); font-weight: var(--font-heading-weight); font-size: 1.0625rem; line-height: 1.2; margin: 0; }
.elev-sm { box-shadow: var(--shadow-sm); }
.elev-md { box-shadow: var(--shadow-md); }
.card-flat { padding: 0; gap: 0; overflow: hidden; }
.card-flat-header {
  padding: calc(16 * var(--space-unit)) calc(20 * var(--space-unit)); border-bottom: 1px solid var(--color-divider);
  display: flex; align-items: center; justify-content: space-between; gap: calc(12 * var(--space-unit));
}

/* — tags/badges — */
.tag {
  display: inline-flex; align-items: center; font-size: 0.6875rem;
  letter-spacing: 0.02em; padding: calc(3 * var(--space-unit)) calc(10 * var(--space-unit));
  border-radius: calc(var(--radius-md) * 0.75);
  font-weight: 500; white-space: nowrap; flex: none;
}
.tag-neutral { background: var(--color-neutral-800); color: var(--color-neutral-100); }
.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }

/* — tables — */
.table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.table th {
  text-align: left; font-size: 0.6875rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 60%, transparent);
  padding: var(--space-2) var(--space-3);
}
.table td { padding: var(--space-2) var(--space-3); }
.table thead tr {
  background: linear-gradient(to right,
    transparent, var(--color-divider) 48px,
    var(--color-divider) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px;
}
.table tbody tr {
  background: linear-gradient(to right,
    transparent, color-mix(in srgb, var(--color-text) 8%, transparent) 48px,
    color-mix(in srgb, var(--color-text) 8%, transparent) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px;
}
.table tbody tr:hover {
  background:
    linear-gradient(color-mix(in srgb, var(--color-text) 4%, transparent),
                    color-mix(in srgb, var(--color-text) 4%, transparent)) no-repeat 0 0 / 100% 100%,
    linear-gradient(to right,
      transparent, color-mix(in srgb, var(--color-text) 8%, transparent) 48px,
      color-mix(in srgb, var(--color-text) 8%, transparent) calc(100% - 48px), transparent) no-repeat bottom / 100% 1px;
}
.color-swatch { width: 14px; height: 14px; border-radius: 4px; display: inline-block; }

/* ══════════════════════════════════════════════════════════════════════
   App shell — sidebar + main
   ══════════════════════════════════════════════════════════════════ */
body.app { display: flex; min-height: 100vh; }

.sidebar {
  /* min-width (not a fixed width) so Dynamic 보기's larger text has room to
     grow the sidebar rather than wrap inside it -- flexbox sizes a `flex:
     none` item to its content's natural width by default, so this only
     changes behavior once that content actually needs more than 232px. */
  min-width: 232px; flex: none;
  background: var(--color-surface);
  border-right: 1px solid var(--color-divider);
  padding: calc(26 * var(--space-unit)) calc(14 * var(--space-unit));
  display: flex; flex-direction: column; gap: calc(4 * var(--space-unit));
  position: sticky; top: 0; height: 100vh; align-self: flex-start;
}
.sidebar-brand {
  display: flex; align-items: center; gap: calc(8 * var(--space-unit));
  padding: 0 calc(10 * var(--space-unit)); margin-bottom: calc(22 * var(--space-unit));
}
.sidebar-brand span { font-family: var(--font-heading); font-weight: 600; font-size: 1rem; white-space: nowrap; }
.sidebar a {
  display: flex; align-items: center; gap: calc(10 * var(--space-unit));
  padding: calc(10 * var(--space-unit)) calc(12 * var(--space-unit)); border-radius: 8px;
  font-size: 0.875rem; text-decoration: none; color: var(--color-text);
  border-left: 2px solid transparent; white-space: nowrap;
}
.sidebar a:hover { background: color-mix(in srgb, var(--color-text) 6%, transparent); }
.sidebar a.active {
  color: var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 14%, transparent);
  border-left: 2px solid var(--color-accent);
}
.sidebar a svg { flex: none; }

/* Pinned to the bottom of the (flex-column) sidebar via margin-top: auto,
   separating it from the primary nav links above it. */
.view-toggle {
  display: flex; align-items: center; justify-content: space-between;
  gap: calc(8 * var(--space-unit)); margin-top: auto;
  padding: calc(10 * var(--space-unit)) calc(12 * var(--space-unit));
  font-size: 0.8125rem; color: color-mix(in srgb, var(--color-text) 70%, transparent);
  cursor: pointer;
}
/* Only the first view toggle owns the flexible spacer that pins the pair
   to the bottom. Giving both labels margin-top:auto splits the spare height
   between them and leaves the two related switches far apart. */
.view-toggle + .view-toggle { margin-top: 0; }
/* The sidebar's own width stays fixed regardless of Dynamic 보기's scale
   (same as its icons/buttons), so at large scales this label's text can
   outgrow the space next to the switch. min-width: 0 lets a flex item
   shrink below its content's natural width in the first place -- without
   it, overflow/text-overflow do nothing and the browser wraps instead. */
.view-toggle-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
/* Same idea as .view-toggle (label + .switch), but a compact inline variant
   for section headers (Input/History pages' "완료 표시" toggles) rather than
   the sidebar's own pinned-to-bottom layout. */
.mini-toggle {
  display: inline-flex; align-items: center; gap: calc(8 * var(--space-unit));
  font-size: 0.8125rem; color: color-mix(in srgb, var(--color-text) 70%, transparent);
  cursor: pointer; white-space: nowrap; flex: none;
}
/* Track/thumb are a fixed physical control size (like the checkbox and
   btn-icon box elsewhere) -- only the gap/padding around it scales. */
.switch { position: relative; display: inline-flex; flex: none; width: 36px; height: 20px; }
.switch input {
  position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer;
}
/* pointer-events: none on the decorative track/thumb so a click anywhere on
   the visible switch always hits the (invisible but real) input directly,
   instead of the click landing on whichever <span> happens to paint on top
   and only reaching the input indirectly via <label> click-forwarding. */
.switch-track {
  position: absolute; inset: 0; border-radius: 999px; pointer-events: none;
  background: var(--color-neutral-800); border: 1px solid var(--color-divider);
  transition: background .15s, border-color .15s;
}
.switch-thumb {
  position: absolute; top: 1px; left: 1px; width: 16px; height: 16px; border-radius: 50%;
  pointer-events: none;
  background: var(--color-neutral-300);
  transition: transform .15s, background .15s;
}
.switch input:checked ~ .switch-track {
  background: color-mix(in srgb, var(--color-accent) 55%, transparent); border-color: var(--color-accent);
}
.switch input:checked ~ .switch-thumb { transform: translateX(16px); background: var(--color-accent-100); }
.switch input:focus-visible ~ .switch-track { outline: 2px solid var(--color-accent); outline-offset: 2px; }

main {
  flex: 1; min-width: 0;
  padding: calc(40 * var(--space-unit)) calc(44 * var(--space-unit)) calc(64 * var(--space-unit));
}
main > .wrap { max-width: 1180px; }
/* Dynamic 보기's font/spacing growth alone doesn't use extra window width --
   without this, a wide monitor just showed bigger text in the same 1180px
   column with empty space past it. main is already flex:1 (fills everything
   beside the sidebar), so dropping .wrap's cap is enough to let cards/
   tables/inputs actually reach the browser edge -- no replacement ceiling
   here, since the viewport (and the sidebar/main padding already scaling
   with --space-unit) naturally bounds it without one. */
.dynamic-view main > .wrap { max-width: none; }
.page-subtitle {
  margin: 0 0 calc(28 * var(--space-unit)); font-size: 0.875rem;
  color: color-mix(in srgb, var(--color-text) 60%, transparent);
  max-width: 64ch;
}

@media (max-width: 700px) {
  body.app { flex-direction: column; }
  .sidebar {
    flex-direction: row; align-items: center; width: 100%; min-width: 0; min-height: auto;
    height: auto; position: static; padding: calc(10 * var(--space-unit)) calc(20 * var(--space-unit));
    /* Nav labels + the switch use nowrap/min-width overrides so they don't
       wrap or clip -- on a narrow phone screen at a large Dynamic 보기
       scale, their combined content width can still exceed the viewport.
       width: 100% (not auto -- flex stretch alignment wasn't reliably
       constraining this) + min-width: 0 lets the box actually shrink to the
       viewport instead of growing to fit content, so overflow-x scrolls it
       inside the bar itself rather than the whole page gaining horizontal
       scroll. */
    overflow-x: auto;
  }
  .sidebar-brand { margin: 0 calc(24 * var(--space-unit)) 0 0; }
  /* The sidebar is a horizontal bar here, not a column, so margin-top: auto
     no longer pins this to an "end" -- flex order handles that instead
     (it's last in the nav's markup already). Drop the label so the switch
     doesn't crowd out the nav links that share this row. */
  .view-toggle { margin-top: 0; margin-left: auto; padding: calc(10 * var(--space-unit)) calc(8 * var(--space-unit)); }
  .view-toggle + .view-toggle { margin-left: 0; }
  .view-toggle-label { display: none; }
}

/* ══════════════════════════════════════════════════════════════════════
   Shared list patterns — project groups, item rows (dashboard/input/history)
   ══════════════════════════════════════════════════════════════════ */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: calc(20 * var(--space-unit)); }
@media (max-width: 860px) { .grid-2 { grid-template-columns: 1fr; } }

.section-kicker { display: flex; align-items: baseline; gap: calc(10 * var(--space-unit)); margin-bottom: calc(16 * var(--space-unit)); }
.section-kicker h6 { margin: 0; color: color-mix(in srgb, var(--color-text) 55%, transparent); }
.section-kicker .hr { flex: 1; margin: 0; }

.project-group {
  display: flex; flex-direction: column; gap: calc(2 * var(--space-unit));
  background: var(--color-bg); border: 1px solid var(--color-divider);
  border-radius: 8px; padding: calc(10 * var(--space-unit)) calc(12 * var(--space-unit));
}
.project-group + .project-group { margin-top: calc(8 * var(--space-unit)); }
.project-group-title { font-size: 0.75rem; font-weight: 600; color: var(--color-accent); margin-bottom: calc(4 * var(--space-unit)); }

.task-row {
  display: flex; align-items: center; gap: calc(12 * var(--space-unit)); padding: calc(10 * var(--space-unit)) calc(6 * var(--space-unit));
  padding-left: calc(10 * var(--space-unit)); border-radius: 4px;
  border-left: 3px solid var(--row-accent, transparent);
  transition: opacity .2s, background .15s;
}
.task-row:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }
.task-row.done { opacity: 0.4; }
.task-row .task-title { flex: 1; font-size: 0.875rem; min-width: 0; overflow-wrap: anywhere; }
.task-row.done .task-title { text-decoration: line-through; }
.task-row .task-who {
  display: inline-flex; font-size: 0.6875rem; padding: calc(3 * var(--space-unit)) calc(9 * var(--space-unit)); border-radius: 6px;
  background: var(--color-neutral-800); color: var(--color-neutral-100);
  font-weight: 500; flex: none; white-space: nowrap;
}
/* Dashboard task rows no longer group under a per-project heading (tasks
   are one flat priority-ranked list instead), so each row carries its own
   project tag -- outlined/accent-colored rather than .task-who's solid
   neutral fill, so the two read as distinct kinds of metadata (which
   project vs. who's assigned) at a glance rather than two similar pills. */
.task-row .task-project {
  display: inline-flex; font-size: 0.6875rem; padding: calc(3 * var(--space-unit)) calc(9 * var(--space-unit)); border-radius: 6px;
  border: 1px solid var(--color-accent); color: var(--color-accent);
  font-weight: 500; flex: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  /* `ch` (not a fixed px) so this cap scales with the badge's own font-size --
     Dynamic 보기 grows font-size up to ~2.3x, and a fixed px max-width stayed
     put while the text grew past it, silently clipping a real character out
     of the project name (e.g. "Magyar PG2390" -> "Magyar PG239") instead of
     just resizing the badge like every other row element already does.
     28 (not 20) because `ch` is defined by the font's "0" glyph, which is
     much narrower than a CJK character -- project names have no length or
     script restriction (server/routes/projects.js), so a Korean name needs
     more of these units per visible character than an English one does. */
  max-width: 28ch;
}
.task-row .task-date { font-size: 0.8125rem; font-weight: 600; color: var(--color-text); flex: none; white-space: nowrap; }
.task-row .task-reschedule-btn { flex: none; white-space: nowrap; }
.task-row .task-date-shift { display: inline-flex; gap: calc(2 * var(--space-unit)); flex: none; }
.task-row .task-shift-btn { padding-left: calc(6 * var(--space-unit)); padding-right: calc(6 * var(--space-unit)); }
/* Only ever rendered on a done row (already dimmed via .task-row.done's
   opacity), so this stays plain/small rather than competing with task-date's
   bolder styling used for still-open tasks' due dates. */
.task-row .task-done-at { font-size: 0.75rem; color: color-mix(in srgb, var(--color-text) 60%, transparent); flex: none; white-space: nowrap; }

/* — dashboard: inline task edit row — */
.task-edit-row {
  display: flex; flex-direction: column; gap: calc(6 * var(--space-unit));
  padding: calc(10 * var(--space-unit));
  background: color-mix(in srgb, var(--color-text) 4%, transparent);
  border-radius: 6px;
}
.task-edit-row-fields { display: flex; gap: calc(6 * var(--space-unit)); flex-wrap: wrap; }
.task-edit-row-fields .input, .task-edit-row-fields select { flex: 1; min-width: 90px; }

/* — dashboard: hover popup showing the info-record a task came from —
   pointer-events:none so it can never intercept clicks meant for the
   checkbox/edit button on the row underneath it. */
.task-popup {
  /* ch (not a fixed px) so this keeps pace with Dynamic 보기's font growth --
     .task-popup-body below clamps to a fixed 6 lines, so a fixed px width
     here would mean strictly less real content survives that clamp as the
     font (a rem value, root-relative) grows, the same class of bug as
     .task-project's max-width fix above (this one silently drops trailing
     content instead of clipping mid-word). .calendar-popup further down
     still overrides this for Statistics' own popup, which doesn't use the
     line-clamp body and was already sized for that separately. */
  position: absolute; z-index: 300; max-width: min(36ch, calc(100vw - 16px));
  overflow-wrap: anywhere;
  background: var(--color-surface); border: 1px solid var(--color-divider);
  border-radius: var(--radius-md); box-shadow: var(--shadow-md);
  padding: calc(10 * var(--space-unit)) calc(12 * var(--space-unit));
  pointer-events: none; opacity: 0; visibility: hidden;
  transition: opacity .12s ease;
}
.task-popup.show { opacity: 1; visibility: visible; }
.task-popup-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: calc(8 * var(--space-unit)); margin-bottom: calc(6 * var(--space-unit));
}
.task-popup-date { font-size: 0.6875rem; color: color-mix(in srgb, var(--color-text) 60%, transparent); white-space: nowrap; }
.task-popup-body {
  font-size: 0.8125rem; color: var(--color-text); line-height: 1.5;
  display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical;
  overflow: hidden; white-space: pre-wrap;
}
/* Statistics page's calendar popup only -- a modifier layered on top of
   .task-popup rather than editing it directly, since Dashboard's own hover
   popup (showing one info-record's summary) shares that base class and
   already looks right at its current width/type-scale. The calendar popup
   can list up to 8 task rows at once (see stats.js's POPUP_TASK_LIMIT), each
   with its own title + project/assignee line, so the narrow 320px base
   width forced enough wrapping per row to make the whole popup grow
   uncomfortably tall; wider + a smaller type scale (~80% of the base
   classes' sizes) fixes that without touching Dashboard's popup at all. */
/* min() against 100vw, not a bare 440px -- showPopup()'s positioning logic
   (public/js/stats.js) only ever moves the popup's `left`, it never shrinks
   its width, so a bare fixed 440px would overflow off-screen on any
   viewport narrower than ~456px (common phone widths) regardless of how
   far left the popup gets pinned. overflow-wrap guards the same narrow
   case for a single long unbroken token (a project name, a ticket number)
   that would otherwise force the popup wider than even this cap allows. */
.calendar-popup { max-width: min(440px, calc(100vw - 16px)); overflow-wrap: anywhere; }
.calendar-popup .task-popup-head { font-size: 0.75rem; margin-bottom: calc(4 * var(--space-unit)); }
.calendar-popup .task-popup-date { font-size: 0.55rem; }
/* A plain block container for the calendar popup's task-row list, used
   INSTEAD OF .task-popup-body (not alongside it) -- .task-popup-body is
   built for Dashboard's popup, which renders one paragraph of plain text
   truncated to 6 lines via display:-webkit-box + -webkit-line-clamp:6.
   Reusing that class for a LIST of row <div>s instead, overriding display
   back to block inline, still left every row computing an inflated ~196px
   height in testing. Most likely cause: .task-popup-body also sets
   `white-space: pre-wrap`, which was never overridden alongside display --
   pre-wrap preserves the literal newlines/indentation whitespace sitting
   between each row <div> in the template-literal markup as visible blank
   lines, which a -webkit-line-clamp-oriented ancestor could well be
   stretching to fill. Rather than chase down and override every property
   .task-popup-body sets that this list doesn't want, a dedicated class
   with none of them avoids the whole class of "did I override enough of
   the base class's properties" bug. */
.calendar-popup-list { color: var(--color-text); }

/* — stats: calendar — */
.calendar-weekdays {
  display: grid; grid-template-columns: repeat(7, 1fr); gap: calc(4 * var(--space-unit));
  font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.06em;
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
  padding: 0 calc(2 * var(--space-unit)) calc(6 * var(--space-unit)); text-align: center;
}
.calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: calc(4 * var(--space-unit)); }
.calendar-day {
  min-height: 68px; border-radius: 8px; padding: calc(6 * var(--space-unit));
  background: color-mix(in srgb, var(--color-text) 4%, transparent);
  display: flex; flex-direction: column; gap: calc(4 * var(--space-unit));
  cursor: default;
}
/* Focusable (tabindex="0" in the markup) so the same popup keyboard users
   get via Tab+focus, not just mouse hover -- see .task-popup's own note
   about being pointer-events:none; this reuses that exact mechanism, just
   triggered by focus/blur in addition to mouseenter/mouseleave. */
.calendar-day:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }
.calendar-day.other-month { opacity: 0.35; }
.calendar-day.today { box-shadow: inset 0 0 0 1px var(--color-accent); }
.calendar-day-num { font-size: 0.75rem; font-weight: 600; color: var(--color-text); }
.calendar-day-dots { display: flex; flex-wrap: wrap; gap: 3px; }
.calendar-day-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--color-accent); flex: none; }
.calendar-day-dot.done { background: color-mix(in srgb, var(--color-text) 35%, transparent); }

.milestone-row { display: flex; align-items: center; justify-content: space-between; padding: calc(8 * var(--space-unit)) calc(6 * var(--space-unit)); }
.milestone-row .milestone-title { font-size: 0.875rem; }
.milestone-row .milestone-meta { display: flex; align-items: center; gap: calc(10 * var(--space-unit)); }
.milestone-row .milestone-date { font-size: 0.8125rem; font-weight: 600; color: var(--color-text); }

.priority-badge {
  display: inline-flex; align-items: center; font-size: 0.6875rem;
  padding: calc(3 * var(--space-unit)) calc(9 * var(--space-unit)); border-radius: 6px; font-weight: 500; flex: none; white-space: nowrap;
}
/* Jira ticket badge -- shares the priority badge's compact pill shape/size
   so the two sit at the same visual weight next to each other, but styled
   as a real link (accent color, underline only on hover/focus) since unlike
   the priority badge this is actually clickable. */
.ticket-link {
  display: inline-flex; align-items: center; font-size: 0.6875rem;
  padding: calc(3 * var(--space-unit)) calc(9 * var(--space-unit)); border-radius: 6px; font-weight: 500; flex: none; white-space: nowrap;
  background: color-mix(in srgb, var(--color-accent) 14%, transparent); color: var(--color-accent);
  text-decoration: none;
}
.ticket-link:hover, .ticket-link:focus-visible { text-decoration: underline; }
.empty-state { font-size: 0.8125rem; opacity: .5; margin: 0; }

/* — stat strip (Dashboard + Report) + overdue section — */
.stat-strip { display: flex; gap: calc(16 * var(--space-unit)); margin-bottom: calc(32 * var(--space-unit)); }
.stat-card {
  flex: 1; background: var(--color-surface); border: 1px solid var(--color-divider);
  border-radius: 10px; padding: calc(16 * var(--space-unit)) calc(18 * var(--space-unit)); display: flex; flex-direction: column; gap: calc(4 * var(--space-unit));
}
.stat-card .stat-label {
  font-size: 0.6875rem; letter-spacing: .06em; text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
}
.stat-card .stat-value { font-family: var(--font-heading); font-size: 1.625rem; font-weight: 500; }
.stat-card .stat-value.overdue { color: var(--danger); }

.overdue-section { margin-bottom: calc(36 * var(--space-unit)); }
.overdue-heading { display: flex; align-items: center; gap: calc(8 * var(--space-unit)); margin-bottom: calc(14 * var(--space-unit)); }
.overdue-heading .dot { width: 6px; height: 6px; border-radius: 50%; background: #e0605c; }
.overdue-heading h6 { margin: 0; color: var(--danger); }
.overdue-card { border: 1px solid rgba(224,96,92,0.45); gap: 0; padding: calc(8 * var(--space-unit)); display: flex; flex-direction: column; gap: calc(4 * var(--space-unit)); }

/* — history-only: all-projects overview table — */
.overview-table th.type-col { text-align: center; }
.overview-table .type-col-head { display: inline-flex; align-items: center; justify-content: center; gap: calc(6 * var(--space-unit)); }
.overview-table .project-name-cell { font-weight: 600; white-space: nowrap; color: #ffffff; }
.overview-table td.count-cell { text-align: center; }
.overview-table .count-btn {
  display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
  min-width: 2.75em; padding: calc(5 * var(--space-unit)) calc(10 * var(--space-unit)); border-radius: 6px;
  border: 1px solid transparent; background: transparent; cursor: pointer;
  font-family: var(--font-heading); font-weight: 500; font-size: 0.9375rem;
  color: color-mix(in srgb, var(--color-text) 32%, transparent);
  transition: background .15s, border-color .15s, color .15s;
}
/* Secondary "할일 2/5" line under the entry count -- only rendered when the
   info type actually has linked tasks, so it never shows for task-less rows. */
.overview-table .count-sub {
  font-family: var(--font-body); font-weight: 400; font-size: 0.625rem;
  color: color-mix(in srgb, var(--color-text) 45%, transparent);
}
/* Non-zero counts are lifted to full white so a glance across the row shows
   exactly which categories actually have records -- zero stays dim so it
   doesn't visually compete with real data. */
.overview-table .count-btn.has-count { color: #ffffff; }
/* A cell with an open (non-DONE) task carrying a due date is colored by the
   most urgent one -- overdue beats due-today beats due-later, so a cell
   spanning many entries/tasks still resolves to one unambiguous color. Rules
   are ordered after .has-count (same 2-class specificity) so they win the
   white-vs-color tie. See .active.urgency-* below for how selection and
   urgency color combine once a cell is clicked. Overdue/today are also bold
   (future/green isn't) -- both are meant to grab attention at a glance, and
   weight gives a second cue beyond color alone. */
.overview-table .count-btn.urgency-overdue { color: var(--urgency-overdue); font-weight: 700; }
.overview-table .count-btn.urgency-today { color: var(--warning); font-weight: 700; }
.overview-table .count-btn.urgency-future { color: var(--success); }
.overview-table .count-btn:hover { background: color-mix(in srgb, var(--color-text) 8%, transparent); }
.overview-table .count-btn.active {
  border-color: var(--color-accent); color: var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 14%, transparent);
}
/* Selection border/background always shows, but urgency color should keep
   telling you *why* a cell matters even after clicking into it -- otherwise
   the moment you'd most want to see "this is overdue" is exactly when
   clicking it hides that signal. Higher specificity (4 classes) than either
   .active or .urgency-* alone, so this wins regardless of source order. */
.overview-table .count-btn.active.urgency-overdue { color: var(--urgency-overdue); font-weight: 700; }
.overview-table .count-btn.active.urgency-today { color: var(--warning); font-weight: 700; }
.overview-table .count-btn.active.urgency-future { color: var(--success); }

/* — history/input: entry cards — */
.entry-row {
  padding: calc(16 * var(--space-unit)) calc(20 * var(--space-unit)); border-bottom: 1px solid var(--color-divider);
  display: flex; flex-direction: column; gap: calc(10 * var(--space-unit));
}
.entry-row:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }
.entry-row:last-child { border-bottom: none; }
.entry-row-head { display: flex; align-items: center; gap: calc(10 * var(--space-unit)); }
.entry-row-body { font-size: 0.8125rem; opacity: .8; }
.entry-tasks {
  display: flex; flex-direction: column; gap: calc(4 * var(--space-unit));
  background: var(--color-bg); border: 1px solid var(--color-divider);
  border-radius: 8px; padding: calc(6 * var(--space-unit)) calc(4 * var(--space-unit));
}

/* — input page: parse preview — */
.preview-field { display: flex; align-items: center; min-height: 36px; padding: calc(6 * var(--space-unit)) calc(10 * var(--space-unit)); background: var(--color-bg); border: 1px solid var(--color-divider); border-radius: var(--radius-md); font-size: 0.875rem; }
.parsed-todo-row {
  display: flex; align-items: center; gap: calc(8 * var(--space-unit)); font-size: 0.8125rem;
  background: var(--color-bg); border: 1px solid var(--color-divider);
  border-radius: 6px; padding: calc(7 * var(--space-unit)) calc(10 * var(--space-unit));
}

/* — inline edit forms (reused across input/history/manage) — */
.edit-form { padding: calc(16 * var(--space-unit)) calc(20 * var(--space-unit)); display: flex; flex-direction: column; gap: calc(12 * var(--space-unit)); }
.row-actions { display: flex; gap: calc(8 * var(--space-unit)); }

/* — toast — */
.toast {
  position: fixed; bottom: 20px; right: 20px;
  background: var(--color-surface); color: var(--color-text);
  border: 1px solid var(--color-divider);
  padding: calc(10 * var(--space-unit)) calc(16 * var(--space-unit)); border-radius: var(--radius-md);
  font-size: 0.8125rem; opacity: 0; transform: translateY(10px);
  transition: all .2s ease; pointer-events: none; box-shadow: var(--shadow-md);
}
.toast.show { opacity: 1; transform: translateY(0); }

/* — report page — */
.report-header-row {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: calc(16 * var(--space-unit)); margin-bottom: calc(24 * var(--space-unit));
}
.rate-bar {
  display: flex; height: 26px; border-radius: 8px; overflow: hidden;
  background: color-mix(in srgb, var(--color-text) 8%, transparent);
}
/* print-color-adjust: some browsers strip background colors by default when
   printing (to save ink), which would otherwise turn this bar blank/white
   in the exact PDF export this page exists to produce. */
.rate-bar-ontime { background: var(--success); print-color-adjust: exact; -webkit-print-color-adjust: exact; }
.rate-bar-late { background: var(--danger); print-color-adjust: exact; -webkit-print-color-adjust: exact; }
.rate-bar-legend {
  display: flex; align-items: center; gap: calc(16 * var(--space-unit));
  margin-top: calc(10 * var(--space-unit)); font-size: 0.8125rem;
}
.rate-swatch { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: calc(4 * var(--space-unit)); vertical-align: -1px; }
.rate-swatch.ontime { background: var(--success); }
.rate-swatch.late { background: var(--danger); }
/* Static text shown only when printing (see @media print below) --
   .no-print's live prev/next controls aren't meaningful once "printed", so
   the currently-viewed period needs its own always-static label instead. */
.print-only { display: none; }

/* ══════════════════════════════════════════════════════════════════════
   모바일 보기 — an explicit, manually-toggled layout tuned for a phone
   (iPhone 13 Pro: 390×844 CSS px, notch + home-indicator safe areas,
   touch-only input) rather than a fixed-width desktop screen. See
   js/shared.js's setMobileView()/renderNav() for the switch itself and the
   class comment there for why this is a separate toggle from the existing
   @media(max-width:700px) rules below (a width breakpoint reacts to a
   resized window; this also covers safe-area insets, ≥44px tap targets,
   and tap-vs-hover interaction, none of which fall out of width alone).
   ══════════════════════════════════════════════════════════════════════ */

/* Sidebar becomes a horizontal top bar, same layout as the narrow-window
   media query below -- duplicated here (not shared via a common class)
   since the two mechanisms are conceptually independent triggers that can
   each be on/off regardless of the other (a desktop browser resized narrow
   without 모바일 보기 toggled, or 모바일 보기 toggled on a wide desktop
   window to preview it) and are simple enough not to be worth factoring
   through a preprocessor just to avoid the repetition. env(safe-area-*)
   uses 0px when the browser doesn't support it (older/non-notched devices),
   so this is harmless outside an actual notch/home-indicator screen. */
.mobile-view body.app { flex-direction: column; }
.mobile-view .sidebar {
  flex-direction: row; align-items: center; width: 100%; min-width: 0; min-height: auto;
  height: auto; position: static; overflow-x: auto;
  padding: calc(10 * var(--space-unit)) calc(20 * var(--space-unit));
  padding-top: calc(10 * var(--space-unit) + env(safe-area-inset-top));
  padding-left: calc(20 * var(--space-unit) + env(safe-area-inset-left));
  padding-right: calc(20 * var(--space-unit) + env(safe-area-inset-right));
}
.mobile-view .sidebar-brand { margin: 0 calc(24 * var(--space-unit)) 0 0; }
.mobile-view .view-toggle { margin-top: 0; margin-left: auto; padding: 0 calc(8 * var(--space-unit)); }
.mobile-view .view-toggle + .view-toggle { margin-left: 0; }
.mobile-view .view-toggle-label { display: none; }
/* The bottom-most content on the page (last task row, save button, etc.)
   otherwise sits flush against the home-indicator gesture bar. */
.mobile-view main { padding: calc(20 * var(--space-unit)) calc(16 * var(--space-unit)) calc(32 * var(--space-unit)); }
.mobile-view main { padding-bottom: calc(32 * var(--space-unit) + env(safe-area-inset-bottom)); }
.mobile-view main > .wrap { max-width: none; }
.mobile-view h1 { font-size: 1.375rem; }
.mobile-view .page-subtitle { max-width: none; }

/* Two-column layouts (Input page's editor/preview split, Dashboard's
   milestone pair, Manage's add-project fields, ...) stack -- there's no
   375-390px-wide screen these read as two real columns on. !important
   because Input page's editor/preview split sets its own inline
   grid-template-columns (a wider-screen-only 1.1fr/0.9fr ratio), and an
   inline style otherwise beats any class selector regardless of specificity. */
.mobile-view .grid-2 { grid-template-columns: 1fr !important; }
/* Manage page's symbol-settings row is a bare `display:grid` div with no
   class of its own (see public/manage.html) -- .grid-cols-5 was added
   there specifically so this has something to target; 5 real columns of
   short symbol inputs would be unreadably squeezed on a 390px screen. */
.mobile-view .grid-cols-5 { grid-template-columns: 1fr !important; }

/* Manage page's tab switcher: 4 tabs' worth of Korean labels is wider than
   the screen. overflow:hidden (the default, fine on a wide screen where it
   never actually clips anything) would silently hide the tabs it can't fit
   instead of wrapping them onto a second line (there's no natural place to
   wrap a segmented control) -- overflow-x:auto keeps every tab reachable
   via a horizontal swipe instead. */
.mobile-view .seg { overflow-x: auto; max-width: 100%; }

/* — touch targets — Apple HIG recommends ~44×44pt; most controls here were
   sized for a mouse pointer's precision, not a fingertip's. */
.mobile-view .btn { min-height: 44px; }
.mobile-view .btn-icon { width: 44px; height: 44px; }
.mobile-view .btn-sm { min-height: 40px; }
.mobile-view .input,
.mobile-view select,
.mobile-view input[type=text],
.mobile-view input[type=date],
.mobile-view input[type=number] { min-height: 44px; font-size: 1rem; }
/* 1rem (not the default 0.875rem) specifically to stay clear of iOS
   Safari's own "font-size below 16px zooms the page in on focus" behavior
   -- this app's root is already 130% of the browser default (≈20.8px), so
   0.875rem (≈18.2px) was already just above that 16px line, but this keeps
   it unambiguous rather than resting on a coincidence of the base scale. */
/* Keep the familiar 36×20 visual track, but put it inside a real 44×44
   interactive box. A transform only scales the old 36×20 box to 45×25,
   which still leaves a fingertip-sized vertical miss area. */
.mobile-view .switch { width: 44px; height: 44px; }
.mobile-view .switch-track { inset: 12px 4px; }
.mobile-view .switch-thumb { top: 13px; left: 5px; }
/* A native checkbox's default browser size (~13-18px) is well under a
   comfortable tap target, and unlike ordinary elements, most browsers
   don't reliably grow a native appearance:checkbox control's actual hit
   box via padding (tested: padding+content-box here measured SMALLER than
   the untouched default, not bigger) -- explicit width/height is the
   approach that actually works cross-browser. accent-color keeps it
   themed consistently with the bigger box instead of an oddly-scaled
   native tick mark. */
.mobile-view .task-done-toggle {
  width: 24px; height: 24px; accent-color: var(--color-accent); margin-right: calc(4 * var(--space-unit));
}
/* Same reasoning as the checkbox above: +1/+2/+3 measured ~27px wide in
   testing (each label is only one or two characters), well under a
   comfortable tap width even with the row's own gap between buttons. */
.mobile-view .task-shift-btn { padding-left: calc(12 * var(--space-unit)); padding-right: calc(12 * var(--space-unit)); }

/* — dashboard: task rows — flex-wrap + the title claiming a full 100%
   flex-basis is enough on its own to reflow the row onto three lines
   (checkbox/priority/ticket, then the title alone, then project/who/date/
   buttons) without touching the elements' DOM order: an item with
   flex-basis:100% always starts a fresh line (nothing else fits beside it)
   and always pushes whatever comes after it onto another fresh line too. */
.mobile-view .task-row { flex-wrap: wrap; row-gap: calc(8 * var(--space-unit)); padding: calc(10 * var(--space-unit)); }
.mobile-view .task-row .task-title { flex: 1 1 100%; font-size: 0.9375rem; }
.mobile-view .task-edit-row-fields { flex-direction: column; }
.mobile-view .task-edit-row-fields .input,
.mobile-view .task-edit-row-fields select { min-width: 0; }

/* — tables — a bare overflow-x:auto + nowrap on the <table> itself (no
   wrapper element needed) turns any table into a horizontally-scrollable
   strip instead of squeezing every column unreadably thin. Some pages
   already wrap their table in its own overflow-x:auto div (harmless
   double-scroll-container, not a bug) but Manage's tables never got one,
   so this also fixes those without editing four separate HTML files. */
.mobile-view .table { display: block; overflow-x: auto; white-space: nowrap; }

/* — task hover popup — position:fixed (not the default absolute, which is
   relative to the DOCUMENT and shifts with scroll) keeps it anchored to the
   viewport itself. Combined with the tap-to-open/tap-away-to-close handlers
   in dashboard.js (there's no mouse hover on a touch screen), a popup drawn
   near the tapped row can otherwise end up partially under a thumb or
   awkwardly close to the screen edge on a 390px-wide viewport -- pinning it
   near the bottom of the screen instead keeps it in a predictable, always-
   reachable spot regardless of which row was tapped. */
.mobile-view .task-popup {
  position: fixed; left: calc(8 * var(--space-unit)) !important; right: calc(8 * var(--space-unit));
  bottom: calc(12 * var(--space-unit) + env(safe-area-inset-bottom)); top: auto !important;
  max-width: none; width: auto;
}

/* PDF export is just the browser's native print dialog (Chrome/Edge's own
   "Save as PDF" destination) aimed at a dedicated print stylesheet -- no
   client-side PDF library, keeping this app's zero-external-dependency
   posture intact. Every color below is a :root custom-property override
   rather than a per-component rule, so the whole page repaints consistently
   through the same var(--color-*) references every component already uses,
   the same way the rest of this design system works. */
@media print {
  :root {
    --color-bg: #ffffff;
    --color-surface: #ffffff;
    --color-text: #1a1a1a;
    --color-divider: #d8d8d8;
    --color-accent: #5b4fb0;
  }
  body { background: #ffffff; }
  .sidebar, .no-print, .toast, .task-popup { display: none !important; }
  .print-only { display: inline; }
  main { padding: 0; }
  main > .wrap { max-width: none; }
  .card, .stat-card { break-inside: avoid; }
}
