/* ============================================================================
   Product canvas — the wiring, drawn as a graph, beside what is missing from it

   Three regions, and they answer three different questions.

   The RAIL, down the left, is everything this installation can see that the
   product does not claim. One connection here serves 135 repositories, so the
   rail is a search box first and a list second: chips narrow it to one family,
   the count says how much is hidden, and the matched text is highlighted
   because nine results is still nine boxes somebody has to read.

   The STEP STRIP, under the bar, is the half a picture cannot draw. A graph
   shows what is there; every sentence on the strip is about something that is
   not, and about the screen that stays blank until it is.

   The STAGE is the product. Nodes are absolutely placed from two custom
   properties the script writes (--node-x, --node-y) after it has measured them
   and swept each lane to uncross the wires; the wires are one SVG underlay
   sized to the same plane, and the plane scales under a zoom layer so a product
   with twenty repositories still fits on a screen.
   ========================================================================== */

/* The one page whose subject is width. Everything else in this application is a column of prose,
   a table or a grid of cards, and --content-max is right for all of them; a graph four lanes wide
   inside a 1280px cap spends a third of its area on a horizontal scrollbar. Written as a :has()
   rule in this sheet rather than a class on <main>, because _Layout renders that element and a
   page cannot reach it. Raised rather than removed: the header above the canvas is prose, and prose
   set 2500 pixels wide is prose nobody finishes a line of. */
.app-main:has(.canvas-shell) { max-width: 1800px; }

.canvas-shell {
    display: grid;
    grid-template-columns: 320px 1fr;
    gap: var(--space-8);
    align-items: start;
}

/* Every write is a fetch, and a screen that looks identical while one is in flight is a screen
   people press twice. The whole shell dims rather than one button, because the answer replaces the
   whole shell. */
.canvas-busy { opacity: 0.72; }
.canvas-busy .canvas-node { cursor: progress; }

/* ---- the rail ---------------------------------------------------------- */

.canvas-rail {
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
    background: var(--surface);
    border: var(--border-width) solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-8);
    /* Tall enough to sit beside the stage and scroll on its own, so dragging
       from the bottom of a long list never scrolls the page out from under the
       node being dropped on. */
    max-height: 720px;
    overflow-y: auto;
}

/* The search box is the first thing in the rail because finding a repository among a hundred and
   thirty-five is the job, and it used to be an unlabelled input the same size as everything else.
   The glyph inside it is what makes it read as a search field rather than as another form row. */
.canvas-search {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-4) var(--space-6);
    border: var(--border-width) solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface-alt);
    color: var(--text-muted);
}

.canvas-search:focus-within {
    border-color: var(--brand);
    box-shadow: var(--shadow-inset-focus);
}

.canvas-search-input {
    flex: 1;
    min-width: 0;
    font-family: inherit;
    font-size: var(--text-sm);
    color: var(--text);
    background: transparent;
    border: 0;
    outline: none;
}

.canvas-rail-count {
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-subtle);
}

.canvas-rail-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-10);
}

.canvas-rail-title {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-wider);
    text-transform: uppercase;
    color: var(--text-muted);
}

/* How many are in this family, beside its name. The number is the argument for typing something
   into the search box rather than scrolling. */
.canvas-rail-tally {
    font-variant-numeric: tabular-nums;
    color: var(--text-subtle);
}

.canvas-rail-empty {
    font-size: var(--text-sm);
    color: var(--text-muted);
    line-height: var(--leading-snug);
}

.canvas-rail-group {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

/* The cap, and the way past it. It says the whole number rather than "more", because a reader
   deciding whether to search needs to know whether it is six or a hundred and six. */
.canvas-rail-more {
    font-family: inherit;
    font-size: var(--text-xs);
    padding: var(--space-3) var(--space-4);
    border: var(--border-width) dashed var(--border-strong);
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
}

.canvas-rail-more:hover {
    border-color: var(--brand);
    color: var(--brand);
}

/* What the search matched, where it matched it. */
.canvas-hit {
    background: var(--warning-soft);
    color: var(--warning-on-soft);
    border-radius: var(--radius-xs);
}

.canvas-filters {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

/* A filter is a toggle, not a link: it says what the rail is showing and the
   pressed one is the state. aria-pressed carries that to a reader who cannot
   see the fill, which is why the selector reads the attribute rather than a
   second class the script would have to keep in step. */
.canvas-filter {
    font-family: inherit;
    font-size: var(--text-2xs);
    padding: var(--space-1) var(--space-4);
    border-radius: var(--radius-pill);
    border: var(--border-width) solid var(--border);
    background: var(--surface);
    color: var(--text-muted);
    cursor: pointer;
}

.canvas-filter:hover { background: var(--surface-hover); }

.canvas-filter[aria-pressed="true"] {
    background: var(--brand-soft);
    border-color: var(--brand);
    color: var(--brand-on-soft);
}

/* ---- the bar ----------------------------------------------------------- */

.canvas-stage {
    background: var(--surface);
    border: var(--border-width) solid var(--border);
    border-radius: var(--radius-lg);
    overflow: hidden;
}

.canvas-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-6);
    padding: var(--space-8);
    border-bottom: var(--border-width) solid var(--border);
    background: var(--surface-alt);
}

.canvas-bar-spacer { flex: 1; }

.canvas-bar-field {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.canvas-bar-field-wide { flex: 1; min-width: 240px; }

.canvas-bar-label {
    font-size: var(--text-2xs);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-wider);
    text-transform: uppercase;
    color: var(--text-muted);
}

.canvas-bar-hint {
    font-weight: var(--weight-normal);
    text-transform: none;
    letter-spacing: var(--tracking-wide);
    color: var(--text-subtle);
}

/* Sync and the way to the dashboard, at the end of the bar. They are what the whole screen builds
   up to, and they used to be a button the size of a business card on a box a drag could push off
   the visible plane. */
.canvas-bar-actions {
    display: flex;
    align-items: center;
    gap: var(--space-4);
}

/* Naming a product is the one act that needs no product, so it is the one row that can be open with
   nothing selected. On a fresh installation this row IS the screen. */
.canvas-create {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-6);
    padding: var(--space-8);
    border-bottom: var(--border-width) solid var(--border);
    background: var(--brand-soft);
}

/* An author rule beats the browser's own [hidden] rule, so any component that sets display has to
   say what hidden means for itself or the attribute stops working. This row shipped open on every
   load because of it. */
.canvas-create[hidden] { display: none; }

/* ---- the chain --------------------------------------------------------- */

.canvas-steps {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: var(--space-px);
    background: var(--border);
    border-bottom: var(--border-width) solid var(--border);
}

/* A rung. A button when pressing it shows the boxes that would fix it, a plain block when there is
   nothing in the rail to press for; both look the same, because a reader should not have to guess
   which sentence is clickable by hovering over four of them. */
.canvas-step {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-6) var(--space-8);
    font-family: inherit;
    text-align: left;
    background: var(--surface);
    border: 0;
    border-top: var(--border-accent) solid var(--border);
}

button.canvas-step { cursor: pointer; }
button.canvas-step:hover { background: var(--surface-hover); }

.canvas-step-title {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    color: var(--text);
}

.canvas-step-detail {
    font-size: var(--text-xs);
    line-height: var(--leading-snug);
    color: var(--text-muted);
}

/* Done is green, missing-and-it-matters is amber, missing-on-purpose is grey. Nothing here is red:
   a product measured from git alone is a working product, and a row of red marks on one is a row
   people learn to read past. */
.canvas-step-done { border-top-color: var(--success); }
.canvas-step-done .canvas-step-title { color: var(--success-on-soft); }
.canvas-step-todo { border-top-color: var(--warning); }
.canvas-step-optional { border-top-color: var(--border-strong); }
.canvas-step-optional .canvas-step-title { color: var(--text-muted); }

.canvas-notice {
    padding: var(--space-5) var(--space-8);
    font-size: var(--text-sm);
    line-height: var(--leading-snug);
    border-bottom: var(--border-width) solid var(--border);
}

.canvas-notice-success { background: var(--success-soft); color: var(--success-on-soft); }
.canvas-notice-warning { background: var(--warning-soft); color: var(--warning-on-soft); }

/* ---- the stage --------------------------------------------------------- */

/* The viewport holds the scrolling plane and the tools that must not scroll with it. A control that
   scrolls away is a control nobody finds twice. */
.canvas-viewport { position: relative; }

.canvas-tools {
    position: absolute;
    top: var(--space-6);
    right: var(--space-6);
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2);
    border: var(--border-width) solid var(--border);
    border-radius: var(--radius-pill);
    background: var(--surface);
    box-shadow: var(--shadow-md);
}

.canvas-tool {
    font-family: inherit;
    font-size: var(--text-2xs);
    padding: var(--space-3) var(--space-6);
    border: 0;
    border-radius: var(--radius-pill);
    background: transparent;
    color: var(--text-body);
    cursor: pointer;
}

.canvas-tool:hover { background: var(--brand-soft); color: var(--brand-on-soft); }

.canvas-zoom-level {
    font-size: var(--text-2xs);
    font-variant-numeric: tabular-nums;
    color: var(--text-subtle);
    min-width: 44px;
    text-align: center;
}

/* The plane. Fixed height for the reason the flow canvas is: a box that grew
   with its contents would move everything below it every time a node was
   dragged in. It scrolls in both directions because a product with twenty
   repositories is wider and taller than any viewport. Nothing on it selects:
   a drag that paints half the labels blue is a drag that looks broken. */
.canvas-board {
    position: relative;
    height: 660px;
    overflow: auto;
    background: var(--surface-sunken);
    cursor: grab;
    user-select: none;
}

.canvas-board-panning { cursor: grabbing; }

/* Something from the rail is over the plane and will land if it is let go. */
.canvas-board-armed { background: var(--dnd-target-bg); }

/* The scaled box. Transform does not change layout, so the scroll extent has to be set here from
   the plane's size times the zoom, or half a zoomed-in graph would be unreachable. */
.canvas-zoom {
    width: var(--zoom-width);
    height: var(--zoom-height);
}

/* The plane the nodes are placed on, which is larger than the box that shows
   it. Sized by the script from where the furthest node landed. */
.canvas-plane {
    position: relative;
    width: var(--plane-width);
    height: var(--plane-height);
    transform: scale(var(--plane-zoom, 1));
    transform-origin: 0 0;
}

.canvas-wires {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    /* The wires are underneath and never swallow a drag aimed at a node. */
    pointer-events: none;
}

.canvas-wire {
    fill: none;
    stroke-width: var(--border-width-thick);
}

/* A wire somebody drew. */
.canvas-wire-link { stroke: var(--brand); }

/* A wire that reports the world: a connection serves a repository whether or
   not anybody here likes it. Dashed and inert, so nobody tries to unplug
   Bitbucket. */
.canvas-wire-serves {
    stroke: var(--border-strong);
    stroke-dasharray: 4 4;
}

/* The target. A two-pixel curve is a two-pixel target, so a transparent stroke wide enough to hit
   with a mouse rides over the hairline that is actually drawn, and it is the thing that carries the
   click that cuts the link. Only the wires somebody drew get one; a fact about the outside world is
   not cuttable and must not react to the pointer at all. */
.canvas-wire-hit {
    fill: none;
    stroke: transparent;
    pointer-events: stroke;
    cursor: pointer;
}

.canvas-wire-hit:hover + .canvas-wire-link,
.canvas-wire-link:hover { stroke: var(--danger); }

.canvas-lane-label {
    position: absolute;
    top: 0;
    left: var(--lane-x);
    padding: var(--space-4) var(--space-5);
    font-size: var(--text-2xs);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-wider);
    text-transform: uppercase;
    color: var(--text-subtle);
    pointer-events: none;
}

/* ---- a node ------------------------------------------------------------ */

.canvas-node {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-6);
    background: var(--surface);
    border: var(--border-width) solid var(--border);
    border-left: var(--border-accent) solid var(--border-strong);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
}

/* Only a box that goes somewhere says it does. A repository nobody has cloned cannot be dropped on
   a product, and a grab cursor on it is a promise the screen then breaks. */
.canvas-node-movable { cursor: grab; }
.canvas-node-movable:active { cursor: grabbing; }

/* On the stage a node is placed and fixed-width, because the lane pitch the script lays out to is
   a number and a box that sized itself would collide with the next lane. In the rail it simply
   fills the rail. One class, two homes. */
.canvas-plane .canvas-node {
    position: absolute;
    left: var(--node-x);
    top: var(--node-y);
    width: 220px;
    z-index: 1;
}

/* A box that has grown past the gap under it, drawn over its neighbours rather than behind them.
   The layout stacks by measured height, so a node that gets taller AFTER it was placed — the one
   being asked a question, and the one being carried — is the only case that can overlap. */
.canvas-plane .canvas-node-dragging,
.canvas-plane .canvas-node:has(.canvas-prompt) { z-index: 3; }

/* And lifted, so it reads as a box in front of its neighbour rather than as two boxes colliding. */
.canvas-plane .canvas-node:has(.canvas-prompt) { box-shadow: var(--shadow-xl); }

.canvas-node-dragging {
    opacity: 0.4;
    box-shadow: var(--shadow-lg);
}

/* The box under the finger while something is being carried out of the rail. Fixed to the pointer
   rather than left in the list, so the reader is dragging the thing rather than a rectangle the
   browser drew for them. */
.canvas-ghost {
    position: fixed;
    left: var(--ghost-x);
    top: var(--ghost-y);
    width: 220px;
    margin: 0;
    transform: translate(-50%, -50%) rotate(-1deg);
    box-shadow: var(--shadow-xl);
    opacity: 0.94;
    pointer-events: none;
    z-index: var(--z-toast);
}

/* The left edge says which family the node belongs to, which is the one thing
   a reader needs at a glance and the one thing a title cannot say from across
   the screen. */
.canvas-node-connection { border-left-color: var(--accent-violet); }
.canvas-node-repository { border-left-color: var(--info); }
.canvas-node-board { border-left-color: var(--warning); }
.canvas-node-team { border-left-color: var(--success); }

.canvas-node-product {
    border-left-color: var(--brand);
    border-color: var(--brand);
    box-shadow: var(--shadow-md);
}

/* The product is where a carried box lands, so it is the only node that answers a drag. */
.canvas-node-over {
    border-color: var(--brand);
    box-shadow: var(--shadow-inset-focus);
    background: var(--dnd-over-bg);
}

.canvas-node-head {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
}

.canvas-node-title {
    flex: 1;
    font-size: var(--text-md);
    font-weight: var(--weight-semibold);
    color: var(--text);
    overflow-wrap: anywhere;
}

.canvas-node-subtitle {
    font-size: var(--text-xs);
    color: var(--text-muted);
    overflow-wrap: anywhere;
}

.canvas-node-detail {
    font-size: var(--text-xs);
    color: var(--text-body);
    line-height: var(--leading-snug);
}

.canvas-node-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-top: var(--space-2);
}

/* The buttons stand down while the node is being asked a question. Spelled out because an author
   rule that sets display beats the browser's own [hidden] rule, so a component that sets one has to
   say what hidden means for itself or the attribute silently does nothing. */
.canvas-node-actions[hidden] { display: none; }

.canvas-node-action {
    font-family: inherit;
    font-size: var(--text-2xs);
    padding: var(--space-1) var(--space-4);
    border-radius: var(--radius-sm);
    border: var(--border-width) solid var(--border);
    background: var(--surface-alt);
    color: var(--text-body);
    cursor: pointer;
}

.canvas-node-action:hover {
    background: var(--brand-soft);
    border-color: var(--brand);
    color: var(--brand-on-soft);
}

.canvas-node-action:disabled {
    opacity: 0.5;
    cursor: default;
}

/* An act that needs one word typed, asked on the node rather than in a dialog: the reader is
   looking at the box the answer is about, and a modal takes that away to ask about it. */
.canvas-prompt {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-top: var(--space-3);
    padding-top: var(--space-4);
    border-top: var(--border-width) dashed var(--border);
}

.canvas-prompt-label {
    font-size: var(--text-2xs);
    line-height: var(--leading-snug);
    color: var(--text-muted);
}

/* ---- how busy a repository is ------------------------------------------ */

/* A band, not a status. RepositoryStatus says how the last sync phase went and
   wears .status-badge from the badge sheet; this says how long it has been
   since anybody wrote code, and the two appear on the same screen. Separate
   names because they are separate facts. */
.canvas-pulse {
    display: inline-flex;
    align-items: center;
    font-size: var(--text-2xs);
    font-weight: var(--weight-semibold);
    letter-spacing: var(--tracking-wide);
    padding: var(--space-px) var(--space-4);
    border-radius: var(--radius-pill);
    white-space: nowrap;
}

.pulse-active { background: var(--success-soft); color: var(--success-on-soft); }
.pulse-stale { background: var(--warning-soft); color: var(--warning-on-soft); }
.pulse-dormant { background: var(--severe-soft); color: var(--severe-on-soft); }
.pulse-unknown { background: var(--neutral-soft); color: var(--neutral-on-soft); }
.pulse-untracked { background: var(--neutral-soft); color: var(--neutral-on-soft); }

/* The board's own sync badge, reusing the status vocabulary the Boards page
   already speaks. Declared here as well so a node built by the script is not
   borrowing a name from the badge sheet. */
.canvas-pulse.status-completed { background: var(--success-soft); color: var(--success-on-soft); }
.canvas-pulse.status-stale { background: var(--warning-soft); color: var(--warning-on-soft); }
.canvas-pulse.status-failed { background: var(--danger-soft); color: var(--danger-on-soft); }
.canvas-pulse.status-pending { background: var(--neutral-soft); color: var(--neutral-on-soft); }

/* ---- the legend -------------------------------------------------------- */

.canvas-legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-6);
    padding: var(--space-5) var(--space-8);
    border-top: var(--border-width) solid var(--border);
    background: var(--surface-alt);
    font-size: var(--text-xs);
    color: var(--text-muted);
}

.canvas-legend-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

@media (max-width: 1100px) {
    .canvas-shell { grid-template-columns: 1fr; }
    .canvas-rail { max-height: 360px; }
}

/* The page is the graph, so on a screen with room for it the graph gets the screen. Everything
   below is one rule: the two boxes stop being fixed-height rectangles that leave a third of the
   window white underneath them, and become a rail and a stage that reach the bottom edge. The
   height is spent where it is worth something — the rail shows twice as many waiting repositories
   before anybody scrolls, and Fit has a plane the shape of the window to fit into.

   Gated on height as well as width. Under 720px there is not enough room to give the board a
   usable share of it, and a viewport-tall page that cannot fit its own legend is worse than a page
   that scrolls; under 1100px the shell is already one column and stacking two full-height boxes is
   nonsense. Both fall back to the fixed heights above, which is what this screen did everywhere
   until now.

   Last in the sheet, and it has to be. Every declaration here overrides one of the plain rules
   above it at the same specificity, so written at the top of the file — where it was first — the
   media query lost to the base rules and the rail and the board kept their fixed heights while the
   stage around them grew. */
@media (min-width: 1101px) and (min-height: 720px) {
    .app-main:has(.canvas-shell) {
        height: 100vh;
        display: flex;
        flex-direction: column;
        /* The page itself never scrolls here. The rail scrolls, the plane scrolls, and a third
           scrollbar on the window moves both of them out from under the pointer. */
        overflow: hidden;
    }

    /* Prose above a graph, kept to the two lines it needs. It is read once and the graph is read
       every time. */
    .app-main:has(.canvas-shell) .page-header { margin-bottom: var(--space-8); }
    .app-main:has(.canvas-shell) .page-header p { max-width: 110ch; }

    .canvas-shell {
        flex: 1;
        /* Without this a grid item sized by its content refuses to shrink and the shell runs past
           the bottom of the window instead of handing its overflow to the two scrollers inside. */
        min-height: 0;
        align-items: stretch;
    }

    /* The cap is what the window leaves, not a number typed here. */
    .canvas-rail { max-height: none; }

    .canvas-stage {
        display: flex;
        flex-direction: column;
        min-height: 0;
    }

    /* The bar, the strip, a notice and the legend are each as tall as their words; the plane takes
       everything that is left, which is the only part of this screen that can use it. */
    .canvas-viewport {
        flex: 1;
        min-height: 220px;
    }

    /* Out of flow, so the plane cannot push the legend off the bottom edge by being tall: the
       viewport says how big it is and the board fills exactly that. */
    .canvas-board {
        position: absolute;
        inset: 0;
        height: auto;
    }
}
