body,
body * {
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

/* Keep taps from producing Safari's grey flash or triggering double-tap zoom. */
html,
body {
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

/* ── Skeleton loading state ──────────────────────────────────────────────────── */
:root {
  --skeleton-base: #f0f0f0;
  --skeleton-sheen: #fafafa;
  --skeleton-radius: 10px;
}

/*
 * One diagonal light beam sweeps the whole invoice. `background-attachment: fixed`
 * anchors every bar's gradient to the same (viewport) coordinate space, so the
 * highlight's position tracks each bar's place on the page — every bar shares the
 * one beam instead of shimmering on its own clock. Linear glide + a brief rest.
 */
@keyframes sheen-sweep {
  0% {
    background-position: 110% 0;
  }
  80% {
    background-position: -10% 0;
  }
  100% {
    background-position: -10% 0;
  }
}

@keyframes sync-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Download-in-progress: the arrow flies down and away, then re-enters from the
   top and drops back to centre, looping. The 40%→41% jump is fully transparent. */
@keyframes download-fly {
  0%,
  100% {
    transform: translateY(0);
    opacity: 1;
  }
  40% {
    transform: translateY(170%);
    opacity: 0;
  }
  41% {
    transform: translateY(-170%);
    opacity: 0;
  }
}

/* Currency switcher has no meaningful value until data loads — hide it so it
   doesn't flash as an empty black pill in the menu. */
body.is-loading #menu-currency-switcher {
  display: none;
}

/* Shared pulse surface for every placeholder */
body.is-loading #invoice-number,
body.is-loading #issue-date,
body.is-loading #issuer-name,
body.is-loading #issuer-address,
body.is-loading #issuer-email,
body.is-loading #issuer-taxid,
body.is-loading #billing-period,
body.is-loading #due-date,
body.is-loading #client-name,
body.is-loading #client-address,
body.is-loading #client-email,
body.is-loading #client-taxid,
body.is-loading #payment-details,
body.is-loading #total-time-value,
body.is-loading #subtotal-value,
body.is-loading #tax-value,
body.is-loading #total-value,
body.is-loading .sk-bar {
  border-radius: var(--skeleton-radius);
  background: linear-gradient(
    115deg,
    var(--skeleton-base) 35%,
    var(--skeleton-sheen) 50%,
    var(--skeleton-base) 65%
  );
  background-size: 200% 100%;
  background-attachment: fixed;
  animation: sheen-sweep 1s linear infinite;
  color: transparent !important;
}

/* Text-field placeholder sizing */
body.is-loading #invoice-number,
body.is-loading #issue-date,
body.is-loading #issuer-name,
body.is-loading #issuer-address,
body.is-loading #issuer-email,
body.is-loading #issuer-taxid,
body.is-loading #billing-period,
body.is-loading #due-date,
body.is-loading #client-name,
body.is-loading #client-address,
body.is-loading #client-email,
body.is-loading #client-taxid,
body.is-loading #payment-details,
body.is-loading #total-time-value,
body.is-loading #subtotal-value,
body.is-loading #tax-value,
body.is-loading #total-value {
  min-height: 11px;
  min-width: 64px;
}

body.is-loading #invoice-number,
body.is-loading #issue-date {
  min-width: 88px;
}

body.is-loading #issuer-name,
body.is-loading #client-name {
  min-width: 128px;
  min-height: 13px;
}

body.is-loading #total-value {
  min-width: 88px;
  min-height: 13px;
}

body.is-loading #total-time-value,
body.is-loading #subtotal-value,
body.is-loading #tax-value,
body.is-loading #total-value {
  width: 88px;
  min-width: 88px;
}

body.is-loading #issuer-address,
body.is-loading #issuer-email,
body.is-loading #issuer-taxid,
body.is-loading #client-address,
body.is-loading #client-email,
body.is-loading #client-taxid {
  display: none;
}

body.is-loading #issuer-name,
body.is-loading #client-name {
  width: var(--issuer-block-width);
  max-width: var(--issuer-block-width);
}

body.is-loading #client-name {
  width: var(--issuer-block-width);
  max-width: var(--issuer-block-width);
}

body.is-loading #payment-details {
  width: calc(
    var(--a4-css-width) - var(--invoice-page-inset) * 2
      - var(--client-block-width)
      - var(--invoice-section-gap)
  );
  max-width: calc(
    var(--a4-css-width) - var(--invoice-page-inset) * 2
      - var(--client-block-width)
      - var(--invoice-section-gap)
  );
}

/* Line-item placeholder rows — mirror the real grid so columns line up */
.skeleton-row {
  display: none !important;
}
body.is-loading .skeleton-row {
  display: flex !important;
}
body.is-loading .skeleton-row .table-header-row {
  align-items: center;
}
body.is-loading .sk-bar {
  display: flex;
  align-items: center;
  height: var(--item-text-line-height);
  background: none;
  animation: none;
}
body.is-loading .sk-bar::before {
  content: "";
  display: block;
  width: 100%;
  height: 11px;
  border-radius: var(--skeleton-radius);
  background: linear-gradient(
    115deg,
    var(--skeleton-base) 35%,
    var(--skeleton-sheen) 50%,
    var(--skeleton-base) 65%
  );
  background-size: 200% 100%;
  background-attachment: fixed;
  animation: sheen-sweep 1s linear infinite;
}
body.is-loading .sk-desc {
  grid-column: 1 / span 1;
  grid-row: 1 / span 1;
}
body.is-loading .sk-time {
  grid-column: 2 / span 1;
  grid-row: 1 / span 1;
}
body.is-loading .sk-amount {
  grid-column: 3 / span 1;
  grid-row: 1 / span 1;
}
body.is-loading .sk-time,
body.is-loading .sk-amount {
  justify-self: end;
}
body.is-loading .sk-time {
  width: 56px;
}
body.is-loading .sk-amount {
  width: 88px;
}

/* Organic description widths + a gentle row-by-row stagger */
body.is-loading .skeleton-row:nth-child(1) .sk-desc {
  width: 82%;
}
body.is-loading .skeleton-row:nth-child(2) .sk-desc {
  width: 64%;
}
body.is-loading .skeleton-row:nth-child(3) .sk-desc {
  width: 90%;
}
body.is-loading .skeleton-row:nth-child(4) .sk-desc {
  width: 52%;
}
body.is-loading .skeleton-row:nth-child(5) .sk-desc {
  width: 73%;
}
body.is-loading .skeleton-row:nth-child(6) .sk-desc {
  width: 58%;
}
body.is-loading .skeleton-row:nth-child(7) .sk-desc {
  width: 86%;
}
body.is-loading .skeleton-row:nth-child(8) .sk-desc {
  width: 68%;
}
/* Reduced-motion: hold a steady, slightly-dimmed fill instead of pulsing */
@media (prefers-reduced-motion: reduce) {
  body.is-loading #invoice-number,
  body.is-loading #issue-date,
  body.is-loading #issuer-name,
  body.is-loading #issuer-address,
  body.is-loading #issuer-email,
  body.is-loading #issuer-taxid,
  body.is-loading #billing-period,
  body.is-loading #due-date,
  body.is-loading #client-name,
  body.is-loading #client-address,
  body.is-loading #client-email,
  body.is-loading #client-taxid,
  body.is-loading #payment-details,
  body.is-loading #total-time-value,
  body.is-loading #subtotal-value,
  body.is-loading #tax-value,
  body.is-loading #total-value,
  body.is-loading .sk-bar::before {
    animation: none;
    background: var(--skeleton-base);
    opacity: 0.85;
  }
  .download-progress-bar,
  .menu > .switcher-indicator::after,
  .btn-download.is-downloading .icon-download,
  .menu #menu-sync .icon-sync.spinning {
    animation: none;
    transition: none;
  }
}
body {
  background: var(--surface);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 50px 0px
    150px;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
.mockup {
  position: relative;
  width: var(--a4-css-width);
  min-height: var(--a4-css-height);
}
.invoice {
  background: var(--paper);
  width: var(--a4-css-width);
  height: var(--a4-css-height);
  flex-shrink: 0;
  box-shadow: var(--shadow-card);
  padding: var(--invoice-page-inset) 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
}

/* ── Shared flex-column section layout ───────────────────────────────────────── */
.top {
  display: flex;
  flex-direction: column;
  gap: var(--invoice-section-gap);
  align-items: flex-start;
  justify-content: flex-start;
  align-self: stretch;
  flex: 1;
  min-height: 0;
  position: relative;
}
.invoice-title {
  display: flex;
  flex-direction: row;
  padding: 0 var(--invoice-page-inset);
  align-items: flex-end;
  justify-content: space-between;
  align-self: stretch;
  flex-shrink: 0;
  position: relative;
}
.left-padding-fix {
  flex-shrink: 0;
  width: var(--invoice-title-width);
  height: var(--invoice-title-height);
  position: relative;
}
.invoice-heading {
  color: var(--ink);
  text-align: left;
  font-family: var(--font);
  font-size: var(--text-xl);
  line-height: var(--leading-xl);
  letter-spacing: var(--tracking-tighter);
  font-weight: 700;
  position: absolute;
  left: var(--invoice-heading-offset);
  top: 0;
}
.invoice-meta {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: flex-end;
  justify-content: flex-end;
  align-self: stretch;
  flex-shrink: 0;
  position: relative;
}
.invoice-number {
  color: var(--ink);
  text-align: left;
  font-family: var(--font);
  font-size: var(--text-sm);
  line-height: var(--leading-sm);
  font-weight: 600;
  position: relative;
}
.issue-date {
  color: var(--ink);
  text-align: left;
  font-family: var(--font);
  font-size: var(--text-sm);
  line-height: var(--leading-sm);
  font-weight: 400;
  position: relative;
}
.invoice-body {
  display: flex;
  flex-direction: column;
  gap: var(--invoice-content-gap);
  padding: 0 var(--invoice-page-inset);
  align-items: flex-start;
  justify-content: flex-start;
  align-self: stretch;
  flex-shrink: 0;
  position: relative;
}

/* ── Dividers ─────────────────────────────────────────────────────────────── */
.divider {
  background: var(--ink);
  align-self: stretch;
  flex-shrink: 0;
  height: 1px;
  position: relative;
}
.row-divider {
  background: var(--muted);
  align-self: stretch;
  flex-shrink: 0;
  height: 1px;
  position: relative;
}
.total-divider {
  background: var(--ink);
  flex-shrink: 0;
  width: var(--totals-width);
  height: 1px;
  position: relative;
}

/* ── Parties section ─────────────────────────────────────────────────────── */
.parties {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  justify-content: space-between;
  align-self: stretch;
  flex-shrink: 0;
  position: relative;
}
.issued-by-block {
  display: flex;
  flex-direction: column;
  gap: var(--invoice-small-gap);
  align-items: flex-start;
  justify-content: flex-start;
  flex-shrink: 0;
  position: relative;
}

/* Section labels (gray caps) */
.issued-by,
.billing-period,
.due-date,
.billed-to,
.payment-details {
  color: var(--muted);
  font-family: var(--font);
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  font-weight: 600;
  text-transform: uppercase;
  position: relative;
}
.issued-by {
  cursor: default;
  user-select: none;
}
.billing-period,
.due-date {
  text-align: right;
}

/* Shared info-block layout */
.issuer-block,
.client-block,
.payment-block {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;
  flex-shrink: 0;
  min-width: var(--info-block-min-width);
  max-width: var(--info-block-max-width);
  position: relative;
}
.issuer-block {
  width: var(--issuer-block-width);
  gap: 4px;
}
.client-block {
  width: var(--client-block-width);
  gap: var(--invoice-small-gap);
  align-items: stretch;
  box-sizing: border-box;
  min-width: 0;
}
.payment-block {
  width: var(--payment-block-width);
  gap: var(--invoice-small-gap);
  align-items: stretch;
  box-sizing: border-box;
  min-width: 0;
}

/* Block width from JS (layout.*BlockWidth); all content spans the full column */
.client-block > :not(.layout-resize-handle),
.payment-block > :not(.layout-resize-handle) {
  width: 100%;
  max-width: 100%;
  min-width: 0;
  box-sizing: border-box;
}
.client-block .client-details,
.client-block .client-details > * {
  width: 100%;
  max-width: 100%;
  min-width: 0;
  box-sizing: border-box;
}

/* Name fields — issuer 14px; billed-to company name 10px (detail size) */
.issuer-name,
.client-name {
  color: var(--ink);
  text-align: left;
  font-family: var(--font);
  letter-spacing: var(--tracking-tight);
  position: relative;
  align-self: stretch;
}
.issuer-name {
  font-size: var(--text-sm);
  line-height: var(--leading-sm);
  font-weight: 600;
}
.client-name {
  font-size: var(--text-xs);
  line-height: var(--item-text-line-height);
  font-weight: 600;
}

/* Shared detail-line layout (issuer + client contact info) */
.issuer-details,
.client-details {
  display: flex;
  flex-direction: column;
  gap: 0;
  align-items: flex-start;
  justify-content: flex-start;
  align-self: stretch;
  flex-shrink: 0;
  position: relative;
}
.client-details {
  align-items: stretch;
  flex-shrink: 1;
}
.client-block .billed-to,
.client-block .client-name,
.client-block .client-address,
.client-block .client-email,
.client-block .client-taxid {
  align-self: stretch;
  overflow-wrap: anywhere;
}

/* All contact-info lines (issuer + client + payment) */
.issuer-address,
.issuer-email,
.issuer-taxid,
.client-address,
.client-email,
.client-taxid,
.payment-info {
  color: var(--ink);
  text-align: left;
  font-family: var(--font);
  font-size: var(--text-xs);
  line-height: var(--item-text-line-height);
  font-weight: 400;
  position: relative;
  align-self: stretch;
  white-space: pre-line;
}

/* Shared date-block layout */
.date-blocks {
  display: flex;
  flex-direction: row;
  gap: var(--invoice-section-gap);
  align-items: flex-start;
  justify-content: flex-end;
  flex-shrink: 0;
  position: relative;
}
.billing-period-block,
.due-date-block {
  display: flex;
  flex-direction: column;
  gap: var(--invoice-small-gap);
  align-items: flex-end;
  justify-content: flex-start;
  flex-shrink: 0;
  position: relative;
}
.due-date-block {
  width: var(--due-date-block-width);
}

.billing-period-value,
.due-date-value {
  color: var(--ink);
  text-align: right;
  font-family: var(--font);
  font-size: var(--text-xs);
  line-height: var(--leading-sm);
  letter-spacing: var(--tracking-tight);
  position: relative;
}
.billing-period-value {
  font-weight: 400;
}
.due-date-value {
  font-weight: 600;
}

/* ── Items table ─────────────────────────────────────────────────────────── */
.items-table {
  display: flex;
  flex-direction: column;
  gap: var(--invoice-section-gap);
  align-items: flex-start;
  justify-content: flex-start;
  align-self: stretch;
  flex: 1;
  min-height: 0;
  position: relative;
}
.price-list {
  display: flex;
  flex-direction: column;
  gap: 0;
  align-items: flex-start;
  justify-content: flex-start;
  align-self: stretch;
  flex: 0 1 auto;
  min-height: 0;
  overflow: hidden;
  position: relative;
}
/* Bottom fade where the last row meets the totals — hidden when fully scrolled */
.price-list::after {
  content: "";
  position: absolute;
  left: var(--invoice-content-gap);
  right: var(--invoice-content-gap);
  bottom: 0;
  height: 32px;
  pointer-events: none;
  z-index: 3;
  opacity: 0;
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0), var(--paper));
  transition: opacity var(--motion-speed) var(--ease-soft);
}
.price-list.has-list-fade:not(.is-scrolled-end)::after {
  opacity: 1;
}
#line-items-container {
  flex: 0 1 auto;
  min-height: 0;
  max-height: calc(var(--line-item-row-height) * 10);
  margin: 0 calc(var(--invoice-page-inset) / 2);
  padding: 0 calc(var(--invoice-page-inset) / 2);
  overflow-y: hidden;
  overscroll-behavior-y: contain;
  scrollbar-width: thin;
  align-self: stretch;
}
#line-items-container.is-scrollable {
  overflow-y: auto;
  /* Safari < ~17 won't create a scroll region for a flex item sized by max-height.
     When scrollable there are always >10 rows, so pin a definite height and take it
     out of flex sizing — this gives WebKit a concrete box to scroll. */
  flex: none;
  height: calc(var(--line-item-row-height) * 10);
  -webkit-overflow-scrolling: touch;
}

.table-header {
  display: flex;
  flex-direction: column;
  gap: var(--invoice-icon-gap);
  padding: 0 var(--invoice-page-inset);
  align-items: flex-start;
  justify-content: flex-start;
  align-self: stretch;
  flex-shrink: 0;
  position: relative;
}
.table-header-row {
  align-self: stretch;
  flex-shrink: 0;
  display: grid;
  gap: var(--invoice-content-gap);
  position: relative;
  grid-template-columns:
    minmax(0, 1fr) var(--table-time-width) var(--table-amount-width);
  align-items: baseline;
}
.table-header-row > * {
  min-width: 0;
}
.description,
.col-time,
.amount {
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  font-weight: 600;
  text-transform: uppercase;
  position: relative;
}
.description {
  text-align: left;
  grid-column: 1 / span 1;
  grid-row: 1 / span 1;
}
.col-time {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--invoice-icon-gap);
  grid-column: 2 / span 1;
  grid-row: 1 / span 1;
  cursor: pointer;
}
.amount {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--invoice-icon-gap);
  grid-column: 3 / span 1;
  grid-row: 1 / span 1;
}

/* Reuse hidden time column space — grid width stays the same */
.items-table.time-hidden .item-description {
  grid-column: 1 / span 2;
  z-index: 1;
}
.items-table.time-hidden .item-time {
  pointer-events: none;
}

.btn-eye,
.btn-sort {
  --icon-visibility-delay: var(--motion-speed);
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  display: block;
  cursor: pointer;
  color: var(--ink);
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 13 8'%3E%3Cpath fill='black' d='M6.28906 7.93945C5.67057 7.93945 5.08464 7.86458 4.53125 7.71484C3.98112 7.5651 3.47331 7.36654 3.00781 7.11914C2.54232 6.87174 2.12402 6.59993 1.75293 6.30371C1.38184 6.00749 1.06445 5.70964 0.800781 5.41016C0.540365 5.11068 0.341797 4.83561 0.205078 4.58496C0.0683594 4.33105 0 4.12598 0 3.96973C0 3.81348 0.0683594 3.6084 0.205078 3.35449C0.345052 3.10059 0.545247 2.82552 0.805664 2.5293C1.06608 2.22982 1.38184 1.93197 1.75293 1.63574C2.12402 1.33626 2.54232 1.06283 3.00781 0.81543C3.47656 0.568034 3.986 0.371094 4.53613 0.224609C5.08952 0.0748698 5.67383 0 6.28906 0C6.91732 0 7.50814 0.0748698 8.06152 0.224609C8.61491 0.371094 9.12435 0.568034 9.58984 0.81543C10.0586 1.06283 10.4769 1.33626 10.8447 1.63574C11.2126 1.93197 11.5251 2.22982 11.7822 2.5293C12.0426 2.82552 12.2396 3.10059 12.373 3.35449C12.5098 3.6084 12.5781 3.81348 12.5781 3.96973C12.5781 4.12598 12.5098 4.33105 12.373 4.58496C12.2396 4.83561 12.0443 5.11068 11.7871 5.41016C11.5299 5.70964 11.2158 6.00749 10.8447 6.30371C10.4769 6.59993 10.0602 6.87174 9.59473 7.11914C9.12923 7.36654 8.61816 7.5651 8.06152 7.71484C7.50814 7.86458 6.91732 7.93945 6.28906 7.93945ZM6.28906 6.48438C6.7513 6.48438 7.17285 6.37207 7.55371 6.14746C7.93457 5.9196 8.23893 5.61523 8.4668 5.23438C8.69466 4.85352 8.80859 4.43197 8.80859 3.96973C8.80859 3.50749 8.69466 3.08594 8.4668 2.70508C8.23893 2.32422 7.93457 2.02148 7.55371 1.79688C7.17285 1.56901 6.7513 1.45508 6.28906 1.45508C5.83008 1.45508 5.41016 1.56901 5.0293 1.79688C4.64844 2.02148 4.34408 2.32422 4.11621 2.70508C3.88835 3.08594 3.77441 3.50749 3.77441 3.96973C3.77441 4.43197 3.88835 4.85352 4.11621 5.23438C4.34408 5.61523 4.64844 5.9196 5.0293 6.14746C5.41016 6.37207 5.83008 6.48438 6.28906 6.48438ZM6.28906 4.92676C6.02539 4.92676 5.80078 4.83398 5.61523 4.64844C5.42969 4.45964 5.33691 4.2334 5.33691 3.96973C5.33691 3.70605 5.42969 3.48145 5.61523 3.2959C5.80078 3.1071 6.02539 3.0127 6.28906 3.0127C6.55599 3.0127 6.78223 3.1071 6.96777 3.2959C7.15332 3.48145 7.24609 3.70605 7.24609 3.96973C7.24609 4.2334 7.15332 4.45964 6.96777 4.64844C6.78223 4.83398 6.55599 4.92676 6.28906 4.92676Z'/%3E%3C/svg%3E")
    center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 13 8'%3E%3Cpath fill='black' d='M6.28906 7.93945C5.67057 7.93945 5.08464 7.86458 4.53125 7.71484C3.98112 7.5651 3.47331 7.36654 3.00781 7.11914C2.54232 6.87174 2.12402 6.59993 1.75293 6.30371C1.38184 6.00749 1.06445 5.70964 0.800781 5.41016C0.540365 5.11068 0.341797 4.83561 0.205078 4.58496C0.0683594 4.33105 0 4.12598 0 3.96973C0 3.81348 0.0683594 3.6084 0.205078 3.35449C0.345052 3.10059 0.545247 2.82552 0.805664 2.5293C1.06608 2.22982 1.38184 1.93197 1.75293 1.63574C2.12402 1.33626 2.54232 1.06283 3.00781 0.81543C3.47656 0.568034 3.986 0.371094 4.53613 0.224609C5.08952 0.0748698 5.67383 0 6.28906 0C6.91732 0 7.50814 0.0748698 8.06152 0.224609C8.61491 0.371094 9.12435 0.568034 9.58984 0.81543C10.0586 1.06283 10.4769 1.33626 10.8447 1.63574C11.2126 1.93197 11.5251 2.22982 11.7822 2.5293C12.0426 2.82552 12.2396 3.10059 12.373 3.35449C12.5098 3.6084 12.5781 3.81348 12.5781 3.96973C12.5781 4.12598 12.5098 4.33105 12.373 4.58496C12.2396 4.83561 12.0443 5.11068 11.7871 5.41016C11.5299 5.70964 11.2158 6.00749 10.8447 6.30371C10.4769 6.59993 10.0602 6.87174 9.59473 7.11914C9.12923 7.36654 8.61816 7.5651 8.06152 7.71484C7.50814 7.86458 6.91732 7.93945 6.28906 7.93945ZM6.28906 6.48438C6.7513 6.48438 7.17285 6.37207 7.55371 6.14746C7.93457 5.9196 8.23893 5.61523 8.4668 5.23438C8.69466 4.85352 8.80859 4.43197 8.80859 3.96973C8.80859 3.50749 8.69466 3.08594 8.4668 2.70508C8.23893 2.32422 7.93457 2.02148 7.55371 1.79688C7.17285 1.56901 6.7513 1.45508 6.28906 1.45508C5.83008 1.45508 5.41016 1.56901 5.0293 1.79688C4.64844 2.02148 4.34408 2.32422 4.11621 2.70508C3.88835 3.08594 3.77441 3.50749 3.77441 3.96973C3.77441 4.43197 3.88835 4.85352 4.11621 5.23438C4.34408 5.61523 4.64844 5.9196 5.0293 6.14746C5.41016 6.37207 5.83008 6.48438 6.28906 6.48438ZM6.28906 4.92676C6.02539 4.92676 5.80078 4.83398 5.61523 4.64844C5.42969 4.45964 5.33691 4.2334 5.33691 3.96973C5.33691 3.70605 5.42969 3.48145 5.61523 3.2959C5.80078 3.1071 6.02539 3.0127 6.28906 3.0127C6.55599 3.0127 6.78223 3.1071 6.96777 3.2959C7.15332 3.48145 7.24609 3.70605 7.24609 3.96973C7.24609 4.2334 7.15332 4.45964 6.96777 4.64844C6.78223 4.83398 6.55599 4.92676 6.28906 4.92676Z'/%3E%3C/svg%3E")
    center / contain no-repeat;
  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--motion-speed) var(--ease-soft),
    color var(--motion-speed) var(--ease-soft),
    visibility 0s linear var(--icon-visibility-delay);
}
.btn-sort {
  position: relative;
  overflow: hidden;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath fill='black' d='M3.68066 2.44778L3.76367 3.9468V10.0308C3.76367 10.161 3.72135 10.2684 3.63672 10.3531C3.55208 10.4409 3.44466 10.4849 3.31445 10.4849C3.18099 10.4849 3.07194 10.4409 2.9873 10.3531C2.90267 10.2684 2.86035 10.161 2.86035 10.0308V3.9468L2.94824 2.44778C2.95801 2.32408 2.9987 2.23131 3.07031 2.16946C3.14518 2.10436 3.22656 2.0718 3.31445 2.0718C3.40234 2.0718 3.48372 2.10436 3.55859 2.16946C3.63672 2.23131 3.67741 2.32408 3.68066 2.44778ZM1.26855 4.69387C1.18392 4.78176 1.07812 4.82571 0.951172 4.82571C0.820964 4.82571 0.713542 4.78502 0.628906 4.70364C0.544271 4.619 0.501953 4.50995 0.501953 4.37649C0.501953 4.24954 0.549154 4.13886 0.643555 4.04446L2.97266 1.66653C3.06706 1.56562 3.17936 1.51516 3.30957 1.51516C3.43978 1.51191 3.55371 1.56236 3.65137 1.66653L5.97559 4.04446C6.07324 4.13886 6.12207 4.24954 6.12207 4.37649C6.12207 4.50995 6.07975 4.619 5.99512 4.70364C5.91048 4.78502 5.80306 4.82571 5.67285 4.82571C5.5459 4.82571 5.4401 4.78176 5.35547 4.69387L4.36426 3.66848L3.31445 2.43801L2.25488 3.67337L1.26855 4.69387Z'/%3E%3Cpath fill='black' d='M8.30175 9.10885L8.23633 8.04834V1.96436C8.23633 1.8374 8.27865 1.73161 8.36328 1.64697C8.44792 1.55908 8.55697 1.51514 8.69043 1.51514C8.82064 1.51514 8.92806 1.55908 9.0127 1.64697C9.09733 1.73161 9.13965 1.8374 9.13965 1.96436V8.04834L9.08127 9.10603L9.74512 8.33154L10.7363 7.30127C10.821 7.21663 10.9251 7.17432 11.0488 7.17432C11.179 7.17432 11.2865 7.21663 11.3711 7.30127C11.4557 7.38265 11.498 7.49007 11.498 7.62354C11.498 7.74723 11.4492 7.85628 11.3516 7.95068L9.02734 10.3335C8.92969 10.4344 8.81576 10.4832 8.68555 10.48C8.55534 10.48 8.44303 10.4312 8.34863 10.3335L6.01953 7.95068C5.92839 7.85628 5.88281 7.74723 5.88281 7.62354C5.88281 7.49007 5.9235 7.38265 6.00488 7.30127C6.08952 7.21663 6.19694 7.17432 6.32715 7.17432C6.4541 7.17432 6.5599 7.21663 6.64453 7.30127L7.63086 8.32666L8.30175 9.10885Z'/%3E%3C/svg%3E")
    center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath fill='black' d='M3.68066 2.44778L3.76367 3.9468V10.0308C3.76367 10.161 3.72135 10.2684 3.63672 10.3531C3.55208 10.4409 3.44466 10.4849 3.31445 10.4849C3.18099 10.4849 3.07194 10.4409 2.9873 10.3531C2.90267 10.2684 2.86035 10.161 2.86035 10.0308V3.9468L2.94824 2.44778C2.95801 2.32408 2.9987 2.23131 3.07031 2.16946C3.14518 2.10436 3.22656 2.0718 3.31445 2.0718C3.40234 2.0718 3.48372 2.10436 3.55859 2.16946C3.63672 2.23131 3.67741 2.32408 3.68066 2.44778ZM1.26855 4.69387C1.18392 4.78176 1.07812 4.82571 0.951172 4.82571C0.820964 4.82571 0.713542 4.78502 0.628906 4.70364C0.544271 4.619 0.501953 4.50995 0.501953 4.37649C0.501953 4.24954 0.549154 4.13886 0.643555 4.04446L2.97266 1.66653C3.06706 1.56562 3.17936 1.51516 3.30957 1.51516C3.43978 1.51191 3.55371 1.56236 3.65137 1.66653L5.97559 4.04446C6.07324 4.13886 6.12207 4.24954 6.12207 4.37649C6.12207 4.50995 6.07975 4.619 5.99512 4.70364C5.91048 4.78502 5.80306 4.82571 5.67285 4.82571C5.5459 4.82571 5.4401 4.78176 5.35547 4.69387L4.36426 3.66848L3.31445 2.43801L2.25488 3.67337L1.26855 4.69387Z'/%3E%3Cpath fill='black' d='M8.30175 9.10885L8.23633 8.04834V1.96436C8.23633 1.8374 8.27865 1.73161 8.36328 1.64697C8.44792 1.55908 8.55697 1.51514 8.69043 1.51514C8.82064 1.51514 8.92806 1.55908 9.0127 1.64697C9.09733 1.73161 9.13965 1.8374 9.13965 1.96436V8.04834L9.08127 9.10603L9.74512 8.33154L10.7363 7.30127C10.821 7.21663 10.9251 7.17432 11.0488 7.17432C11.179 7.17432 11.2865 7.21663 11.3711 7.30127C11.4557 7.38265 11.498 7.49007 11.498 7.62354C11.498 7.74723 11.4492 7.85628 11.3516 7.95068L9.02734 10.3335C8.92969 10.4344 8.81576 10.4832 8.68555 10.48C8.55534 10.48 8.44303 10.4312 8.34863 10.3335L6.01953 7.95068C5.92839 7.85628 5.88281 7.74723 5.88281 7.62354C5.88281 7.49007 5.9235 7.38265 6.00488 7.30127C6.08952 7.21663 6.19694 7.17432 6.32715 7.17432C6.4541 7.17432 6.5599 7.21663 6.64453 7.30127L7.63086 8.32666L8.30175 9.10885Z'/%3E%3C/svg%3E")
    center / contain no-repeat;
}
.btn-sort::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--ink);
  opacity: 0;
  transition: opacity var(--motion-speed) var(--ease-soft);
}

/* ── Summary rows ────────────────────────────────────────────────────────── */
.sum {
  display: flex;
  flex-direction: column;
  gap: var(--invoice-block-gap);
  align-items: flex-end;
  justify-content: flex-start;
  flex-shrink: 0;
  width: 100%;
  padding: 0 var(--invoice-page-inset);
  position: relative;
}
.items-table.time-hidden .total-time-row {
  display: none;
}
.subtotal,
.tax,
.total {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  justify-content: space-between;
  flex-shrink: 0;
  width: var(--totals-width);
  position: relative;
}
.subtotal-label,
.tax-label {
  color: var(--ink);
  text-align: right;
  font-family: var(--font);
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  font-weight: 400;
  text-transform: uppercase;
  position: relative;
}
.subtotal-amount,
.tax-amount {
  color: var(--ink);
  text-align: right;
  font-family: var(--font);
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  font-weight: 400;
  position: relative;
  width: var(--table-amount-width);
}
.total-label,
.total-amount {
  color: var(--ink);
  text-align: right;
  font-family: var(--font);
  font-size: var(--text-sm);
  line-height: var(--leading-sm);
  font-weight: 600;
  position: relative;
}
.total-label {
  text-transform: uppercase;
}
.btn-round {
  position: absolute;
  left: 100%;
  top: 0;
  padding-left: 5px;
  display: block;
  cursor: pointer;
  color: var(--muted);
  font-family: var(--font);
  font-size: var(--text-sm);
  line-height: var(--leading-sm);
  font-weight: 400;
  user-select: none;
  opacity: 0;
  pointer-events: none;
  transition:
    opacity var(--motion-speed) var(--ease-soft),
    color var(--motion-speed) var(--ease-soft);
}
.btn-round.is-rounded {
  color: var(--ink);
}
.items-table:hover .btn-round {
  opacity: 1;
  pointer-events: auto;
  transition:
    opacity var(--motion-speed) var(--ease-soft),
    color var(--motion-speed) var(--ease-soft);
}

/* ── Invoice layout (resize / reorder) ───────────────────────────────────── */
.layout-drag-ghost {
  position: fixed;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
.is-resizable {
  position: relative;
  transition: width var(--motion-speed) var(--ease-soft);
}
.layout-resize-handle {
  position: absolute;
  top: 0;
  right: -5px;
  width: 10px;
  height: 100%;
  cursor: ew-resize;
  z-index: 1;
  /* Own the horizontal gesture so a finger drag resizes instead of scrolling. */
  touch-action: none;
}

/* Footer grabber: right edge of block, pill in the gap to the next column */
.layout-resize-handle--between {
  right: auto;
  left: calc(100% + 13px);
  top: 0;
  bottom: 0;
  width: 10px;
  height: auto;
  transform: none;
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-speed) var(--ease-soft);
  transition-delay: 0.2s;
}
.layout-resize-handle--between::before {
  content: "";
  width: 2px;
  height: var(--invoice-content-gap);
  border-radius: 1px;
  background: var(--muted-light);
  flex-shrink: 0;
}
.footer:hover .layout-resize-handle--between,
.footer:focus-within .layout-resize-handle--between,
.invoice-body:hover .layout-resize-handle--between,
body.is-layout-resizing .layout-resize-handle--between {
  opacity: 1;
  pointer-events: auto;
  transition-delay: 0s;
}
/* Touch has no hover to reveal the grabber — keep it visible (and give it a
   fatter hit area). On the reflow layout (≤820px) blocks are full-width and the
   handle is display:none, so this only surfaces on wider touch screens (iPad). */
@media (hover: none) {
  .layout-resize-handle--between {
    opacity: 1;
    pointer-events: auto;
    transition-delay: 0s;
    width: 24px;
  }
}
.layout-reorderable {
  cursor: grab;
  border-radius: var(--radius-md);
  transition:
    transform var(--motion-speed) var(--ease-soft),
    background-color var(--motion-speed) var(--ease-soft);
}
.layout-reorderable.layout-reorder-by-handle {
  cursor: default;
}
.layout-drag-handle {
  cursor: grab;
  user-select: none;
  width: fit-content;
}
.client-block > .layout-drag-handle,
.payment-block > .layout-drag-handle {
  width: fit-content;
}
.layout-reorderable.is-drag-active,
.layout-reorderable.is-drag-active .layout-drag-handle {
  cursor: grabbing;
  position: relative;
  z-index: 2;
}
.is-layout-pinned {
  cursor: default;
}
.is-layout-disabled {
  display: none !important;
}
body.is-layout-resizing {
  cursor: ew-resize;
  user-select: none;
}
body.is-layout-resizing * {
  cursor: ew-resize !important;
}
.date-blocks,
.footer-details,
.issuer-details {
  transition: gap var(--motion-speed) var(--ease-soft);
}

/* ── Footer ──────────────────────────────────────────────────────────────── */
.footer {
  display: flex;
  flex-direction: column;
  gap: var(--invoice-content-gap);
  padding: 0 var(--invoice-page-inset);
  align-items: flex-start;
  justify-content: flex-start;
  align-self: stretch;
  flex-shrink: 0;
  position: relative;
}
.footer-details {
  display: flex;
  flex-direction: row;
  gap: var(--invoice-section-gap);
  align-items: flex-start;
  justify-content: flex-start;
  align-self: stretch;
  flex-shrink: 0;
  position: relative;
}
/* ── Overlay buttons ─────────────────────────────────────────────────────── */
.btn-merge {
  background: var(--ink);
  border-radius: var(--radius-md);
  padding: var(--space-md) var(--space-lg)
    var(--space-md) var(--space-md);
  display: flex;
  flex-direction: row;
  gap: var(--space-xs);
  align-items: center;
  justify-content: center;
  position: absolute;
  right: calc(100% - 13px);
  box-shadow: 4px 8px 15px 0px rgba(0, 0, 0, 0.1);
  z-index: 10;
}

/* Pill/menu label typography */
.merge,
.pill-label,
.reset-edits,
.reconnect,
.sync,
.currency-round {
  font-family: var(--font);
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  letter-spacing: var(--tracking-tight);
  font-weight: 600;
  text-transform: uppercase;
  position: relative;
}
.merge,
.reconnect {
  color: var(--paper);
}

.icon-merge {
  color: var(--paper);
  transform: rotate(-90deg);
}

.icon-merge,
.icon-tasks,
.icon-milestones,
.icon-projects,
.icon-reset,
.icon-reconnect,
.icon-sync,
.icon-round {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  position: relative;
  overflow: visible;
}

.icon-sync {
  transform-box: border-box;
  transform-origin: center;
}

.view-table-switcher {
  background: var(--paper);
  border-radius: var(--radius-md);
  padding: 5px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  align-items: center;
  justify-content: center;
  /* Wide screens: an always-visible menu parked in the page's right gutter
     (setSwitcherTop sets the vertical offset). Compact overrides this to a
     bottom popup opened by the view-cycle button. The always-on rule lives
     after the hidden/.visible base rules below so it wins the cascade. */
  position: absolute;
  left: calc(100% - 13px);
  box-shadow: -4px 8px 15px 0px rgba(0, 0, 0, 0.07);
  z-index: 10;
}
.menu-currency-switcher {
  background: var(--surface);
  border-radius: calc(var(--radius-md) - 4px);
  padding: 4px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  min-height: 44px;
  gap: 0;
  align-self: stretch;
  justify-items: stretch;
  position: relative;
}
.tasks, .milestones, .projects {
  border-radius: var(--radius-md);
  padding: var(--space-md) var(--space-lg)
    var(--space-md) var(--space-md);
  display: flex;
  flex-direction: row;
  gap: var(--space-sm);
  align-items: center;
  justify-content: flex-start;
  align-self: stretch;
  flex-shrink: 0;
  position: relative;
  background: var(--paper);
}
.tasks {
  background: var(--ink);
}
.menu {
  background: var(--paper);
  border-radius: var(--radius-md);
  padding: 5px;
  display: flex;
  flex-direction: column;
  gap: 0;
  align-items: stretch;
  justify-content: center;
  position: fixed;
  bottom: 160px;
  left: 50%;
  translate: -50% 0;
  box-shadow: -4px 8px 15px 0px rgba(0, 0, 0, 0.07);
  z-index: 50;
}
.buttons {
  display: flex;
  flex-direction: row;
  gap: 5px;
  align-items: center;
  justify-content: flex-start;
  position: fixed;
  bottom: 50px;
  left: 50%;
  translate: -50% 0;
  z-index: 50;
}
.btn-download {
  background: var(--ink);
  border-radius: var(--radius-lg);
  padding: 18px 24px 18px 22px;
  display: flex;
  flex-direction: row;
  gap: 8px;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  position: relative;
  box-shadow:
    0px 8px var(--invoice-content-gap) 0px rgba(0, 0, 0, 0.1),
    0px 4px 8px 0px rgba(0, 0, 0, 0.1);
  /* Seamlessly morph between the solid-black hold state and the muted
     animating state instead of snapping. */
  transition: background-color var(--progress-finish) ease-out;
}
.btn-download.menu-muted,
.btn-download.is-downloading,
.btn-download.is-cooldown,
body.is-loading .btn-download {
  background-color: var(--muted);
}
/* Not clickable while loading, downloading, or rate-limited */
.btn-download.is-downloading,
.btn-download.is-cooldown,
body.is-loading .btn-download {
  pointer-events: none;
}
/* Rate-limit countdown: overlays the label so the button never resizes */
.download-timer {
  display: none;
  position: absolute;
  inset: 0;
  align-items: center;
  justify-content: center;
  color: var(--paper);
  font-family: var(--font);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  z-index: 2;
  pointer-events: none;
}
.btn-download.is-cooldown .download-timer {
  display: flex;
}
.btn-download.is-cooldown .icon-download,
.btn-download.is-cooldown .download {
  visibility: hidden;
}
.download-progress {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-lg);
  overflow: hidden;
  z-index: 0;
  pointer-events: none;
}
/* Indeterminate progress fill — shared by the download button and the sync
   pill: fill to 90% on --progress-ease, then complete to 100% and fade on done. */
.download-progress-bar,
.menu > .switcher-indicator::after {
  position: absolute;
  inset: 0;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left;
  opacity: 0;
  will-change: transform;
  transition: transform var(--progress-fill) var(--progress-ease);
}
.menu > .switcher-indicator::after {
  content: "";
}
.btn-download.is-downloading .download-progress-bar,
.menu.is-syncing > .switcher-indicator::after {
  opacity: 1;
  transform: scaleX(0.9);
}
.btn-download.is-download-done .download-progress-bar,
.menu.is-sync-done > .switcher-indicator::after {
  opacity: 1;
  transform: scaleX(1);
  transition: transform var(--progress-finish) ease-out;
}
/* Download bar dissolves as it completes instead of vanishing on class reset —
   opacity had no transition, so it blinked out. Fades alongside the fill. */
.btn-download.is-download-done .download-progress-bar {
  opacity: 0;
  transition:
    transform var(--progress-finish) ease-out,
    opacity var(--progress-finish) ease-out;
}
/* Keep the surface muted while the sync bar completes so the ink fill shows. */
.menu.is-syncing > .switcher-indicator,
.menu.is-sync-done > .switcher-indicator {
  background: var(--surface);
}
.icon-download {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  position: relative;
  z-index: 1;
  overflow: visible;
  color: var(--paper);
}
/* Disabled for now — keep the download-fly keyframe for later tuning.
.btn-download.is-downloading .icon-download {
  animation: download-fly 1.1s var(--ease-soft) infinite;
}
*/
.download {
  color: var(--paper);
  text-align: left;
  font-family: var(--font);
  font-size: var(--text-sm);
  line-height: var(--leading-sm);
  letter-spacing: var(--tracking-tight);
  font-weight: 600;
  text-transform: uppercase;
  position: relative;
  z-index: 1;
}
.btn-menu {
  background: var(--paper);
  border-radius: var(--radius-lg);
  padding: 24px;
  display: flex;
  flex-direction: row;
  gap: var(--space-sm);
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  position: relative;
  box-shadow: -4px 8px 15px 0px rgba(0, 0, 0, 0.07);
}
.icon-dots {
  flex-shrink: 0;
  width: 9px;
  height: 9px;
  position: relative;
}
.menu-dot {
  background: var(--ink);
  border-radius: 50%;
  width: 5px;
  height: 5px;
  position: absolute;
  top: 3px;
}
.icon-dots .menu-dot:nth-child(1) {
  left: -6px;
}
.icon-dots .menu-dot:nth-child(2) {
  left: 2px;
}
.icon-dots .menu-dot:nth-child(3) {
  left: 10px;
}

.frame {
  display: none;
}

/* ── Interactions ─────────────────────────────────────────────────────────── */

/* Rubber band selection rectangle */
.selection-rect {
  display: none;
  position: fixed;
  border: 1px solid var(--accent);
  background: var(--accent-faint);
  pointer-events: none;
  z-index: 10;
}

/* Hover-only checkboxes; always visible for merged or selected rows */
.checkbox {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  aspect-ratio: 1;
  transform: translateX(-100%);
  cursor: pointer;
  opacity: 0;
  z-index: var(--row-status-layer, 1);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity var(--motion-speed) var(--ease-soft);
}
.item-row:hover .checkbox,
.item-row.is-merged-row .checkbox,
.item-row.is-selected .checkbox {
  opacity: 1;
}
/* One status order everywhere: blue selection < purple merge < green rename. */
.item-row.is-merged-row .checkbox {
  --row-status-layer: 2;
}

/* Checkbox dot indicators */
.dot,
.dot-inactive {
  width: 4px;
  height: 4px;
}
.dot {
  background: var(--accent);
}
.dot-inactive {
  border-radius: 50%;
  background: var(--muted);
}

/* Purple checkbox for merged items */
.dot.dot-purple {
  background: #7c3aed;
  border-radius: 50%;
}

/* Gray-out non-selected items during selection */
.item-row.is-grayed .item-description,
.item-row.is-grayed .item-time,
.item-row.is-grayed .item-amount {
  color: var(--muted);
  transition: color var(--motion-speed) var(--ease-soft);
}
.item-row .row-divider {
  transition: background-color var(--motion-speed) var(--ease-soft);
}
.item-row.is-grayed .row-divider {
  background-color: var(--muted-light);
}

/* Item row wrapper */
.item-row {
  padding: var(--invoice-tiny-gap) 0px 0px 0px;
  display: flex;
  flex-direction: column;
  gap: var(--invoice-tiny-gap);
  align-items: flex-start;
  justify-content: flex-start;
  align-self: stretch;
  flex-shrink: 0;
  position: relative;
  cursor: grab;
  border-radius: var(--radius-md);
  transition:
    transform var(--motion-speed) var(--ease-soft),
    background-color var(--motion-speed) var(--ease-soft);
}
.item-row:active {
  cursor: grabbing;
}
.item-row.is-drag-active {
  cursor: grabbing;
  z-index: 2;
}
/* Hide the renamed-dot and checkbox while dragging — they're absolutely
   positioned in the left gutter and would otherwise float as a stray artifact
   as the row moves. */
.item-row.is-drag-active::before,
.item-row.is-drag-active .checkbox {
  opacity: 0;
}
/* Layout blocks keep their normal shape; mute their existing content instead. */
.layout-reorderable.is-drag-active,
.layout-reorderable.is-drag-active * {
  color: var(--muted);
}
/* Rows keep their normal shape: muting their content is the whole drag cue. */
.item-row.is-drag-active .item-description,
.item-row.is-drag-active .item-time,
.item-row.is-drag-active .item-amount {
  color: var(--muted);
}

/* Empty grouping: matches a real row's rhythm, but inert and muted. */
.empty-row {
  padding: var(--invoice-tiny-gap) 0 0;
  display: flex;
  flex-direction: column;
  gap: var(--invoice-tiny-gap);
  align-self: stretch;
}
.empty-row .item-description,
.empty-row .item-time,
.empty-row .item-amount {
  color: var(--muted);
}

/* Item row cells */
.item-description,
.item-time,
.item-amount {
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  font-weight: 400;
  position: relative;
}
.item-description {
  line-height: var(--item-text-line-height);
  height: var(--item-text-line-height);
  letter-spacing: var(--tracking-tight);
  grid-column: 1 / span 1;
  grid-row: 1 / span 1;
  outline: none;
  cursor: text;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  position: relative;
  z-index: 1;
}
.item-description[contenteditable="true"] {
  width: 100%;
  height: auto;
  min-height: var(--item-text-line-height);
  line-height: var(--item-text-line-height);
  overflow-x: auto;
  overflow-y: hidden;
  text-overflow: clip;
  caret-color: var(--ink);
  scrollbar-width: none;
}
.item-description[contenteditable="true"]::-webkit-scrollbar {
  display: none;
}
.item-time {
  text-align: right;
  grid-column: 2 / span 1;
  grid-row: 1 / span 1;
}
.item-amount {
  text-align: right;
  grid-column: 3 / span 1;
  grid-row: 1 / span 1;
}

/* Green renamed status is the highest-priority marker. */
.item-row.is-renamed::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  aspect-ratio: 1;
  transform: translateX(-100%);
  z-index: 3;
  pointer-events: none;
  background: radial-gradient(
    circle 2px at 50% 50%,
    var(--dot-renamed) 99%,
    transparent 100%
  );
}

/* Blue dot on selected items */
.item-row.is-selected .dot-inactive {
  background: var(--accent);
  border-color: var(--accent);
}

/* Overlay switchers */
.view-table-switcher,
.menu {
  opacity: 0;
  transform: translateY(3px);
  pointer-events: none;
  transition:
    opacity var(--motion-speed) var(--ease-soft),
    transform var(--motion-speed) var(--ease-soft);
}
.view-table-switcher.visible,
.menu.visible {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
/* Wide screens: the gutter switcher is a permanent menu (no hover reveal, no
   tap). Placed after the hidden rule above so it wins the cascade. */
@media (min-width: 1001px) {
  .view-table-switcher {
    opacity: 1;
    transform: none;
    pointer-events: auto;
  }
}

/* Sliding black pill indicator */
.switcher-indicator {
  position: absolute;
  inset: auto var(--space-xs);
  background: var(--ink);
  border-radius: 32px;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}
.switcher-indicator.ready {
  transition:
    top var(--motion-speed) var(--ease-soft),
    left var(--motion-speed) var(--ease-soft),
    width var(--motion-speed) var(--ease-soft),
    height var(--motion-speed) var(--ease-soft),
    opacity var(--motion-speed) var(--ease-soft);
}
.switcher-indicator--horizontal {
  inset: var(--space-xs);
  right: auto;
  width: 0;
}
.menu-currency-switcher .switcher-indicator {
  top: var(--space-xs);
  bottom: var(--space-xs);
  height: auto;
}
.menu > .switcher-indicator {
  opacity: 0;
}
.menu-currency-switcher .switcher-indicator {
  opacity: 1;
}
.menu.has-active > .switcher-indicator {
  opacity: 1;
}
.menu.has-active:has(#menu-reset.active) > .switcher-indicator {
  background: var(--warning);
}
.menu.has-active:has(#menu-sync.is-sync-failed) > .switcher-indicator {
  background: var(--warning);
}

/* Reset Figma-hardcoded item backgrounds; stack above indicator; never clip icons */
.view-table-switcher,
.view-table-switcher .tasks,
.view-table-switcher .milestones,
.view-table-switcher .projects,
.menu .menu-item {
  overflow: visible;
}
/* clip keeps the morph pill inside the rounded menu; unlike overflow:hidden it
   doesn't create a scroll container or BFC, so icon layout is unaffected */
.menu {
  overflow: clip;
  min-width: 140px;
  /* Confine the sync label's difference blend to the menu so it inverts against
     the indicator/surface, never the page behind the menu. */
  isolation: isolate;
}
.menu-currency-switcher {
  overflow: visible;
  position: relative;
  z-index: 1;
  margin-top: 4px;
}
.view-table-switcher .tasks,
.view-table-switcher .milestones,
.view-table-switcher .projects {
  background: transparent;
  position: relative;
  z-index: 1;
  cursor: pointer;
}
.menu .menu-item {
  background: transparent;
  position: relative;
  z-index: 1;
  cursor: pointer;
  border-radius: var(--radius-md);
  display: flex;
  flex-direction: row;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-lg)
    var(--space-md) var(--space-md);
  align-items: center;
  justify-content: flex-start;
}
.menu-currency-option {
  background: transparent;
  position: relative;
  z-index: 1;
  cursor: pointer;
  border-radius: calc(var(--radius-md) - 7px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-md) 0;
  min-height: 16px;
  min-width: 0;
}
.menu .icon-reset,
.menu .icon-reconnect,
.menu .icon-sync,
.menu .icon-round {
  align-self: center;
}

/* Item labels: black by default, white when active */
.view-table-switcher .tasks .pill-label,
.view-table-switcher .milestones .pill-label,
.view-table-switcher .projects .pill-label,
.menu .reconnect,
.menu .sync,
.menu .currency-round {
  color: var(--ink);
  transition: color var(--motion-speed) var(--ease-soft);
}
.menu .sync {
  min-width: 56px;
}
.menu-currency-label {
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--text-xs);
  line-height: var(--leading-xs);
  letter-spacing: var(--tracking-tight);
  font-weight: 600;
  text-transform: none;
  text-align: center;
  white-space: nowrap;
  position: relative;
  transition: color var(--motion-speed) var(--ease-soft);
}
.menu .reset-edits {
  color: var(--warning);
  transition: color var(--motion-speed) var(--ease-soft);
  /* Reserve the widest label ("Reset edits") via a same-column hidden sizer, so
     swapping to the shorter "Sure?" confirm text never reflows the menu width. */
  display: inline-grid;
  justify-items: start;
}
.menu .reset-edits::after {
  content: "Reset edits";
  height: 0;
  overflow: hidden;
  visibility: hidden;
}
.view-table-switcher .tasks.active .pill-label,
.view-table-switcher .milestones.active .pill-label,
.view-table-switcher .projects.active .pill-label,
.menu #menu-reconnect.active .reconnect,
.menu #menu-sync.active .sync {
  color: var(--paper);
}
.menu #menu-reset.active .reset-edits {
  color: var(--paper);
}
.menu-currency-option.active .menu-currency-label {
  color: var(--paper);
}

/* Item icons use currentColor: black by default, white on the active pill */
.view-table-switcher .tasks .icon-tasks,
.view-table-switcher .milestones .icon-milestones,
.view-table-switcher .projects .icon-projects,
.menu .icon-reconnect,
.menu .icon-sync {
  color: var(--ink);
  transition: color var(--motion-speed) var(--ease-soft);
}
.view-table-switcher .tasks.active .icon-tasks,
.view-table-switcher .milestones.active .icon-milestones,
.view-table-switcher .projects.active .icon-projects,
.menu #menu-reconnect.active .icon-reconnect,
.menu #menu-sync.active .icon-sync {
  color: var(--paper);
}
/* Reset icon is the warning colour, turning white on the active pill */
.menu .icon-reset {
  color: var(--warning);
  transition: color var(--motion-speed) var(--ease-soft);
}
.menu #menu-reset.active .icon-reset {
  color: var(--paper);
}
.menu #menu-sync .icon-sync.spinning {
  animation: sync-spin 0.85s linear infinite;
}
/* The ink bar sweeps left-to-right under the label as it fills. Instead of
   masking a white label copy to the fill width, the icon and label are pure
   white with mix-blend-mode: difference, so each pixel auto-inverts against
   whatever is behind it — near-black on the light surface, white over the ink
   bar — no duplicate node, no width sync, no colour math. */
#menu-sync {
  position: relative;
}
.menu #menu-sync.is-syncing .sync,
.menu #menu-sync.is-sync-done .sync,
.menu #menu-sync.is-syncing .icon-sync,
.menu #menu-sync.is-sync-done .icon-sync {
  color: #fff;
  mix-blend-mode: difference;
}
/* The menu-item's own z-index:1 stacking context would isolate the label from
   the ink bar, so difference blends against nothing. Drop it during sync so the
   label blends against the indicator (light surface -> dark text, ink -> white).
   z-index:auto on a position:relative box creates no stacking context; DOM order
   still keeps the item above the indicator. */
.menu #menu-sync.is-syncing,
.menu #menu-sync.is-sync-done {
  z-index: auto;
}

/* Never clip inline SVG icons in any overlay or button */
.btn-download,
.btn-eye {
  overflow: visible;
}

/* Button hover lift */
.btn-download,
.btn-menu {
  cursor: pointer;
  overflow: visible;
  position: relative;
  transition:
    transform var(--motion-speed) var(--ease-soft),
    background-color var(--motion-speed) var(--ease-soft);
}

/* Invisible bridge fills the gap between button and menu so mouseleave doesn't fire mid-travel */
.btn-menu::after {
  content: "";
  position: absolute;
  bottom: 100%;
  left: 0;
  width: 100%;
  height: 8px;
  pointer-events: all;
}
.btn-download:hover,
.btn-menu:hover,
.btn-menu.menu-open {
  transform: translateY(var(--lift));
}

/* Merge button — animated positioning and appear/disappear */
.btn-merge {
  cursor: pointer;
  overflow: visible;
  opacity: 0;
  transform: translateY(var(--lift));
  pointer-events: none;
  transition:
    top var(--motion-speed) var(--ease-soft),
    opacity var(--motion-speed) var(--ease-soft),
    transform var(--motion-speed) var(--ease-soft);
}
.btn-merge.is-visible {
  opacity: 1;
  transform: translateX(0);
  pointer-events: auto;
}
.btn-merge.is-visible:hover {
  transform: translateX(0) translateY(var(--lift));
}

.price-list:hover .btn-eye,
.price-list:hover .btn-sort {
  --icon-visibility-delay: 0s;
  opacity: 1;
  visibility: visible;
}
.btn-eye.is-off,
.btn-sort {
  color: var(--muted);
}
.btn-sort.sort-asc::after {
  clip-path: inset(0 0 0 50%);
  opacity: 1;
}
.btn-sort.sort-desc::after {
  clip-path: inset(0 50% 0 0);
  opacity: 1;
}

/* ── Print / PDF export ───────────────────────────────────────────────────── */
@media print {
  /* The invoice is already a pixel-perfect A4 page (794×1123px @96dpi).
     Margin 0 — the design's own 50px inset is the page margin. */
  @page {
    size: A4;
    margin: 0;
  }

  html, body {
    background: #fff;
    margin: 0;
    padding: 0;
    min-height: 0;
    display: block;
  }

  /* Hide UI chrome */
  .buttons,
  .btn-merge,
  .view-table-switcher,
  .menu,
  .btn-eye,
  .btn-sort,
  .btn-round,
  .checkbox,
  .dot,
  .dot-inactive,
  .item-row.is-renamed::before,
  .selection-rect,
  .layout-resize-handle,
  .price-list::after {
    display: none !important;
  }

  /* Drop the scene shadow only — keep the invoice's flex geometry intact
     so the footer stays pinned to the bottom and columns stay aligned. */
  .mockup, .invoice {
    box-shadow: none;
  }

  /* Show every line item (screen caps the preview at 10 rows + scroll) */
  #line-items-container {
    max-height: none;
    overflow: visible;
  }

  /* Dividers — replace background with border so they always print */
  .divider {
    background: none;
    border-top: 1px solid #000;
    height: 0;
  }
  .row-divider {
    background: none;
    border-top: 1px solid #a9a9a9;
    height: 0;
  }
  .total-divider {
    background: none;
    border-top: 1px solid #000;
    height: 0;
  }
}

/* Bottom view-cycle button — a three-dots-style round button (reuses .btn-menu)
   that cycles Tasks → Milestones → Projects, and becomes a Merge action (its
   4th state) when ≥2 rows are selected. Only the COMPACT layout uses it; wide
   screens show the always-on gutter menu + gutter merge instead. */
.btn-view-cycle {
  display: none;
  padding: 18px;
}
.btn-view-cycle .vc-icon {
  width: 22px;
  height: 22px;
  color: var(--ink);
  display: none;
}
.btn-view-cycle[data-mode="tasks"] .vc-tasks,
.btn-view-cycle[data-mode="milestones"] .vc-milestones,
.btn-view-cycle[data-mode="projects"] .vc-projects {
  display: block;
}
.btn-view-cycle.has-merge .vc-icon {
  display: none;
}
.btn-view-cycle.has-merge .vc-merge {
  display: block;
  color: var(--ink);            /* dark arrow on the white button */
  transform: rotate(-90deg);    /* arrow points right */
}

/* ── Compact controls (≤1000px) ──────────────────────────────────────────────
   Phones, iPad portrait, and small laptop windows: too narrow for the gutter
   menu, so the view-cycle button + bottom popup take over and the gutter merge
   is retired (merge is the view-cycle's 4th state). */
@media (max-width: 1000px) {
  .view-table-switcher {
    /* Bottom popup opened by the view-cycle button; JS sets left/bottom inline.
       left is a plain override of the base gutter position (NOT !important, or
       it would clobber the JS-set popup left); top:auto clears any stale inline
       top left by setSwitcherTop after a wide→compact resize. */
    position: fixed;
    left: auto;
    top: auto !important;
    z-index: 50;
  }
  .btn-view-cycle {
    display: flex;
  }
  .btn-merge {
    display: none !important;
  }
}

/* ── Mobile — true reflow of the invoice editor ───────────────────────────────
   The desktop invoice is a fixed 794px A4 mockup whose geometry mirrors the PDF
   point-space constants in lib/layout.ts. On phones we drop the fixed page and
   let the invoice reflow into a fluid, single-column, touch-friendly editor.
   The PDF renders separately in lib/pdf.ts, so none of this affects PDF output.
   Hover-gated controls are surfaced here because touch has no hover. */
@media (max-width: 820px) {
  html, body {
    overflow-x: hidden;
  }
  body {
    margin: 0;
    padding: 0 0 128px;   /* just enough to clear the fixed download bar */
    background: var(--paper);   /* no grey surround on phones */
  }

  /* Fluid page — drop the fixed A4 width/height and the paper shadow */
  .mockup {
    width: 100%;
    min-height: 0;
  }
  /* The A4 layout uses flex:1 to distribute height inside the fixed page; with
     an auto-height page that collapses .top and lets the footer overlap the
     totals. Flow every section as a normal block instead. */
  .top,
  .items-table,
  .price-list {
    flex: none;
  }
  .invoice {
    width: 100%;
    height: auto;
    min-height: 0;
    justify-content: flex-start;
    box-shadow: none;
    padding: 24px 0 16px;
  }

  /* One consistent 20px side gutter instead of the 50px A4 inset */
  .invoice-title,
  .invoice-body,
  .table-header,
  .sum,
  .footer {
    padding-left: 20px;
    padding-right: 20px;
  }
  #line-items-container,
  #line-items-container.is-scrollable {
    margin: 0;
    padding: 0 20px;
  }

  /* Title — shrink the display heading and un-pin it from its fixed box, but
     keep the row so the number/date sit on the right, opposite "Invoice". */
  .left-padding-fix {
    width: auto;
    height: auto;
  }
  .invoice-heading {
    position: static;
    left: auto;
    font-size: 44px;
    line-height: 1;
  }

  /* Parties — stack issuer over the dates, blocks go full width */
  .parties {
    flex-direction: column;
    gap: 24px;
  }
  /* !important overrides the inline width app.js sets on these resizable
     blocks (e.g. the 396px client block, which would overflow the phone). */
  .issuer-block,
  .client-block,
  .payment-block {
    width: 100% !important;
    max-width: none !important;
  }
  .date-blocks {
    gap: 40px;
    justify-content: flex-start;
    flex-wrap: wrap;
  }
  .billing-period-block,
  .due-date-block {
    align-items: flex-start;
  }
  .due-date-block {
    width: auto !important;
  }
  .billing-period,
  .due-date,
  .billing-period-value,
  .due-date-value {
    text-align: left;
  }

  /* Line items — fixed time/amount columns so every row's independent grid
     lines up vertically (auto columns drift row-to-row). Description keeps its
     content width, so the trailing space in its column is a tap-to-select
     target rather than part of the editable name. */
  .table-header-row {
    grid-template-columns: minmax(0, 1fr) 82px 92px;
    gap: 12px;
  }
  /* Show every row and let the page scroll — no inner 10-row scroller/fade */
  .price-list {
    overflow: visible;
  }
  .price-list::after {
    display: none;
  }
  #line-items-container,
  #line-items-container.is-scrollable {
    max-height: none;
    height: auto;
    overflow: visible;
    flex: none;
  }

  /* Space between the totals and the footer (on desktop the A4's
     space-between provides this; the auto-height mobile page needs it added). */
  .footer {
    margin-top: 40px;
  }
  /* Footer — stack billed-to over payment details */
  .footer-details {
    flex-direction: column;
    gap: 24px;
  }

  /* No block resize on the reflow layout — drop the desktop resize handle. */
  .layout-resize-handle {
    display: none;
  }
  .item-row {
    cursor: default;
    /* snappier reorder — the 0.4s default feels sluggish on touch */
    transition:
      transform 0.15s var(--ease-soft),
      background-color 0.15s var(--ease-soft);
  }

  /* White fade at the bottom so scrolling rows don't show through the gaps
     between the floating action buttons (which sit at z-index 50). Tied to the
     reflow's white background. */
  body::after {
    content: "";
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    height: 150px;
    background: linear-gradient(to top, var(--paper) 55%, rgba(255, 255, 255, 0));
    pointer-events: none;
    z-index: 40;
  }
}

/* ── Compact controls (≤1000px) ──────────────────────────────────────────────
   Below ~966px the paper's side gutters get too small to hold the merge/switcher
   pills without clipping. So from 1000px down — phones, iPad portrait, AND small
   laptop windows (which keep the fixed A4 page) — move view-switching + merge to
   the bottom bar's view-cycle button and drop the gutter overlays. */
/* ── Touch affordances (no hover) ────────────────────────────────────────────
   Any width, phones through iPad landscape. Without a pointer we can't
   hover-reveal controls or start an HTML5 drag, so surface everything and drive
   selection/reorder from taps + long-press (see app.js isTouch()). */
@media (hover: none) {
  /* Don't let taps/drags start a text selection on the invoice UI — makes
     row select + drag-to-reorder clean. The editable name stays selectable. */
  .mockup {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
  }
  .item-description[contenteditable="true"] {
    -webkit-user-select: text;
    user-select: text;
  }
  /* Checkboxes have no hover on touch — show them always, as status dots. The
     gray idle dot stays hidden (it was only ever a hover affordance); only the
     meaningful states (selected = blue, merged = purple) light up. */
  .checkbox {
    opacity: 1;
  }
  .checkbox .dot-inactive {
    opacity: 0;
    transition: opacity var(--motion-speed) var(--ease-soft);
  }
  .item-row.is-selected .checkbox .dot-inactive {
    opacity: 1;
  }
  /* Both status dots (checkbox + renamed) share the same small box just left of
     the name. */
  .checkbox,
  .item-row.is-renamed::before {
    transform: none;
    left: -13px;
    right: auto;
    width: 6px;
    aspect-ratio: auto;
  }
  /* Surface the hover-gated inline toggles (rounding, hide-time, sort) so
     they're usable without a pointer. */
  .btn-round,
  .btn-eye,
  .btn-sort {
    opacity: 1 !important;
    visibility: visible !important;
    pointer-events: auto !important;
  }
  /* Tap-to-round on the total is a touch-only behavior (see app.js). */
  #total-value {
    cursor: pointer;
  }
  /* Drop the hover-lift — a pointer affordance that just makes buttons jump on
     tap. */
  .btn-download:hover,
  .btn-menu:hover,
  .btn-menu.menu-open,
  .btn-view-cycle:hover {
    transform: none !important;
  }
}
