/* The project card, defined once.

   projects.html and deployments.html carried byte-identical copies of
   this under two different parent selectors, and the builder's "Your
   recent Projects" grid carried a third that had drifted: a flat 135px
   band instead of a 16/9 frame, a 56px tile instead of 54, no inner
   highlight ring, no badge, and none of the sheen or hover glow. Same
   app, three different cards, depending on which page you opened.

   .pcard is the name the builder uses; .card and .app are what
   projects.html and deployments.html already had in their markup, kept so
   neither page had to be re-templated. Children stay scoped under those
   three, because code.html has .nm, .foot and .pv of its own. */

  /* The card's own surface tokens, so the component carries what it needs.

     projects.css and deployments.css both declare these, identically; the
     builder declared none of them, which is the other half of why its card
     looked like a different component. A page that still defines them wins
     on source order and the values match, so nothing moves there. */
  :root{
    --card-surface:linear-gradient(180deg,#ffffff,#fbfbfa);
    --card-sheen:#ffffff;
    --hair:#eceae8;
    --card-tray:rgba(246,245,244,.72);
    --card-rest-shadow:0px 1px 2px #00000008,0px 4px 8px -2px #0000000a;
    --card-hover-border:#bdb7b1;
    --card-hover-shadow:0px 2px 4px #0000000a,0px 8px 16px -4px #0000000f;
  }
  :root[data-theme="dark"]{
    --card-surface:linear-gradient(180deg,#2b2d31,#232528);
    --card-sheen:rgba(255,255,255,.055);
    --hair:rgba(255,255,255,.06);
    --card-tray:rgba(0,0,0,.16);
    --card-rest-shadow:0 1px 2px rgba(0,0,0,.45);
    --card-hover-border:rgba(255,255,255,.15);
    --card-hover-shadow:0 2px 6px rgba(0,0,0,.35),0 16px 34px -16px rgba(0,0,0,.8);
  }

  .pcard,.card,.app{position:relative;isolation:isolate;display:flex;flex-direction:column;text-align:left;
    border:1px solid var(--line);border-radius:16px;background:var(--card-surface);
    padding:0;overflow:hidden;cursor:pointer;width:100%;
    box-shadow:var(--card-rest-shadow);
    transition:transform .24s var(--ease),box-shadow .24s var(--ease),border-color .18s}
  /* Above the glow. Both pseudo-elements are positioned and the children are
     not, so without this the spotlight paints over the title. */
  .pcard>*,.card>*,.app>*{position:relative;z-index:1}

  /* The lit top edge, as a pseudo-element rather than a second border: no
     layout cost and it follows the radius for free. */
  .pcard::before,.card::before,.app::before{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
    box-shadow:inset 0 1px 0 var(--card-sheen)}
  /* Under the cursor the card glows in ITS OWN colour - --tile is the identity
     colour the tile is painted with, set inline per card. --mx/--my track the
     pointer; with no JS they fall back to the tile's corner, so this degrades
     to a fixed corner glow rather than to nothing. */
  .pcard::after,.card::after,.app::after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
    z-index:0;opacity:0;transition:opacity .3s var(--ease);
    /* A neutral wash, not the type colour. --tile still identifies the app on
       its tile mark, but bathing the whole card in cyan or magenta on hover
       made the grid flicker between unrelated hues as the pointer crossed it,
       and said nothing a glance at the tile does not already say. One grey
       lift for every card, so hover reads as "this one" rather than as a
       second, louder colour system. */
    background:radial-gradient(420px circle at var(--mx,16%) var(--my,0%),
      rgba(190,198,208,.55),transparent 62%)}
  /* Hover changes the card's COLOUR, never its position. The lift and the
     tile's scale-and-rotate meant the icon you were pointing at slid out from
     under the cursor, and on a grid of these the whole page twitched as the
     pointer crossed it. */
  .pcard:hover,.card:hover,.app:hover{border-color:var(--card-hover-border);box-shadow:var(--card-hover-shadow)}

  .pcard:hover::after,.card:hover::after,.app:hover::after{opacity:.09}

  .pcard:hover .tile,.card:hover .tile,.app:hover .tile{transform:none}
  /* A focus ring is not a hover state. Keyboard users were getting the mouse
     treatment and no ring at all. */
  .pcard:focus-visible,.card:focus-visible,.app:focus-visible{outline:2px solid var(--accent);outline-offset:2px;
    box-shadow:var(--card-hover-shadow)}

  /* ---- the thumbnail --------------------------------------------------
     The card leads with the app, not with its name. .thumb is the frame and
     .pv is the preview inside it - two elements rather than one, because
     pvMount()/pvFallback() replace .pv's innerHTML wholesale and anything
     that must survive that (the type badge, the live pill) has to be a
     SIBLING of it, not a child. */
  .pcard .thumb,.card .thumb,.app .thumb{position:relative;width:100%;aspect-ratio:16/9;flex:none;
    overflow:hidden;background:var(--paper-2);border-bottom:1px solid var(--hair)}
  /* The type mark, bottom-left, the way a file icon sits on a thumbnail. */
  .pcard .badge,.card .badge,.app .badge{position:absolute;left:10px;bottom:10px;z-index:2;
    width:26px;height:26px;border-radius:8px;display:grid;place-items:center;
    background:var(--card);border:1px solid var(--line);color:var(--ink-2);
    box-shadow:0 1px 3px rgba(0,0,0,.12)}

  .pcard .badge svg,.card .badge svg,.app .badge svg{width:14px;height:14px}
  /* Only a RUNNING app gets a pill. A "not deployed" badge on three quarters
     of the grid is a label that says nothing, and the reference carries none. */
  .pcard .thumb .pill,.card .thumb .pill,.app .thumb .pill{position:absolute;top:9px;right:9px;z-index:2;
    box-shadow:0 1px 4px rgba(0,0,0,.12)}

  .pcard .top,.card .top,.app .top{display:flex;align-items:flex-start;gap:11px;padding-bottom:13px}
  /* One inline --tile drives the fill, the inner highlight and the drop glow.
     The wash over it is fixed white-to-black, so every identity colour gets
     the same light without needing a second value per card. */
  /* A dark square with the type mark in the type's colour — the same tile
     the builder's recent-projects grid uses. --tile is still the card's
     identity colour (it drives the hover glow below), but it now means
     something: a website card glows cyan, a game card red. */
  .pcard .tile,.card .tile,.app .tile{width:36px;height:36px;border-radius:11px;display:grid;place-items:center;flex:none;
    font-family:var(--disp);font-weight:700;font-size:1.02rem;
    color:var(--tile);background:#0f0f0f;
    box-shadow:0 4px 12px -6px var(--tile),inset 0 0 0 1px rgba(255,255,255,.07);
    transition:transform .24s var(--ease)}

  .pcard .tile svg,.card .tile svg,.app .tile svg{width:20px;height:20px}

  /* ---- card preview -----------------------------------------------------
     A real miniature of the running app: the deployed site itself in an
     iframe, scaled down. A full 1280px frame scaled with transform, not a
     small iframe — a 300px-wide viewport makes a responsive site render its
     MOBILE layout, which is not what the app looks like and reads as a
     different site altogether.

     Only a project with a RUNNING deployment gets one. Everything else shows
     its type mark: a Souqi Code project is React/TSX until something builds
     it — not one revision in this database contains an .html file — so there
     is genuinely no document to put in a frame yet, and a grey placeholder
     dressed up as a screenshot would be a lie about that. */
  /* Fills its .thumb frame. The frame owns the ratio and the clipping now,
     so this is a bare surface - pvScale() still measures clientWidth off it
     and gets the frame's width, which is what it always measured. */
  .pv{position:absolute;inset:0;display:block;overflow:hidden;background:var(--paper-2)}
  /* pointer-events:none — the card is the click target, and a live app inside
     it must not swallow the click or scroll the grid. */
  .pv iframe{position:absolute;top:0;left:0;width:1280px;height:800px;border:0;
    transform-origin:0 0;pointer-events:none;background:#fff}

  .pv.loading::after{content:"";position:absolute;inset:0;
    background:linear-gradient(90deg,transparent,rgba(139,152,165,.10),transparent);
    transform:translateX(-100%);animation:sweep 1.4s var(--ease) infinite}
  /* One large mark, centred, and nothing else. The "Deploy to show preview
     here" caption moved to the card's title attribute: printed under the
     icon it turned every undeployed card into an instruction, and a grid of
     forty of them read as forty error states. */
  .pv-none{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}

  /* The mark on the thumbnail carries the identity colour twice: once as the
     glyph, once as a light source.

     It was a flat black square with a coloured glyph in the middle, which is
     a smaller version of the badge rather than the thing the card is built
     around. The small .tile in the footer already had the coloured drop glow
     and this, the one you actually look at, did not.

     The wash is a share of --tile rather than a second colour per type, so
     six build types get six lit tiles out of one declaration, and anything
     unclassified still lights up in its hashed colour. */
  .pv-none i{width:54px;height:54px;border-radius:16px;display:grid;place-items:center;
    flex:none;color:var(--tile);
    background:
      radial-gradient(125% 125% at 30% 18%,color-mix(in srgb,var(--tile) 26%,transparent),transparent 62%),
      #0f0f0f;
    box-shadow:
      0 10px 26px -12px var(--tile),
      inset 0 1px 0 rgba(255,255,255,.10),
      inset 0 0 0 1px rgba(255,255,255,.07)}

  .pv-none i svg{width:27px;height:27px}

  .pv-none b{display:none}
  @media (prefers-reduced-motion:reduce){ .pv.loading::after{animation:none} }

  .pcard .nm,.card .nm,.app .nm{flex:1;min-width:0}
  /* ONE line, not two. The footer has to be exactly as tall on every card in
     the grid or the cards stop matching, and a title that is allowed a
     second line decides that per card. The full string is on the card's
     title attribute, which is where a truncated name is recoverable. */
  .pcard .nm b,.card .nm b,.app .nm b{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
    font-size:.9rem;font-weight:600;line-height:1.35;letter-spacing:-.006em}

  .pcard .nm i,.card .nm i,.app .nm i{display:flex;align-items:center;gap:5px;font-style:normal;
    font-size:.745rem;color:var(--mut);margin-top:2px;
    letter-spacing:.004em;font-variant-numeric:tabular-nums}

  .pcard .nm i svg,.card .nm i svg,.app .nm i svg{width:11px;height:11px;flex:none}

  /* The footer is the caption under the thumbnail: name on the first line,
     who-can-see-it and when on the second, actions on the right. Its own
     ground and a hairline above separate it from the preview, which is most
     of what makes a console card look like one.

     flex:1 and a fixed min-height together are what keep the grid even: the
     footer takes whatever is left under a fixed-ratio thumbnail, and the two
     lines inside it never change count. */
  .pcard .foot,.card .foot,.app .foot{display:flex;align-items:center;gap:10px;flex:1;
    font-size:.765rem;color:var(--mut);
    min-height:58px;box-sizing:border-box;padding:10px 13px 11px;
    background:var(--card-tray)}

  .pcard .foot .sp,.card .foot .sp,.app .foot .sp{flex:1}
  /* An unconstrained inline SVG fills its flex line. META_PRIV's padlock did
     exactly that the moment the old .body .m svg rule went with .body - a
     12px lock became a 200px one and the card grew around it. Scoped to the
     meta line so the action buttons keep their own sizes. */
  .pcard .foot .nm svg,.card .foot .nm svg,.app .foot .nm svg{width:12px;height:12px;flex:none}

  /* Actions arrive on hover, the way the reference does it - at rest the row
     is the name and the date, and three grey glyphs on every card in a grid
     of forty is most of what makes a gallery look busy. Keyboard focus counts
     as hover, and a touch device (which has no hover to give) keeps them. */
  .pcard .acts,.card .acts,.app .acts{display:flex;align-items:center;gap:1px;flex:none;
    opacity:0;transition:opacity .16s var(--ease)}

  .pcard:hover .acts,.card:hover .acts,.app:hover .acts,.pcard:focus-within .acts,.card:focus-within .acts,.app:focus-within .acts{opacity:1}
  @media (hover:none){.pcard .acts,.card .acts,.app .acts{opacity:1} }

  .pcard .lnk,.card .lnk,.app .lnk{border:0;background:transparent;color:var(--mut);flex:none;
    width:26px;height:26px;border-radius:8px;display:grid;place-items:center;cursor:pointer;
    transition:background .15s,color .15s}

  .pcard .lnk:hover,.card .lnk:hover,.app .lnk:hover{background:var(--tint-2);color:var(--ink)}

  .pcard .lnk svg,.card .lnk svg,.app .lnk svg{width:15px;height:15px}

  .pcard .star,.card .star,.app .star{border:0;background:transparent;color:var(--mut);flex:none;
    width:26px;height:26px;border-radius:8px;display:grid;place-items:center;cursor:pointer;
    transition:background .15s,color .15s}

  .pcard .star:hover,.card .star:hover,.app .star:hover{background:var(--tint-2);color:var(--ink)}

  .pcard .star.on,.card .star.on,.app .star.on{color:#e0a11a}

  .pcard .star svg,.card .star svg,.app .star svg{width:15px;height:15px}

  .pcard .star.on svg,.card .star.on svg,.app .star.on svg{fill:currentColor}

  @media (hover:none){.pcard .acts,.card .acts,.app .acts{opacity:1}
  }
