/* Builder — the Projects app.
   Spec: specs/SPEC-BUILDER-V4-PROJECTS-2026-09-08.md
   ⛔ THE FRAME IS THE SHARED ONE. .rhead, .osAppCard, .osAppBody and .osAppBar
      live in os-app.css and are not re-declared here — a second frame class is
      how two apps come to look like two products. Everything below is the
      project CARD and the specification, which no other app has.
   ⛔ ONE DESIGN (owner, 2026-09-04). The clay world, `data-theme` forced; there
      is no dark block here because there is no dark theme to write one for. */

/* ── the split: the list owns the column until a click has an answer ────── */
/* ⛔ ONE column by default. The old grid split the frame 32/68 even with
   nothing open, so every card lived in a strip a third of the screen wide and
   grew tall to fit. The split exists only while `.hasDetail` is on. */
.osApp .pjSplit{display:grid;grid-template-columns:1fr;gap:18px;align-items:start}
.osApp .pjSplit.hasDetail{grid-template-columns:minmax(240px,300px) 1fr}
@media (max-width:820px){
  /* At phone width the columns stack and the detail takes the screen; the
     crumb inside it is what walks back. */
  .osApp .pjSplit.hasDetail{grid-template-columns:1fr}
  .osApp .pjSplit.hasDetail .pjList{display:none}
}

/* ── groups ──────────────────────────────────────────────────────────────── */
.osApp .pjGroup{margin:0 0 22px}
.osApp .pjGroup h3{
  display:flex;align-items:center;gap:8px;margin:0 0 10px;
  font:650 12px/1 var(--disp);letter-spacing:.08em;text-transform:uppercase;color:var(--muted);
}
.osApp .pjGroup h3 .c{
  font:600 11px/1 var(--mono);color:var(--faint);
  background:var(--surf);border-radius:999px;padding:3px 7px;
}
.osApp .pjCards{display:flex;flex-direction:column;gap:12px}

/* ── the card: a ROW — picture left, the project right ───────────────────── */
.osApp .pjCard{
  display:grid;grid-template-columns:300px minmax(0,1fr);gap:20px;align-items:start;
  width:100%;min-height:44px;text-align:left;cursor:pointer;
  background:var(--panel);border:1px solid var(--line2);border-radius:16px;
  padding:12px;box-shadow:var(--lift);
  transition:border-color .15s,transform .15s,box-shadow .15s;
}
.osApp .pjCard:hover{border-color:var(--line);transform:translateY(-1px);box-shadow:var(--lift-2)}
.osApp .pjCard.on{border-color:var(--ink)}
/* ⛔ AMBER, and only for the one thing a person can unblock. */
.osApp .pjCard.ask{border-color:var(--amber);box-shadow:0 0 0 1px var(--amber) inset,var(--lift)}
@media (max-width:1100px){ .osApp .pjCard{grid-template-columns:240px minmax(0,1fr);gap:16px} }
@media (max-width:640px){
  /* On a phone the picture sits on top at full width; the text follows. */
  .osApp .pjCard{grid-template-columns:1fr;gap:12px;padding:10px 10px 12px}
}

/* The 16:10 slot NEVER changes size — what fills it is what changes. */
.osApp .pjShot{
  aspect-ratio:16/10;border-radius:10px;overflow:hidden;background:var(--surf);
  display:flex;align-items:center;justify-content:center;position:relative;
  border:1px solid var(--line);
}
/* ⛔ TOP-ALIGNED. A page's header is what a person recognises; centring a
   1440x900 capture in a 16:10 slot crops away the half that identifies it. */
.osApp .pjShot img{width:100%;height:100%;object-fit:cover;object-position:top center;display:block}
/* ⛔ NOT A GREY BOX, and ⛔ NOT THE CAPTION AGAIN. A project with no picture
   shows its kind's mark and the served reason — the one-liner lives once,
   under the name, where it belongs. */
.osApp .pjShot.pjNone{
  flex-direction:column;gap:8px;padding:16px;text-align:center;
  background:color-mix(in srgb,var(--panel) 60%,var(--surf));
}
.osApp .pjNone .pjGl{width:34px;height:34px;color:var(--faint);opacity:.7}
.osApp .pjNone .pjWhy{font:400 11.5px/1.3 var(--mono);color:var(--faint);max-width:24ch}

/* ── the right half ──────────────────────────────────────────────────────── */
.osApp .pjBody{display:flex;flex-direction:column;gap:8px;min-width:0;padding:2px 2px 0 0}
.osApp .pjHead{display:flex;align-items:center;gap:9px;flex-wrap:wrap;min-width:0}
/* The site's OWN favicon — a real logo, framed like one. */
.osApp .pjLogo{
  flex:0 0 auto;width:28px;height:28px;border-radius:8px;object-fit:cover;
  background:var(--cream2);border:1px solid var(--line);display:inline-flex;
  align-items:center;justify-content:center;color:var(--muted);
}
.osApp .pjLogo.none .pjGl{width:16px;height:16px}
.osApp .pjName{font:700 17px/1.25 var(--disp);color:var(--ink);letter-spacing:-.01em;min-width:0}
/* The state word comes from the schema, its tone from the schema too. */
.osApp .pjState{
  font:500 11px/1 var(--mono);letter-spacing:.04em;padding:4px 8px;border-radius:999px;
  color:var(--muted);background:var(--surf);
}
.osApp .pjState.ok{color:var(--mint);background:var(--clay-mint)}
.osApp .pjState.amber{color:var(--amber);background:var(--clay-peach)}
.osApp .pjState.rose{color:var(--rose);background:color-mix(in srgb,var(--rose) 12%,transparent)}
/* The description as typography: one size up, ink, two lines at most. */
.osApp .pjOne{
  font:400 14px/1.5 var(--body);color:var(--ink);opacity:.86;max-width:62ch;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
}

/* ── the capability strip: what it is MADE OF ────────────────────────────── */
.osApp .pjCaps{display:flex;gap:6px;flex-wrap:wrap;margin-top:2px}
.osApp .pjCap{
  display:inline-flex;align-items:center;justify-content:center;
  width:34px;height:34px;border-radius:9px;border:1px solid transparent;
  color:var(--faint);transition:transform .12s;
}
.osApp .pjCap .pjGl{width:20px;height:20px}
/* served — present and legible: filled clay-mint, mint glyph. */
.osApp .pjCap.on{color:var(--mint);background:var(--clay-mint);border-color:color-mix(in srgb,var(--mint) 25%,transparent)}
/* declared, not in this version — DECIDED. A dashed amber edge, muted glyph:
   quieter than served, and never a red anything. */
.osApp .pjCap.mid{color:var(--muted);border:1px dashed color-mix(in srgb,var(--amber) 55%,transparent);background:transparent}
/* not needed — the person's own answer. Bare and faint; no edge at all. */
.osApp .pjCap.off{color:var(--faint);opacity:.55}
/* not read yet — the pane has not seen this project's spec. Dotted, so it
   reads as "unknown" rather than as any of the three real states. */
.osApp .pjCap.unk{color:var(--faint);opacity:.6;border:1px dotted var(--line2)}
.osApp .pjCard:hover .pjCap.on{transform:translateY(-1px)}

/* ── base stats: mono numbers, quiet words ───────────────────────────────── */
.osApp .pjStats{display:flex;gap:16px;flex-wrap:wrap;margin-top:2px}
.osApp .pjStats .st{display:inline-flex;align-items:baseline;gap:5px;font:400 11.5px/1 var(--mono);color:var(--faint)}
.osApp .pjStats .st b{font:600 12.5px/1 var(--mono);color:var(--ink)}
.osApp .pjStats:empty{display:none}

/* ── compact once a detail is open: the list becomes a spine ─────────────── */
.osApp .pjSplit.hasDetail .pjCard{grid-template-columns:88px minmax(0,1fr);gap:12px;padding:8px;border-radius:12px;box-shadow:none}
.osApp .pjSplit.hasDetail .pjShot{border-radius:7px}
.osApp .pjSplit.hasDetail .pjNone .pjGl{width:18px;height:18px}
.osApp .pjSplit.hasDetail .pjNone .pjWhy{display:none}
.osApp .pjSplit.hasDetail .pjOne,.osApp .pjSplit.hasDetail .pjCaps,
.osApp .pjSplit.hasDetail .pjStats .st:nth-child(n+3),.osApp .pjSplit.hasDetail .pjLogo{display:none}
.osApp .pjSplit.hasDetail .pjName{font-size:13.5px}
.osApp .pjSplit.hasDetail .pjBody{gap:5px}
.osApp .pjSplit.hasDetail .pjState{display:none}

/* Under the detail's hero picture: the same strip and stats, one line. */
.osApp .pjDetStrip{display:flex;align-items:center;gap:18px;flex-wrap:wrap;margin:10px 0 0}
.osApp .pjDetStrip .pjStats{margin:0}

/* ── the live strip · ⛔ NO BAR ANYWHERE, and none at all while waiting ──── */
.osApp .pjLive{display:flex;gap:10px;align-items:center;margin:2px 0 0}
.osApp .pjSteps,.osApp .pjEta{font:400 11.5px/1 var(--mono);color:var(--muted)}
.osApp .pjAsk{
  margin:4px 0 0;padding:9px 10px;border-radius:10px;
  background:color-mix(in srgb,var(--amber,#b45309) 8%,var(--panel));
  border:1px solid color-mix(in srgb,var(--amber,#b45309) 30%,transparent);
}
.osApp .pjAsk p{margin:0 0 7px;font:500 12.5px/1.4 var(--body);color:var(--ink)}
.osApp .pjAskRow{display:flex;gap:7px}
.osApp .pjAskRow input{
  flex:1;min-width:0;border:1px solid var(--line2);border-radius:8px;
  padding:6px 9px;font:400 12.5px var(--body);background:var(--bg);color:var(--ink);
  min-height:44px;
}
/* ⛔ The nested controls are the only things inside the card a finger may
   miss — 44px, both of them. */
.osApp .pjAskRow .pjBtn{min-height:44px}
.osApp .pjGo{display:inline-flex;align-items:center;min-height:44px;padding:0 6px}

/* ── buttons ─────────────────────────────────────────────────────────────── */
.osApp .pjBtn{
  border:1px solid var(--line2);border-radius:9px;background:var(--surf);
  color:var(--ink);font:600 12.5px var(--body);padding:7px 12px;cursor:pointer;
  /* 44px hit area even though the box is smaller (DESIGN.md). */
  min-height:34px;
}
.osApp .pjBtn:hover{background:var(--surf2)}
.osApp .pjBtn.ghost{background:transparent;color:var(--muted);font-weight:500}
.osApp .pjBtnNote{font:400 11.5px/1.4 var(--mono);color:var(--faint)}
.osApp .pjRefused{display:block;margin-top:7px;font:400 12px/1.4 var(--body);color:var(--bad,#dc2626)}
.osApp .pjActions{display:flex;flex-direction:column;gap:5px;margin:14px 0}

/* ── the detail ──────────────────────────────────────────────────────────── */
.osApp .pjDetTop{display:flex;align-items:baseline;gap:10px;margin:0 0 12px}
.osApp .pjBack{
  border:0;background:none;color:var(--muted);cursor:pointer;
  font:500 12.5px var(--body);padding:0;
}
.osApp .pjDetTop h2{margin:0;font:650 18px/1.25 var(--disp);color:var(--ink)}
/* §3 of the app standard: the SUBJECT is the project; the app's name is the
   subline, never the headline. */
.osApp .pjSub{font:400 11px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}

.osApp .pjStory{margin:0;font:400 13.5px/1.7 var(--body);color:var(--ink)}
/* ⛔ THE MARK THAT KEEPS THE WHOLE THING HONEST. A dotted underline says "we
   guessed this" — without it a person cannot tell their own words from ours,
   and stops trusting every line on the page. */
.osApp .pjGuess{border-bottom:1px dotted var(--muted);cursor:help}

/* ── facets: five rows, never five tables ────────────────────────────────── */
.osApp .pjFacet{border-top:1px solid var(--line2);padding:10px 2px}
.osApp .pjFacet:first-of-type{border-top:0}
.osApp .pjFacet.dim{opacity:.55}
.osApp .pjFacetTop{display:flex;align-items:center;gap:8px}
.osApp .pjFacetName{font:600 13px/1 var(--disp);color:var(--ink)}
.osApp .pjFacetState{font:400 11.5px/1 var(--mono);color:var(--muted)}
.osApp .pjFacetTop .c{margin-left:auto;font:600 11px/1 var(--mono);color:var(--faint)}
.osApp .pjFacetSay{margin:4px 0 0 26px;font:400 12.5px/1.5 var(--body);color:var(--muted)}
/* ⛔ AMBER — "decided against", never rose. Rose is "we tried and it broke",
   and reading one as the other turns an honest limit into a bug report. */
.osApp .pjWhyNot{
  margin:6px 0 0 26px;padding:7px 9px;border-radius:8px;
  font:400 12px/1.5 var(--body);color:var(--ink);
  background:color-mix(in srgb,var(--amber,#b45309) 8%,transparent);
}
.osApp .pjFeat{list-style:none;margin:7px 0 0 26px;padding:0}
.osApp .pjFeat li{display:flex;gap:7px;padding:2px 0;font:400 12.5px/1.5 var(--body);color:var(--ink)}
.osApp .pjFeat li.mid{color:var(--muted)}
.osApp .pjFeat .mk{color:var(--faint)}

/* The technical fold — the ONLY place dependencies, configs and params live. */
.osApp details.tech{margin:8px 0 0 26px}
.osApp details.tech summary{
  cursor:pointer;font:400 11.5px/1 var(--mono);color:var(--faint);list-style:none;
}
.osApp details.tech summary::-webkit-details-marker{display:none}
.osApp .pjTechRow{display:flex;gap:10px;padding:3px 0;font:400 11.5px/1.5 var(--mono)}
.osApp .pjTechRow .k{color:var(--muted);min-width:12ch}
.osApp .pjTechRow .v{color:var(--ink)}
.osApp .pjTechRow .v.t{font-family:var(--body);color:var(--muted)}

/* ── versions: the diff is the point, not the timestamp ──────────────────── */
.osApp .pjVer{display:flex;align-items:center;gap:10px;padding:8px 2px;border-top:1px solid var(--line2)}
.osApp .pjVer:first-of-type{border-top:0}
.osApp .pjVerMid{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.osApp .pjVerTop{font:400 11.5px/1 var(--mono);color:var(--muted)}
.osApp .pjVerDelta{font:400 12.5px/1.45 var(--body);color:var(--ink)}
.osApp .pjGo{font:500 12px var(--body);color:var(--muted);text-decoration:none}
.osApp .pjGo:hover{color:var(--ink)}

.osApp .osAppFoot{margin-top:16px;font:400 11.5px/1.65 var(--body);color:var(--faint)}

/* ── the stages: what a project can become ───────────────────────────────
   ⛔ NOT A PROGRESS BAR, and the CSS must not let it read as one: no
      connecting line, no fill, no "you are here". A project may stop at any
      stage and be finished, and drawing them as steps toward completion would
      tell somebody with a good one-page site that they are a quarter done. */
.osApp .pjStages{margin:14px 0 0;display:flex;flex-direction:column;gap:1px}
.osApp .pjStage{
  display:flex;align-items:baseline;gap:11px;padding:11px 12px;
  background:var(--panel);border:1px solid var(--line2);
}
.osApp .pjStage:first-child{border-radius:12px 12px 0 0}
.osApp .pjStage:last-child{border-radius:0 0 12px 12px}
.osApp .pjStage + .pjStage{border-top:0}
/* The number is an ordinal, not a score — muted, and never coloured by state. */
.osApp .pjStageN{
  flex:0 0 auto;width:20px;text-align:center;
  font:600 11px/1 var(--mono);color:var(--faint);
}
.osApp .pjStageTx{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.osApp .pjStageTx .k{font:650 13px/1.3 var(--disp);color:var(--ink)}
.osApp .pjStageTx .w{font:400 12.5px/1.5 var(--body);color:var(--muted)}
/* ⛔ The time is checkable, so it is mono like every other number a person
   could hold us to. */
.osApp .pjStageT{flex:0 0 auto;font:400 11.5px/1 var(--mono);color:var(--faint)}
@media (max-width:520px){
  .osApp .pjStage{flex-wrap:wrap}
  .osApp .pjStageT{width:100%;padding-left:31px}
}

/* The company a card belongs to — shown only when the list spans more than one. */
.osApp .pjWs{
  display:inline-block;margin:5px 2px 0;padding:2px 7px;border-radius:999px;
  font:500 10.5px/1.5 var(--mono);color:var(--muted);
  background:var(--surf);border:1px solid var(--line2);
}

/* ── the refresh button on a picture ─────────────────────────────────────
   ⛔ HIDDEN UNTIL THE POINTER IS OVER THE PICTURE. Eight cards with a visible
      button each is eight controls competing with the thing they sit on.
   ⚠ Always visible to the keyboard (:focus-visible) and on a touch screen,
      where there is no hover and a hidden control is an absent one. */
.osApp .pjShot{position:relative}
.osApp .pjShotBtn{
  position:absolute;top:8px;right:8px;width:30px;height:30px;
  display:grid;place-items:center;border-radius:8px;cursor:pointer;
  border:1px solid var(--line2);background:color-mix(in srgb,var(--panel) 88%,transparent);
  color:var(--muted);font:400 15px/1 var(--body);
  opacity:0;transition:opacity .14s,color .14s;
  /* 44px touch target without a 44px box (DESIGN.md). */
  box-shadow:0 0 0 7px transparent;
}
.osApp .pjShot:hover .pjShotBtn,
.osApp .pjShotBtn:focus-visible{opacity:1}
.osApp .pjShotBtn:hover{color:var(--ink);background:var(--panel)}
@media (hover:none){ .osApp .pjShotBtn{opacity:.75} }

/* ⚠ A div with role=button loses the focus ring a real <button> had for free.
   It has to be drawn back or the card becomes invisible to keyboard users —
   the cost of the nesting rule that forced the div in the first place. */
.osApp .pjCard:focus-visible{
  outline:2px solid var(--ink);outline-offset:2px;border-color:var(--ink);
}

/* The count of components nobody has described yet — carried, never drawn as
   absent. ⛔ Quieter than a mark, because it is a fact about our knowledge and
   not about the project. */
.osApp .pjCapMore{
  display:inline-grid;place-items:center;min-width:24px;height:24px;padding:0 5px;
  border-radius:7px;border:1px dashed var(--line2);
  font:600 10.5px/1 var(--mono);color:var(--faint);
}
