/* ==========================================================================
   Composer — word entry, composition strip, price panel.
   Photograph presentation lives in photograph.css and is not overridden here.
   ========================================================================== */

.composer { display: grid; gap: var(--space-5) var(--space-6); }

.composer__entry { max-width: var(--measure); }

.composer__entry-row { display: flex; gap: var(--space-3); align-items: flex-start; }
.composer__entry-row .text-input { flex: 1; min-width: 0; }

.counter {
  display: flex;
  justify-content: space-between;
  font-size: 0.8125rem;
  color: var(--ink-soft);
  margin-top: var(--space-2);
  font-variant-numeric: tabular-nums;
}

.counter--over { color: #8c2f24; font-weight: 500; }

/* --- composition strip --------------------------------------------------- */

/*
  The photographs fill the width they are given, at every viewport.

  This used to be a wrapping flex row of fixed-width buttons
  (`width: clamp(4.5rem, 17vw, 7.5rem)`), and the wide-viewport stage below
  replaced it with a `minmax()` grid so a short word could render large. That
  fix was inside `@media (min-width: 46rem)` and so never reached a phone:
  measured on production 2026-09-12, composing MOM on a 375px viewport rendered
  each photograph at 54 x 81 px inside a 343px container — the product, on the
  device most people will meet it on, smaller than an app icon.

  Same technique, now unconditional. `minmax(4.5rem, 1fr)` lets three letters
  take a third of the stage each and twelve fall back to the 4.5rem floor and
  scroll, rather than shrinking into unreadable confetti. The floor is what
  keeps a long word's letters legible and tappable — these are buttons that
  open the picker, not just pictures. The wide-viewport block below swaps the
  `1fr` for a 320px cap, because a stage 900px wide should not stretch three
  photographs to 300px each when 320 is the ceiling decision 0021 allows.
*/
.composition {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(4.5rem, 1fr);
  justify-content: start;
  overflow-x: auto;
  gap: var(--space-3);
  list-style: none;
  margin: 0;
  padding: 0;
}

/*
  `#result` is a grid item of `.composer` at narrow widths, and a grid item
  defaults to `min-width: auto` — sized to its content's own minimum. Without
  this, a long word's photo strip pushes this box wider than the column, the
  `overflow-x: auto` above never gets a constrained width to scroll inside, and
  the whole PAGE scrolls sideways instead of the strip.

  Measured while making this change: GRANDMOTHER on a 375px viewport gave
  `#result` a width of 872px and `documentElement.scrollWidth > clientWidth`.

  The wide-viewport block sets `#result { display: contents }`, which removes
  this box altogether — which is why the same bug never appeared on desktop and
  why the `min-width: 0` there is on `.composer__composition` instead. Decision
  0043 hit the identical trap on the frame cards.
*/
#result { min-width: 0; }

.position { margin: 0; }

/* The interactive wrapper. Hover and focus change THIS element — never the
   photograph inside it. No transform here: it would scale the image. */
.position__button {
  display: grid;
  gap: var(--space-2);
  /* The grid column decides the width now, at every viewport — see
     `.composition` above for why this stopped being a fixed clamp. */
  width: 100%;
  padding: var(--space-2);
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: transparent;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-align: center;
  transition: background 120ms ease, border-color 120ms ease;
}

.position__button:hover { background: var(--paper-warm); border-color: var(--rule); }

.position__button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;   /* clear of the photograph */
}

.position__meta {
  display: grid;
  gap: 1px;
  font-size: 0.6875rem;
  line-height: 1.3;
}

.position__char {
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.08em;
}

.position__code {
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}

/* Forced repeat: shape and text, never colour alone, and always outside the
   image. Says "we ran out of variants", not "something went wrong". */
.position__badge {
  justify-self: center;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 0.625rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 1px var(--space-2);
  border-radius: 999px;
  border: 1px solid #e8dbb4;
  background: var(--warn-bg);
  color: var(--warn-ink);
}

.position__badge::before { content: "\21BB"; font-size: 0.75rem; line-height: 1; }

.position--handpicked .position__code { color: var(--ink-soft); font-weight: 500; }

/* --- summary panel ------------------------------------------------------- */

.summary {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--space-5);
  background: var(--paper-warm);
  max-width: 26rem;
  display: grid;
  gap: var(--space-4);
}

.summary__title { font-size: var(--step-1); font-weight: 500; }

.summary__what {
  font-size: 0.9375rem;
  color: var(--ink-soft);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--rule);
}

.lines { display: grid; gap: var(--space-3); font-size: 0.9375rem; }

.line { display: flex; justify-content: space-between; gap: var(--space-4); align-items: baseline; }
.line__label { display: grid; gap: 1px; }
.line__detail { font-size: 0.8125rem; color: var(--ink-soft); }
.line__amount { font-variant-numeric: tabular-nums; white-space: nowrap; }
.line__amount--pending { color: var(--ink-soft); font-size: 0.8125rem; font-style: italic; }

.line--total {
  border-top: 1px solid var(--rule-strong);
  padding-top: var(--space-3);
  font-weight: 500;
}

.summary__note { font-size: 0.8125rem; color: var(--ink-soft); }

.summary__actions { display: flex; flex-wrap: wrap; gap: var(--space-3); }

.empty-state {
  color: var(--ink-soft);
  padding: var(--space-7) 0;
  max-width: var(--measure);
}

@media (max-width: 32rem) {
  .composer__entry-row { flex-direction: column; }
  .composer__entry-row .button { width: 100%; justify-content: center; }
  .composition { gap: var(--space-2); }
}

/*
  Rail + stage. Below this, everything stacks in document order (entry,
  composition, summary) exactly as it always has — this is a wide-viewport
  layout only. Still placed last, after every unconditional rule it overrides —
  an override earlier in the file loses to a later unconditional rule of equal
  specificity regardless of the media query, which is the bug this ordering
  avoids. (`.position__button`'s width used to be the specific rule at risk
  here. It is `width: 100%` unconditionally now, so this block no longer has to
  outrank it — the ordering stays because `.composition` still does.)

  `#result` renders `.composer__composition` and `.summary` as siblings
  (composer.js owns that and is not changed for this). `display: contents`
  drops its own box so those two children become direct grid items of
  `.composer` and can be placed independently, without composer.js knowing
  or caring that the grid exists.
*/
@media (min-width: 46rem) {
  .composer { grid-template-columns: 22rem 1fr; }

  .composer__entry { grid-column: 1; grid-row: 1; }
  #editing-banner:not(:empty) { grid-column: 1 / -1; }
  #result { display: contents; }

  /* The stage: photographs (once composed) or the empty-state prompt
     (before). `min-width: 0` is required here — a grid item defaults to
     `min-width: auto`, sized to fit its content's own minimum, which would
     let a long word's photo row grow this column wider than the stage
     instead of clipping/shrinking inside it (the same fix already made for
     the frame cards, decision 0043). */
  .composer__composition, .empty-state { grid-column: 2; grid-row: 1 / span 2; min-width: 0; }

  /*
    The rail holds the price and the buy button, and it stays in view.

    Measured on production 2026-09-12 at 1440x900 with MOM composed: "Add to
    basket" sat at y = 962 in a 1552px document — below the fold, on the page
    whose whole job is to get something into the basket. A phone already had
    this solved (the fixed `.sticky-bar` below, decision 0048); the desktop rail
    is the equivalent and was simply never pinned.

    `align-self: start` is required, not decoration: a grid item stretches to
    its row by default, and an element as tall as its scroll container can never
    become stuck.
  */
  .summary {
    grid-column: 1;
    grid-row: 2;
    position: sticky;
    top: var(--space-5);
    align-self: start;
  }

  /*
    A short word should look like it belongs on the stage and a long one
    should still read as a word rather than shrink into unreadable confetti.
    `photograph.css`'s `.word--natural` already solved exactly this for the
    order-confirmation grid — `grid-auto-columns: minmax(0, 320px)` caps a
    photo at natural size and shrinks the columns to fit when there isn't
    room, because the minimum is 0, not `auto`. Reused here with a 4.5rem
    floor instead of 0 (matching today's mobile size), because unlike a
    read-only confirmation grid these are tap targets with a letter and,
    sometimes, a repeat badge under the photo — below that floor they stop
    being legible or tappable, so a very long word scrolls the stage
    horizontally instead of shrinking further.
  */
  /*
    The only thing the stage changes about the photographs is their ceiling.
    Everything else — the grid, the floor, the horizontal scroll for a long
    word — is now unconditional, so a phone gets the same treatment. A stage
    this wide would stretch three photographs well past the 320px that decision
    0021 allows, so `1fr` becomes a cap here.
  */
  .composition { grid-auto-columns: minmax(4.5rem, 320px); }
}

/*
  The sticky mobile buy bar. Above 46rem the rail already keeps the real
  "Add to basket" button in view at all times, so this stays hidden there
  regardless of the `hidden` attribute — `display: none` below is the only
  declaration for `.sticky-bar` outside the media query, so there is no
  unconditional rule elsewhere for the query below to have to outrank (the
  cascade-order trap decision 0044 found does not apply here: that bug
  needed a *later* unconditional rule of equal specificity to exist).

  Lives outside #result in the page markup (composer.js grabs it once in
  mountComposer) rather than being something renderResult() recreates —
  that function tears down and rebuilds #result's children on every
  composition, randomize and photo swap, which would flicker a fixed bar
  and need its own re-mount logic for no reason.

  z-index 800, below the activity launcher/panel/readout's 900-902 — those
  are QC instruments (gated behind ACTIVITY_LOG, off by default including
  in production) that already float in this same bottom-right corner, and
  a lower z-index here means they simply sit visibly on top during a QC
  pass rather than needing bespoke mutual-avoidance logic.
*/
.sticky-bar { display: none; }

@media (max-width: 46rem) {
  .sticky-bar:not([hidden]) {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 800;
    padding: var(--space-3) var(--space-4);
    padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom));
    background: var(--paper);
    border-top: 1px solid var(--rule-strong);
    box-shadow: 0 -2px 12px rgba(0, 0, 0, .08);
  }

  .sticky-bar__info { display: grid; gap: 1px; min-width: 0; }
  .sticky-bar__word {
    font-weight: 500;
    letter-spacing: 0.04em;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .sticky-bar__price { font-size: 0.875rem; color: var(--ink-soft); font-variant-numeric: tabular-nums; }

  .sticky-bar__button { flex: none; padding: 0.625rem 1.125rem; }

  /* Reserves the space the fixed bar itself doesn't take in document flow,
     so scrolling to the very bottom of the page never tucks the footer
     underneath it. Toggled by composer.js alongside the bar's own
     visibility, on <body> rather than .page, so it applies regardless of
     which element the bar's z-index stacking context actually sits in. */
  body.has-sticky-bar .page {
    padding-bottom: calc(4.5rem + env(safe-area-inset-bottom));
  }
}
