/*
 * Styles for the dynamic app.
 *
 * Hand-written, no framework and no build step — /dynamic/ is plain PHP on
 * shared hosting and adding a toolchain to it would defeat the point.
 *
 * The tokens below are copied from the static app's app/globals.css rather than
 * imported: that file is compiled by Tailwind into a content-hashed bundle, so
 * there is nothing stable for PHP to link to. Copying means the two can drift,
 * so keep the :root and .dark blocks in sync by hand when the palette changes.
 */

:root {
  --brand-400: #60a5fa;
  --brand-500: #3b82f6;
  --brand-600: #2563eb;
  --brand-700: #1d4ed8;
  --accent-500: #10b981;
  --accent-600: #059669;
  --danger: #dc2626;

  --surface-page: #f6f8fc;
  --surface-glow-a: rgb(37 99 235 / 0.12);
  --surface-glow-b: rgb(16 185 129 / 0.1);
  --surface-card: rgb(255 255 255 / 0.72);
  --surface-panel: rgb(255 255 255 / 0.55);
  --surface-input: rgb(255 255 255 / 0.85);
  --border-soft: rgb(15 23 42 / 0.08);
  --border-strong: rgb(15 23 42 / 0.14);
  --text-primary: #0f172a;
  --text-secondary: #475569;
  --text-muted: #64748b;
  --shadow-card: 0 24px 60px -28px rgb(15 23 42 / 0.35);
}

.dark {
  --brand-600: #3b82f6;
  --surface-page: #070b16;
  --surface-glow-a: rgb(37 99 235 / 0.22);
  --surface-glow-b: rgb(16 185 129 / 0.14);
  --surface-card: rgb(15 23 42 / 0.62);
  --surface-panel: rgb(30 41 59 / 0.42);
  --surface-input: rgb(15 23 42 / 0.6);
  --border-soft: rgb(148 163 184 / 0.14);
  --border-strong: rgb(148 163 184 / 0.26);
  --text-primary: #f1f5f9;
  --text-secondary: #cbd5e1;
  --text-muted: #94a3b8;
  --shadow-card: 0 24px 70px -30px rgb(0 0 0 / 0.85);
  --danger: #f87171;
}

* {
  box-sizing: border-box;
  border-color: var(--border-soft);
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100dvh;
  background-color: var(--surface-page);
  background-image:
    radial-gradient(60rem 40rem at 12% -8%, var(--surface-glow-a), transparent 65%),
    radial-gradient(50rem 36rem at 92% 8%, var(--surface-glow-b), transparent 60%);
  background-attachment: fixed;
  color: var(--text-primary);
  font:
    16px/1.6 ui-sans-serif,
    system-ui,
    -apple-system,
    "Segoe UI",
    Roboto,
    sans-serif;
}

/*
 * Sticky footer.
 *
 * The wrap fills at least the viewport and <main> absorbs the slack, so on a
 * short page — sign-in, an empty dashboard, a "code not found" notice — the
 * footer sits on the bottom edge of the screen instead of floating halfway up
 * under the content. On a page taller than the viewport nothing changes: main
 * is its natural height and the footer follows it as before.
 *
 * `flex: 1 0 auto` rather than `flex: 1`: basis stays at the content height and
 * shrinking is disabled, so tall content can never be squeezed to fit the
 * viewport. box-sizing is border-box globally, so this element's own padding
 * counts inside the 100dvh and the page gains no scrollbar it did not need.
 */
.wrap {
  width: 100%;
  max-width: 72rem;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  margin: 0 auto;
  padding: 12px 16px 48px;
}

main {
  flex: 1 0 auto;
}

/* --- glass surfaces ------------------------------------------------------ */

/* Matches the static app's --radius-card (1.5rem). The header overrides this
   with a tighter radius below, exactly as the static app does. */
.glass {
  background: var(--surface-card);
  border: 1px solid var(--border-soft);
  border-radius: 1.5rem;
  backdrop-filter: blur(16px);
  box-shadow: var(--shadow-card);
}

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

.site-header {
  position: sticky;
  top: 12px;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  margin-bottom: 20px;
  /* Tighter than a card, matching the static app's rounded-2xl header. */
  border-radius: 1rem;
}

.mark {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  flex: none;
  border-radius: 12px;
  background: var(--brand-600);
  color: #fff;
  box-shadow: 0 10px 20px -8px rgb(37 99 235 / 0.6);
}

.site-header h1 {
  margin: 0;
  font-size: 1rem;
  line-height: 1.25;
  font-weight: 600;
}

.site-header .sub {
  margin: 0;
  font-size: 0.75rem;
  color: var(--text-muted);
}

.header-titles {
  min-width: 0;
  flex: 1;
}

.header-titles a {
  color: inherit;
  text-decoration: none;
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap;
}

.badge-pro {
  border-color: rgb(16 185 129 / 0.35);
  background: rgb(16 185 129 / 0.12);
  color: var(--accent-600);
}

.dark .badge-pro {
  color: #34d399;
}

/* --- nav ----------------------------------------------------------------- */

.site-nav {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-wrap: wrap;
}

.site-nav a {
  padding: 6px 10px;
  border-radius: 8px;
  color: var(--text-secondary);
  text-decoration: none;
  font-size: 0.875rem;
}

.site-nav a:hover {
  background: var(--surface-panel);
  color: var(--text-primary);
}

/* --- typography ---------------------------------------------------------- */

/*
 * Two heading roles, deliberately far apart in weight.
 *
 * h2 is the page title ("My codes", "Edit code") — this app is multi-page, so
 * unlike the static app it genuinely needs one. It is sized to lead a page
 * without shouting; the old 1.375rem made every screen read like a document
 * rather than a tool.
 *
 * h3 is a section label inside a card, and matches the static app's quiet
 * `text-sm font-semibold`. It had no rule at all before, which left it at the
 * browser default (~1.17em) — close enough to h2 that the two levels barely
 * read as different, and pages were patching the margin inline to compensate.
 */
h2 {
  margin: 0 0 4px;
  font-size: 1.125rem;
  line-height: 1.3;
  font-weight: 600;
  letter-spacing: -0.01em;
}

h3 {
  margin: 0 0 8px;
  font-size: 0.875rem;
  line-height: 1.35;
  font-weight: 600;
  letter-spacing: -0.005em;
}

.lede {
  margin: 0 0 20px;
  color: var(--text-muted);
}

.muted {
  color: var(--text-muted);
}

.small {
  font-size: 0.8125rem;
}

a {
  color: var(--brand-600);
}

.card {
  padding: 20px;
  margin-bottom: 20px;
}

.card > :last-child {
  margin-bottom: 0;
}

.narrow {
  max-width: 26rem;
  margin-inline: auto;
}

/* --- forms --------------------------------------------------------------- */

label {
  display: block;
  margin-bottom: 6px;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text-secondary);
}

.field {
  margin-bottom: 16px;
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="url"],
select,
textarea {
  width: 100%;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--border-strong);
  background: var(--surface-input);
  color: var(--text-primary);
  font: inherit;
}

input:focus-visible,
textarea:focus-visible,
button:focus-visible,
a:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}

.hint {
  margin: 6px 0 0;
  font-size: 0.75rem;
  color: var(--text-muted);
}

.prefix-input {
  display: flex;
  align-items: stretch;
}

.prefix-input .prefix {
  display: flex;
  align-items: center;
  padding: 0 10px;
  border: 1px solid var(--border-strong);
  border-right: 0;
  border-radius: 10px 0 0 10px;
  background: var(--surface-panel);
  color: var(--text-muted);
  font-size: 0.8125rem;
  white-space: nowrap;
}

.prefix-input input {
  border-radius: 0 10px 10px 0;
}

/* A password field with a show/hide toggle. The button sits inside the same
   bordered box as the input rather than floating over it, so it reads as one
   control instead of an icon awkwardly pinned on top of a text field. */
.password-field {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface-input);
  overflow: hidden;
}

.password-field:focus-within {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}

.password-field input {
  flex: 1;
  min-width: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
}

.password-field input:focus-visible {
  outline: none;
}

.pw-toggle {
  flex: none;
  display: grid;
  place-items: center;
  width: 40px;
  border: 0;
  border-left: 1px solid var(--border-soft);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}

.pw-toggle:hover {
  color: var(--text-primary);
  background: var(--surface-panel);
}

.pw-toggle svg {
  width: 18px;
  height: 18px;
}

/*
 * Replaces the browser's own validation bubble.
 *
 * The native one is positioned by the browser, not by this stylesheet, so it
 * floats wherever the browser decides — often overlapping the card below it
 * rather than sitting inside the form's own visual boundary. app.js
 * intercepts the `invalid` event and renders this instead, in the same place
 * every other message on the page appears.
 */
.field-error {
  margin: 6px 0 0;
  font-size: 0.75rem;
  color: var(--danger);
}

.field-invalid {
  border-color: var(--danger) !important;
}

/* --- buttons ------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px 16px;
  border: 1px solid transparent;
  border-radius: 10px;
  background: var(--brand-600);
  color: #fff;
  font: inherit;
  font-weight: 600;
  font-size: 0.875rem;
  text-decoration: none;
  cursor: pointer;
}

.btn:hover {
  background: var(--brand-700);
}

.btn-secondary {
  background: var(--surface-panel);
  border-color: var(--border-strong);
  color: var(--text-primary);
}

.btn-secondary:hover {
  background: var(--surface-input);
}

.btn-danger {
  background: transparent;
  border-color: var(--danger);
  color: var(--danger);
}

.btn-danger:hover {
  background: rgb(220 38 38 / 0.1);
}

.btn-block {
  width: 100%;
}

.btn-row {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
}

/* --- messages ------------------------------------------------------------ */

.flash {
  padding: 12px 14px;
  border-radius: 12px;
  border: 1px solid var(--border-strong);
  background: var(--surface-panel);
  margin-bottom: 16px;
  font-size: 0.875rem;
}

.flash-ok {
  border-color: rgb(16 185 129 / 0.4);
  background: rgb(16 185 129 / 0.1);
}

.flash-error {
  border-color: rgb(220 38 38 / 0.4);
  background: rgb(220 38 38 / 0.08);
}

.flash strong {
  display: block;
  margin-bottom: 2px;
}

/* --- footer -------------------------------------------------------------- */

.site-footer {
  margin-top: 32px;
  padding-top: 16px;
  border-top: 1px solid var(--border-soft);
  font-size: 0.8125rem;
  color: var(--text-muted);
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
}

.site-footer a {
  color: var(--text-secondary);
}

/* A button that has to read as a nav link — used for sign-out, which is a POST
   so that no GET in this app changes state. */
.nav-form {
  display: inline;
  margin: 0;
}

.linklike {
  padding: 6px 10px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--text-secondary);
  font: inherit;
  font-size: 0.875rem;
  cursor: pointer;
}

.linklike:hover {
  background: var(--surface-panel);
  color: var(--text-primary);
}

/* --- page head ------------------------------------------------------------ */

.page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}

.page-head .lede {
  margin-bottom: 0;
}

/* --- dashboard layout ------------------------------------------------------ */

/*
 * Content plus a fixed sidebar, mirroring the static app's
 * `lg:grid-cols-[minmax(0,1fr)_360px]`. Below the breakpoint it collapses to a
 * single column and the sidebar falls underneath, which is where a plan summary
 * belongs on a phone anyway.
 *
 * `minmax(0, 1fr)` rather than plain `1fr` on purpose: a grid track's default
 * `min-width: auto` refuses to shrink below its content, and one pasted
 * destination URL with forty tracking parameters would otherwise push the whole
 * dashboard sideways.
 */
.dash {
  display: grid;
  gap: 16px;
  align-items: start;
}

@media (min-width: 64rem) {
  .dash {
    grid-template-columns: minmax(0, 1fr) 360px;
  }
}

.dash-side {
  display: grid;
  gap: 16px;
}

/* The grid gap owns the spacing here; the cards' own margin would double it. */
.dash-side > .card {
  margin-bottom: 0;
}

/* --- code list ------------------------------------------------------------ */

.code-list {
  display: grid;
  gap: 16px;
  margin-bottom: 20px;
}

.code-row {
  display: flex;
  gap: 20px;
  align-items: flex-start;
  padding: 20px;
  margin-bottom: 0;
}

.thumb {
  flex: none;
  width: 76px;
  height: 76px;
  border-radius: 12px;
  overflow: hidden;
  background: #fff;
  border: 1px solid var(--border-soft);
}

.thumb canvas,
.thumb svg {
  display: block;
  width: 100%;
  height: 100%;
}

.code-main {
  flex: 1;
  min-width: 0;
}

.code-main h3 {
  margin: 0 0 6px;
  font-size: 1rem;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.short-link {
  margin: 0 0 4px;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 0.875rem;
}

/* Long destinations must truncate rather than widen the row — a pasted URL with
   forty tracking parameters would otherwise push the page sideways. */
.dest {
  margin: 0 0 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.copy {
  padding: 2px 8px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface-panel);
  color: var(--text-secondary);
  font: inherit;
  font-size: 0.75rem;
  cursor: pointer;
}

.copy.copied {
  border-color: rgb(16 185 129 / 0.5);
  color: var(--accent-600);
}

.code-side {
  flex: none;
  text-align: right;
}

.scans {
  margin: 0 0 8px;
}

.scans strong {
  font-size: 1.5rem;
  line-height: 1;
}

.badge-off {
  border-color: rgb(220 38 38 / 0.35);
  background: rgb(220 38 38 / 0.1);
  color: var(--danger);
}

.btn-sm {
  padding: 6px 10px;
  font-size: 0.8125rem;
}

.upgrade {
  border-style: dashed;
}

/* --- stats ---------------------------------------------------------------- */

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: 16px;
  margin-bottom: 20px;
}

.stat {
  margin-bottom: 0;
  text-align: center;
}

.stat-n {
  margin: 0;
  font-size: 2.25rem;
  line-height: 1.1;
  font-weight: 600;
}

.stat p:last-child {
  margin: 0;
}

.bars {
  list-style: none;
  margin: 0 0 12px;
  padding: 0;
  display: grid;
  gap: 3px;
}

.bars li {
  display: grid;
  grid-template-columns: 3.25rem 1fr 2.5rem;
  align-items: center;
  gap: 8px;
  font-size: 0.75rem;
}

.bar-track {
  height: 10px;
  border-radius: 999px;
  background: var(--surface-panel);
  border: 1px solid var(--border-soft);
  overflow: hidden;
}

.bar-fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--brand-600);
}

.bar-n {
  text-align: right;
  color: var(--text-muted);
}

/* --- tables --------------------------------------------------------------- */

.kv,
.history {
  width: 100%;
  border-collapse: collapse;
}

.kv td,
.history td {
  padding: 6px 8px 6px 0;
  border-bottom: 1px solid var(--border-soft);
  vertical-align: top;
}

.kv tr:last-child td,
.history tr:last-child td {
  border-bottom: 0;
}

/* A destination is one long unbroken token; without this it would force the
   table wider than the viewport. */
.wrapany {
  overflow-wrap: anywhere;
}

.checkline {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 600;
}

.checkline input {
  width: auto;
}

code {
  font-size: 0.9em;
  overflow-wrap: anywhere;
}

/* --- admin ---------------------------------------------------------------- */

/*
 * The admin console header. Same .site-header shape as the personal one, with
 * two differences on purpose: the mark carries an accent (green, not brand
 * blue) so the eye catches "different mode" before reading a single word, and
 * the nav is the four admin sections instead of My codes/Account — nothing
 * from the personal dashboard is present, which is the whole point.
 */
.site-header-admin {
  border-top: 2px solid var(--accent-500);
}

.mark-admin {
  background: var(--accent-600);
  box-shadow: 0 10px 20px -8px rgb(16 185 129 / 0.6);
}

/* Marks which of the four admin sections is current, the same way a browser
   tab shows which one is open. site-nav itself needs no other change — its
   normal a/a:hover rules already apply to these links. */
.site-nav a[aria-current="page"] {
  background: var(--surface-panel);
  color: var(--text-primary);
  font-weight: 600;
}

.filter-row {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
}

/* The text input takes the slack; the select and buttons keep their size. */
.filter-row input[type="text"] {
  flex: 1 1 14rem;
  width: auto;
}

/* Takes its look from the shared control rule above; only the sizing differs,
   because in this row the select sits inline instead of filling a field. */
.filter-row select {
  width: auto;
  flex: none;
  font-size: 0.875rem;
}

/*
 * A wide table has to scroll inside its own box. Without this the page body
 * scrolls sideways instead, which on a phone moves the header and the nav off
 * screen along with it.
 */
.table-scroll {
  overflow-x: auto;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.8125rem;
}

.data-table th {
  text-align: left;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-muted);
  padding: 0 12px 8px 0;
  border-bottom: 1px solid var(--border-strong);
  white-space: nowrap;
}

.data-table td {
  padding: 8px 12px 8px 0;
  border-bottom: 1px solid var(--border-soft);
  vertical-align: top;
}

.data-table tr:last-child td {
  border-bottom: 0;
}

/* Figures line up column-wise only if the digits are the same width. */
.data-table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  padding-right: 12px;
}

/* --- motion --------------------------------------------------------------- */

/*
 * The static app has an entrance animation and hover feedback; this app had
 * neither, which was the single clearest reason it read as the lesser product
 * despite using the same palette.
 *
 * Everything here is inside prefers-reduced-motion so a visitor who has asked
 * their OS for less movement gets the previous, entirely static behaviour.
 *
 * Note the fill mode: `backwards`, never `both`. `both` implies `forwards`,
 * which makes the final keyframe persist in the animation origin — and that
 * origin outranks normal declarations, so the `transform: none` in the last
 * keyframe would permanently defeat the :hover lift below. With `backwards`
 * the element returns to its normal styles once the animation ends, and the
 * end state already matches those styles, so nothing jumps.
 */
@media (prefers-reduced-motion: no-preference) {
  @keyframes fade-up {
    from {
      opacity: 0;
      transform: translateY(6px);
    }
    to {
      opacity: 1;
      transform: none;
    }
  }

  .card {
    animation: fade-up 0.35s cubic-bezier(0.22, 1, 0.36, 1) backwards;
  }

  .btn,
  .copy,
  .linklike,
  .site-nav a {
    transition:
      background-color 0.2s,
      border-color 0.2s,
      color 0.2s,
      transform 0.2s;
  }

  .code-row {
    transition:
      transform 0.2s,
      border-color 0.2s;
  }

  .code-row:hover {
    transform: translateY(-2px);
    border-color: var(--border-strong);
  }

  .btn:hover {
    transform: translateY(-1px);
  }
}

/* --- narrow screens ------------------------------------------------------- */

@media (max-width: 34rem) {
  /* Matches the static app's p-4 below its sm breakpoint. */
  .card {
    padding: 16px;
  }

  .code-row {
    flex-wrap: wrap;
  }

  .code-side {
    text-align: left;
    width: 100%;
  }

  .site-header {
    flex-wrap: wrap;
  }

  .header-titles {
    flex-basis: 60%;
  }

  .bars li {
    grid-template-columns: 2.75rem 1fr 2rem;
  }
}
