
/* ============================================================
   SITE VIEWER — chrome copied from the Lazy builder.

   Every token and every class body below is lifted verbatim from
   lazy-project/app/css/lazyapp.css so the viewer's bar is the SAME
   bar as the editor's, minus the editor. Nothing here is invented:
   if a control needs a value, it comes from that file. Additions
   specific to the viewer are marked VIEWER and kept to a minimum.
   ============================================================ */
:root{
  /* colour */
  --la-paper:  #ffffff;
  --la-bg:     #f6f5f2;
  --la-canvas: #808080;   /* preview canvas behind the device frame */
  --la-ink:    #1a1a1a;
  --la-line:   #bfbaac;
  --la-teal:   #33c4bf;
  --la-coral:  #ff6f5e;
  --la-on-coral:   #1a1a1a;
  --la-hover:       color-mix(in oklab, var(--la-ink), var(--la-paper) 94%);
  --la-selected:    color-mix(in oklab, var(--la-teal), var(--la-paper) 60%);
  --la-on-selected: #12403c;
  --la-disabled-fade: .45;
  --la-press:  16%;
  /* type */
  /* THE SAME TWO FACES THE MARKETING PAGE SETS. The viewer sits on that page and
     frames its work; a different typeface would read as a different product. */
  --la-font:        'Noto Serif', serif;
  --la-text:        16px;
  --la-text-title:  15px;
  --la-text-propname: 16px;
  --la-propname-max: 18ch;
  --la-text-label:  12px;
  --la-track-title: .3px;
  --la-track-label: .8px;
  --la-text-field:  15px;
  --la-text-hint:   var(--la-text);
  --la-weight:      600;
  --la-weight-body: 400;
  --la-weight-strong: 800;
  --la-track:       .5px;
  /* form */
  --la-frame:       1px;
  --la-radius:      6px;
  --la-pad-btn:     9px 10px;
  --la-pad-input:   9px 11px;
  --la-muted:       color-mix(in oklab, var(--la-ink), var(--la-paper) 30%);
  --la-hint:        color-mix(in oklab, var(--la-ink), var(--la-paper) 48%);
  --la-placeholder: color-mix(in oklab, var(--la-ink), var(--la-paper) 60%);
  --la-trans:       .1s ease;
  /* space */
  --la-s1: 4px;
  --la-s2: 8px;
  --la-s3: 12px;
  --la-s4: 16px;
  --la-s5: 24px;
  --la-control-h:  40px;
  --la-control-h-sm: 32px;
  --la-glyph:      24px;
  --la-glyph-row:  18px;
  --la-ring-w:        2px;
  --la-ring-gap:      2px;
  --la-stack-gap:     2px;
  --la-toolbar-h: calc(var(--la-control-h) + var(--la-s2) * 2 + var(--la-frame));
  --la-z-tip:        400;
  /* ============================================================
     VIEWER — the marketing surface.

     The builder's chrome is deliberately the quietest thing on screen, because
     everything it frames is a host's design. This viewer sits on the MARKETING
     page, where the opposite is true: it has to look like the page it is embedded
     in. So the greys below stay for the admin list, and the bar takes the brand's
     own ink, mint and coral, its serif and its caps face.

     Every colour here is mixed from --la-teal / --la-coral / --la-paper, so the bar
     cannot drift from the accents the rest of the product uses.
     ============================================================ */
  --lv-ink:        #12403c;   /* deep teal — the brand's reading ink; the same value the kit
                                 already uses for ink on mint */
  --lv-ink-soft:   color-mix(in oklab, var(--lv-ink), var(--la-paper) 45%);  /* an icon at rest */
  --lv-hair:       color-mix(in oklab, var(--lv-ink), var(--la-paper) 88%); /* the bar's one edge — a hairline, not a colour */
  --lv-track:      color-mix(in oklab, var(--lv-ink), var(--la-paper) 80%);  /* the switch, off */

  --lv-font-display: var(--la-font);   /* the site's name */
  /* The marketing page's own three, so the intro reads as the same site. */
  --lv-site-ink:     #17363c;
  --lv-site-green:   #068b88;
  --lv-site-coral:   #f9426f;
  --lv-font-label:   var(--la-font);   /* caps labels */

  /* SMALL. The bar is a strip on someone else's page, so its controls are sized to be
     read rather than pressed hard: bare icons at 18px in a 28px target, and one pill. */
  --lv-bar-h:      44px;
  --lv-icon:       18px;
  --lv-tap:        28px;
  --lv-icon-pad:   10px;   /* the air either side of anything bare in the bar */
  --lv-logo-h:     18px;
  --lv-text-name:  13px;
  --lv-text-label: 10px;

  /* THE LIST. A card is sized to be scanned fifty at a time, not admired three at a
     time: the marketing page's cards carry a frame and a ribbon and earn it at hero
     size, and the same treatment at this size covers the only thing that distinguishes
     one card from another — the site's own hero. So the card is the picture and the
     name, and the tokens below are all it has.

     --lv-tile-min is what decides the column count, and it is the only number that
     needs to hold at every width: one column on a handset, where a second would make
     each card too small to read; three on a slate; five or six on a desktop. auto-fill
     does the rest, so there is no breakpoint per size to keep in step. */
  --lv-tile-min:     220px;
  --lv-tile-gap:     32px;      /* air between cards; rows get more, below */
  --lv-tile-radius:  10px;
  --lv-tile-ratio:   4 / 3;     /* the mockups' own shape, so a monitor and phone show whole */
  --lv-tile-edge:    3px;       /* the site's own colour, under its picture */
  --lv-tile-name:    12px;
  /* THE TOUR. It runs once and then never again this visit, so it moves quickly:
     a visitor came to see sites, not a tour. Six lights, each opening in turn, left to right. --lv-tour-step
     is the whole of the sequencing — the stops carry an index and the delay is that index
     times this. */
  --lv-tour-shade:   color-mix(in oklab, var(--lv-ink), black 55%);
  --lv-tour-scrim:   color-mix(in srgb, var(--lv-tour-shade) 50%, transparent);
  --lv-tour-ring:    var(--la-paper);
  --lv-tour-ring-w:  2px;
  --lv-tour-blur:    5px;
  --lv-tour-fade:    450ms;
  --lv-tour-step:    260ms;
  --lv-tour-label:   20px;
  /* THE TOUR IS HANDWRITTEN. Everything else in the viewer is the product's own voice —
     the slab for names, the signage face for labels. The tour is not the product talking
     about a site, it is someone leaning over and pointing, and a hand does that where a
     typeface cannot. It is the only place this font appears. */
  --lv-font-hand:    'Lobster', cursive;   /* the page's own script face */
  /* The handset's own status and address strips are the PHONE, not the brand, so they
     keep the face a phone uses rather than the one the site is set in. */
  --lv-font-os:      system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  /* A DASHED LEAD, not a rule. A solid line reads as structure — a border, a divider,
     something the page is made of. Dashes read as a gesture: drawn just now, by hand, at
     the thing being pointed at. */
  --lv-tour-dash:     6px;
  --lv-tour-dash-gap: 5px;
  --lv-tour-halo:    0 1px 2px var(--lv-tour-shade), 0 0 14px var(--lv-tour-shade);
  --lv-tour-tick-w:  2px;
  /* THE BEAT INSIDE A STOP. A stop is not one thing arriving, it is three: the light,
     then the line, then the word. This is the wait between them, and three of them fit
     inside one --lv-tour-step, so the parts of a stop never overrun the next stop. */
  --lv-tour-beat:    80ms;
  --lv-tour-title:   40px;
  --lv-tour-title-fade: 600ms;
  --lv-tour-say:     18px;
  --lv-tour-ok-text: 18px;
  --lv-tour-z:       var(--la-z-tip);

  --lv-list-pad:     var(--la-s5);
  --lv-list-max:     1320px;

  /* The device body. One ink, one edge, two radii — the screen must curve LESS than
     the body holding it or the two corners fight. */
  --lv-body:          var(--lv-ink);
  --lv-bezel:         10px;
  --lv-body-radius:   34px;
  --lv-screen-radius: 24px;
  --lv-speaker-w:     44px;
  --lv-speaker-h:     4px;
  --lv-device-gap:    16px;   /* the air a device stands in */

  /* The blurred ground. Bleed and zoom together keep the blur's own soft edge outside
     the surface, which is the difference between "frosted" and "a blurry rectangle". */
  --lv-bg-blur:       48px;
  --lv-bg-sat:        1.6;
  --lv-bg-zoom:       1.12;
  --lv-bg-fade:       .9;
  --lv-bg-bleed:      80px;
  --lv-veil-clear:    58%;    /* how much of the blur the veil lets through */
  --lv-veil-blur:     blur(6px);

  /* The handset's furniture. Real proportions, because the whole point is that a
     visitor recognises their own phone at a glance. */
  --lv-ios-status-h:   50px;
  --lv-ios-status-pad: 30px;
  --lv-ios-time:       15px;
  --lv-ios-browser-h:  84px;
  --lv-ios-browser-pad:12px;
  --lv-ios-pill-h:     44px;
  --lv-ios-host:       15px;
  /* THE BROWSER BAR IS GLASS. It floats over the page — over the hero, which is what
     is under it when a site opens — so it is a tint plus a blur of whatever it covers,
     never a solid strip that hides a band of the design. */
  --lv-ios-bar:        rgb(28 28 30 / .62);
  --lv-ios-chip:       rgb(255 255 255 / .16);
  --lv-ios-blur:       blur(22px) saturate(180%);
  --lv-ios-island-w:   96px;
  --lv-ios-island-h:   28px;
  --lv-ios-home-w:     136px;
  --lv-ios-home-h:     5px;
}

*, *::before, *::after { box-sizing:border-box; }
html, body { height:100%; }
body {
  margin:0; font-family:var(--la-font); color:var(--la-ink); background:transparent;
  display:flex; flex-direction:column; overflow:hidden;
}

/* Keyboard focus — one ring for every control in the kit. */
:focus-visible { outline:var(--la-ring-w) solid var(--la-teal); outline-offset:var(--la-ring-gap); }

.la-title { font-family:var(--la-font); font-weight:var(--la-weight-strong); font-size:var(--la-text-title); text-transform:uppercase; letter-spacing:var(--la-track-title); color:var(--la-ink); }
.la-hint { font-family:var(--la-font); font-size:var(--la-text-hint); line-height:1.4; color:var(--la-hint); }

.la-btn {
  font-family:var(--la-font); font-weight:var(--la-weight); font-size:var(--la-text-label); text-transform:uppercase; letter-spacing:var(--la-track);
  background:var(--la-paper); color:var(--la-ink);
  border:var(--la-frame) solid var(--la-line);
  border-radius:var(--la-radius);
  padding:var(--la-pad-btn); cursor:pointer; transition:border-color var(--la-trans), background var(--la-trans), color var(--la-trans);
  display:inline-flex; align-items:center; justify-content:center; gap:var(--la-s2); line-height:1;
  min-height:var(--la-control-h); box-sizing:border-box;
  text-decoration:none;
}
.la-btn:hover  { background:var(--la-hover); }
.la-btn[aria-pressed="true"] { background:var(--la-selected); color:var(--la-on-selected); border-color:var(--la-selected); }
.la-btn[aria-pressed="true"]:hover { background:color-mix(in oklab, var(--la-selected), white var(--la-press)); color:var(--la-on-selected); border-color:var(--la-selected); }
.la-btn--coral       { background:var(--la-coral); color:var(--la-on-coral); border-color:var(--la-coral); }
.la-btn--coral:hover { background:color-mix(in oklab, var(--la-coral), black var(--la-press)); border-color:var(--la-coral); color:var(--la-on-coral); }
.la-btn:disabled, .la-btn[disabled] {
  background:var(--la-paper); color:var(--la-muted);
  border-color:color-mix(in oklab, var(--la-muted), var(--la-paper) 62%);
  opacity:var(--la-disabled-fade);
  cursor:not-allowed;
}
.la-btn:disabled:hover, .la-btn[disabled]:hover { border-color:var(--la-muted); }
.la-btn--mini   { font-size:var(--la-text-label); padding:var(--la-s1) var(--la-s3); }
.la-btn--arrow  { padding:0; width:var(--la-control-h); flex-shrink:0; }
.la-btn--glyph  { padding:0; width:var(--la-control-h); flex-shrink:0; }
.la-btn--icon   { padding:0; width:var(--la-control-h-sm); min-height:var(--la-control-h-sm);
                  flex-shrink:0; font-size:var(--la-glyph-row); line-height:1; }
.la-btn--icon svg { height:var(--la-glyph-row); width:auto; }
.la-toolbar .la-btn svg { width:var(--la-glyph); height:var(--la-glyph); }

.la-tabs { display:flex; gap:var(--la-s1); align-items:stretch; }
.la-tab {
  flex:1; padding:var(--la-pad-btn); min-height:var(--la-control-h);
  display:inline-flex; align-items:center; justify-content:center; line-height:1;
  font-family:var(--la-font); font-size:var(--la-text-label); font-weight:var(--la-weight);
  text-transform:uppercase; letter-spacing:var(--la-track);
  color:var(--la-muted); background:var(--la-paper);
  border:var(--la-frame) solid var(--la-line); border-radius:var(--la-radius);
  cursor:pointer; transition:color var(--la-trans), background var(--la-trans), border-color var(--la-trans);
}
.la-tab:hover { color:var(--la-ink); background:var(--la-hover); }
.la-tab[aria-selected="true"] { color:var(--la-on-selected); background:var(--la-selected); border-color:var(--la-selected); }
/* VIEWER — Fit/100% sit in a bar, not a pane: they take their own width instead of
   splitting the row. Same treatment, no growth. */
.la-viewtabs .la-tab { flex:0 0 auto; }

.la-row   { display:flex; align-items:center; gap:var(--la-s1); }
.la-row-2 { display:flex; flex-direction:row; align-items:center; gap:var(--la-s2); }

/* min-height:0 is what lets this shrink to the window. Without it a flex item refuses
   to go below its content's height, so the whole viewer grew past the viewport, the
   body's overflow:hidden clipped the bottom off, and the list never became a scroller
   — it was already as tall as it wanted to be. */
.la-preview { flex:1; min-width:0; min-height:0; display:flex; flex-direction:column; background:transparent; position:relative; }
.la-toolbar { height:var(--la-toolbar-h); box-sizing:border-box; display:flex; align-items:center; justify-content:space-between; gap:var(--la-s3); padding:var(--la-s2) var(--la-s4); background:var(--la-paper); border-bottom:var(--la-frame) solid var(--la-line); flex-shrink:0; }
.la-toolbar > * { flex-shrink:0; }

/* ---- THE BAR ----
   One strip: the mark, which is also the way back to lazy.vacations; the site's
   name between two arrows; then how you are looking at it. No dropdown — with a
   handful of sites the arrows ARE the navigation, and a menu on a marketing page
   is a control the visitor has to learn before they can look at anything.       */
.lv-bar {
  height:var(--lv-bar-h); box-sizing:border-box; flex-shrink:0;
  display:flex; align-items:center; justify-content:space-between; gap:var(--la-s5);
  padding:0 var(--la-s5);
  background:var(--la-paper);
  border-bottom:1px solid var(--lv-hair);
}
.lv-bar > * { flex-shrink:0; }
.lv-group { display:flex; align-items:center; gap:var(--la-s4); }

/* THE MARK IS THE LINK — and only the mark. No pill, no fill, no box: on a bar made
   of bare marks a button would be the one boxed thing on it. Coral, because that is
   the brand's own accent and the wordmark is the one place it belongs here. */
.lv-brand {
  display:inline-flex; align-items:center;
  color:var(--la-coral); text-decoration:none; flex-shrink:0;
  padding:0 var(--lv-icon-pad);
  transition:color var(--la-trans);
}
.lv-brand:hover { color:color-mix(in oklab, var(--la-coral), black var(--la-press)); }
.lv-brand svg { height:var(--lv-logo-h); width:auto; display:block; }

/* The site's name, in the brand's slab — and the way back to the list, which is why
   it is a button and carries a grid mark. Bare like every other control on this bar:
   no fill, no border. Capped so a long name ellipsizes rather than pushing the device
   row off the right edge of a narrow embed. */
.lv-name {
  display:inline-flex; align-items:center; gap:var(--la-s2);
  background:none; border:0; cursor:pointer;
  font-family:var(--lv-font-display); font-weight:600; font-size:var(--lv-text-name);
  letter-spacing:.06em; text-transform:uppercase; color:var(--lv-ink);
  max-width:24ch; padding:0 var(--la-s1);
  transition:color var(--la-trans);
}
.lv-name__grid { width:var(--lv-icon); height:var(--lv-icon); flex-shrink:0; color:var(--lv-ink-soft); transition:color var(--la-trans); }
.lv-name__text { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.lv-name:hover { color:var(--la-coral); }
.lv-name:hover .lv-name__grid { color:var(--la-coral); }

/* What the bar says when nothing is framed. Same face as the name it stands in for, so
   the strip does not change weight between the two views. */
.lv-title {
  font-family:var(--lv-font-display); font-weight:600; font-size:var(--lv-text-name);
  letter-spacing:.06em; text-transform:uppercase; color:var(--lv-ink);
  padding:0 var(--la-s1);
}

/* COLOURED MARKS, NOT BUTTONS. Every control on this bar is the drawing alone —
   no fill, no border, no box. Colour carries the state: soft teal at rest, deep
   teal under the pointer, coral when it is the one that is on. */
.lv-icon {
  display:inline-flex; align-items:center; justify-content:center;
  /* The mark is the target, and the target wants room either side of it, so the
     width is a minimum and the padding decides the rest. */
  min-width:var(--lv-tap); height:var(--lv-tap); padding:0 var(--lv-icon-pad);
  background:none; border:0; border-radius:999px;
  color:var(--lv-ink-soft); cursor:pointer;
  transition:color var(--la-trans);
}
.lv-icon svg { width:var(--lv-icon); height:var(--lv-icon); display:block; }
.lv-icon:hover { color:var(--lv-ink); }
.lv-icon[aria-pressed="true"] { color:var(--la-coral); }
.lv-icon:disabled { color:var(--lv-ink-soft); opacity:var(--la-disabled-fade); cursor:default; }

/* FIT / 100% AS ONE SWITCH. Two tabs asked the visitor to compare two words before
   pressing either; a switch has one state and one meaning — off fits the site to the
   space, on shows it at its real size. */
.lv-switch {
  display:inline-flex; align-items:center; gap:var(--la-s2);
  background:none; border:0; padding:0 var(--lv-icon-pad); cursor:pointer;
  color:var(--lv-ink-soft);
}
.lv-switch:hover { color:var(--lv-ink); }
.lv-switch__track {
  position:relative; width:32px; height:18px; border-radius:999px;
  background:var(--lv-track); transition:background var(--la-trans); flex-shrink:0;
}
.lv-switch__knob {
  position:absolute; top:2px; left:2px; width:14px; height:14px; border-radius:50%;
  background:var(--la-paper); transition:transform var(--la-trans);
}
.lv-switch[aria-checked="true"] .lv-switch__track { background:var(--la-coral); }
.lv-switch[aria-checked="true"] .lv-switch__knob { transform:translateX(14px); }
.lv-switch__label {
  font-family:var(--lv-font-label); font-weight:700; font-size:var(--lv-text-label);
  letter-spacing:.12em; text-transform:uppercase; color:inherit;
}

/* ---- THE LIST ----
   The first view. One grid, one card, and a rule that holds at every width without a
   breakpoint of its own: auto-fill with a minimum card width, so the columns are however
   many fit. At 390px that is one — deliberately, because two cards on a handset are 170px
   wide and a 170px screenshot of a website shows nothing at all. */
.lv-list { flex:1; min-height:0; overflow:auto; padding:var(--lv-list-pad); background:transparent; }
/* What the list is, said once at the top of it. */
.lv-intro { max-width:var(--lv-list-max); margin:0 auto var(--la-s5); text-align:center; }
/* The same block again under the grid, the last thing on the list. */
.lv-outro { max-width:var(--lv-list-max); margin:calc(var(--la-s5) * 2) auto var(--la-s5); text-align:center; }
/* Title, lead and button as the marketing page sets them: Lobster section title, green
   lead, the green call to action. */
.lv-intro__title, .lv-outro__title { margin:20px 0; font-family:'Lobster', cursive; font-weight:400; font-size:55px;
                   line-height:1.1; color:var(--lv-site-ink); }
.lv-intro__sub, .lv-outro__sub { max-width:750px; margin:0 auto 24px; font-size:18px; font-weight:500;
                 line-height:1.5; color:var(--lv-site-green); }
.lv-intro__cta, .lv-outro__cta, .lv-tile__live { display:inline-flex; text-decoration:none; align-items:center; justify-content:center; height:44px;
                 padding:0 24px; border:0; border-radius:8px; cursor:pointer;
                 font-family:var(--la-font); font-weight:700; font-size:16px; letter-spacing:.04em;
                 text-transform:uppercase; color:#fff; background:var(--lv-site-green);
                 box-shadow:0 6px 18px rgba(13,43,46,.10);
                 transition:background .28s, transform .28s, box-shadow .28s; }
.lv-intro__cta:hover, .lv-outro__cta:hover, .lv-tile__live:hover { background:var(--lv-site-coral); transform:translateY(-2px);
                       box-shadow:0 12px 26px rgba(13,43,46,.18); }
@media (max-width: 700px) { .lv-intro__title, .lv-outro__title { font-size:37px; } .lv-intro__sub, .lv-outro__sub { font-size:16px; } }

.lv-grid {
  list-style:none; margin:0 auto; padding:0;
  max-width:var(--lv-list-max);
  display:grid; gap:56px var(--lv-tile-gap);
  /* At most three across: a track is never narrower than a third of the row. */
  grid-template-columns:repeat(auto-fill, minmax(max(var(--lv-tile-min), calc((100% - 2 * var(--lv-tile-gap)) / 3)), 1fr));
}

/* A REAL LINK, not a click handler on a box. Middle-click, open-in-new-tab and the
   keyboard all work for free, and the href is the same URL the viewer would write
   anyway — the handler only keeps the pick inside the frame. */
.lv-tile {
  display:flex; flex-direction:column; gap:var(--la-s2);
  text-decoration:none; color:var(--lv-ink);
  border-radius:var(--lv-tile-radius);
}

/* The picture. Cropped from the TOP, because the top of a site is its hero and its
   header colour, which is the whole of what one card has to say. */
.lv-tile__shot {
  position:relative; overflow:hidden;
  aspect-ratio:var(--lv-tile-ratio);
  border-radius:var(--lv-tile-radius);
  border:1px solid var(--lv-hair);
  /* A TINT of the site's colour, not the colour. This is what a tile without a picture
     shows, and at your scale that is twenty of them at once — full-strength brand colours
     twenty across read as a paint chart rather than a list waiting on its pictures. The
     colour stays at full strength in the edge below, where one thin line of it is a mark
     and not a field. */
  background:color-mix(in oklab, var(--lv-tile-colour, var(--la-line)), var(--la-paper) 82%);
  transition:border-color var(--la-trans);
}
.lv-tile:hover .lv-tile__shot { border-color:var(--lv-ink-soft); }
.lv-tile__img {
  width:100%; height:100%; display:block;
  object-fit:cover; object-position:top center;
}

/* THE SITE'S OWN COLOUR, under its picture. It ties the shot to the name below it, and
   it is the one thing a card can show before its picture has loaded — or when it has
   none yet, where the whole shot is this colour and the edge is simply the bottom of it. */
.lv-tile__edge {
  position:absolute; left:0; right:0; bottom:0; height:var(--lv-tile-edge);
  background:var(--lv-tile-colour, var(--lv-hair));
}

/* NO PICTURE YET is a state, not a failure: a flat field of the site's colour. Nothing
   is written on it — the name is already underneath, and saying it twice is the kind of
   ornament this list exists to avoid. */
.lv-tile__shot[data-bare="true"] .lv-tile__img { display:none; }

.lv-tile__name {
  font-family:var(--lv-font-display); font-weight:600; font-size:var(--lv-tile-name);
  letter-spacing:.06em; text-transform:uppercase; color:var(--lv-ink);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  padding:0 var(--la-s1);
  transition:color var(--la-trans);
}
.lv-tile:hover .lv-tile__name { color:var(--la-coral); }
/* What kind of website it is, in the site's green; then what it demonstrates. */
.lv-tile__kind { font-size:13px; font-weight:700; color:var(--lv-site-green); padding:0 var(--la-s1); }
.lv-tile__kind:empty { display:none; }
.lv-tile__desc {
  font-size:13px; line-height:1.45; color:color-mix(in oklab, var(--lv-ink), var(--la-paper) 25%);
  padding:0 var(--la-s1);
  display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:3; line-clamp:3; overflow:hidden;
}
.lv-tile__desc:empty { display:none; }

/* THE HOVER OVERLAY: a wash over the picture and one button to the live site. It sits
   on the picture's box exactly — same width, same ratio — and lets every click but the
   button's fall through to the card. No hover on a touch screen, so none there. */
.lv-grid > li { position:relative; }
.lv-tile__over {
  position:absolute; left:0; right:0; top:0; aspect-ratio:var(--lv-tile-ratio);
  display:flex; align-items:center; justify-content:center;
  border-radius:var(--lv-tile-radius); background:rgba(23,54,60,.55);
  opacity:0; transition:opacity var(--la-trans); pointer-events:none;
}
.lv-tile__live { pointer-events:auto; text-decoration:none; }
.lv-grid > li:hover .lv-tile__over,
.lv-grid > li:focus-within .lv-tile__over { opacity:1; }
.lv-grid > li:not(:hover):not(:focus-within) .lv-tile__live { pointer-events:none; }
@media (hover: none) { .lv-tile__over { display:none; } }
/* On hover the picture grows a little inside its frame, then breathes. */
.lv-grid > li:hover .lv-tile__img {
  animation:lv-tile-grow .4s ease-out forwards, lv-tile-pulse 1.8s ease-in-out .4s infinite;
}
@keyframes lv-tile-grow  { from { transform:scale(1); } to { transform:scale(1.05); } }
@keyframes lv-tile-pulse { 0%, 100% { transform:scale(1.05); } 50% { transform:scale(1.09); } }
@media (prefers-reduced-motion: reduce) { .lv-grid > li:hover .lv-tile__img { animation:none; } }

/* ---- WHICH VIEW ----
   One page, two views, and the bar keeps its shape across both: the controls that
   belong to a framed site are absent from the list rather than disabled on it. */
.la-preview[data-view="list"] .lv-surf { display:none; }
.la-preview[data-view="list"] #lv-prev,
.la-preview[data-view="list"] #lv-next,
.la-preview[data-view="list"] .lv-name,
.la-preview[data-view="list"] #lv-devices { display:none; }
.la-preview[data-view="site"] .lv-list { display:none; }
/* The site's header on the list, the tool bar on a framed site — one or the other. */
.la-preview[data-view="list"] .lv-bar,
.la-preview[data-view="site"] .site-head { display:none; }
/* No water behind the viewer, so the header carries it itself. */
.la-preview .site-head { flex:0 0 auto; background-color:var(--water); }
.menu[hidden] { display:none; }
.la-preview[data-view="site"] .lv-title { display:none; }

/* ---- the surface the framed site sits on ----
   Mint, not the builder's neutral grey: this page is part of the marketing site, and
   the site being shown should read as a card lying on it. */
/* NO GROUND. The viewer paints nothing behind the site: whatever the page holding
   the embed uses shows through wherever a narrower device leaves room. */
.lv-surf { flex:1; min-height:0; background:transparent; overflow:auto; display:block; }
.lv-surf[data-scale="fit"] { overflow:hidden; }
/* A device stands in the middle of the space with a little air around it. A desktop
   does not: it fills, because it is the window. */
.lv-surf[data-device="phone"],
.lv-surf[data-device="tablet"] {
  position:relative;
  display:flex; align-items:center; justify-content:center; padding:var(--lv-device-gap);
}

/* ---- the ground behind the device ----
   The site again, blurred out of legibility and pushed past the edges so the blur has
   no visible border, with a veil over it so the device still reads as the subject. */
.lv-bg, .lv-veil { display:none; }
.lv-surf[data-device="phone"] .lv-bg,
.lv-surf[data-device="tablet"] .lv-bg,
.lv-surf[data-device="phone"] .lv-veil,
.lv-surf[data-device="tablet"] .lv-veil {
  display:block; position:absolute; inset:calc(var(--lv-bg-bleed) * -1);
  width:calc(100% + var(--lv-bg-bleed) * 2); height:calc(100% + var(--lv-bg-bleed) * 2);
  border:0; pointer-events:none;
}
.lv-surf[data-device="phone"] .lv-bg,
.lv-surf[data-device="tablet"] .lv-bg {
  filter:blur(var(--lv-bg-blur)) saturate(var(--lv-bg-sat));
  transform:scale(var(--lv-bg-zoom));
  opacity:var(--lv-bg-fade);
}
/* The veil is the glass itself: a wash of paper over the blur, so a dark site does not
   turn the page into a lightbox and a bright one does not shout past the device. */
.lv-surf[data-device="phone"] .lv-veil,
.lv-surf[data-device="tablet"] .lv-veil {
  background:color-mix(in oklab, var(--la-paper), transparent var(--lv-veil-clear));
  -webkit-backdrop-filter:var(--lv-veil-blur); backdrop-filter:var(--lv-veil-blur);
}
/* NO PAPER UNDER THE SITE. The frame is exactly the screen's width and paints nothing,
   so any edge the zoom rounds onto a half pixel shows the bezel's own colour rather
   than a white hairline between the site and the body. */
/* NO BAR IN THE FRAME. The page inside scrolls, it just does not draw a scrollbar —
   which is the only way to be sure nothing of one shows against the bezel, on any
   machine, whichever way that machine draws them. */
.lv-frame { display:block; border:0; background:transparent; overflow:hidden; }
.lv-screen { background:var(--lv-body); }

/* ---- THE DEVICE ----
   A handset and a slate get a body around them; a desktop does not, because the
   window you are already looking through IS the desktop. The body is one thin ink
   edge and a speaker line — enough to say "phone" and nothing more. No notch, no
   buttons, no chrome: this is a container for a site, not a picture of hardware. */
.lv-device { position:relative; margin:0 auto; width:max-content; }
.lv-device[data-device="desktop"] { width:auto; }
.lv-device[data-device="desktop"] .lv-screen { border-radius:0; }

/* The screen: the only thing that clips, and on a handset it is a column — status
   bar, page, browser bar — rather than a single pane. */
.lv-screen { position:relative; overflow:hidden; display:flex; flex-direction:column; }

/* ---- the handset's own UI ----
   Off everywhere but the phone. A slate and a desktop are not held in one hand and do
   not carry this furniture; drawing it there would be decoration pretending to be a
   device. */
.lv-ios { display:none; flex-shrink:0; }
.lv-device[data-device="phone"] .lv-ios { display:flex; align-items:center; }

/* THE STATUS BAR IS THE SITE'S HEADER, CONTINUED. On a real handset the strip above
   the page carries the page's own colour, which is why it reads as one surface rather
   than as furniture sitting on top of a website. The colour is the site's --header,
   stated per site in sites.json; with none stated it falls back to paper. */
.lv-ios--status {
  position:relative;
  height:var(--lv-ios-status-h); padding:0 var(--lv-ios-status-pad);
  justify-content:space-between;
  background:var(--lv-status-bg, var(--la-paper));
  color:var(--lv-status-ink, var(--lv-ink));
  font-family:var(--lv-font-os); font-weight:700; font-size:var(--lv-ios-time);
  letter-spacing:.2px;
}
/* The island. Always the same black whatever the header behind it — it is a hole in
   the screen, not a painted shape, and a hole does not take the page's colour. */
.lv-ios__island {
  position:absolute; left:50%; top:50%; transform:translate(-50%, -50%);
  width:var(--lv-ios-island-w); height:var(--lv-ios-island-h);
  border-radius:999px; background:#000;
}
.lv-ios__meters { display:inline-flex; align-items:center; gap:6px; }
.lv-ios__meters svg { height:11px; width:auto; display:block; }

/* THE BROWSER BAR TAKES ITS OWN STRIP. It used to float over the page, the way it does
   on a real handset — but a real handset scrolls the last inch of the page out from
   under it, and a frame cannot: whatever the site puts at the bottom stayed hidden
   behind the glass for good. So the strip is part of the column, and the framed page
   ends where it begins. */
.lv-ios--browser {
  position:relative; z-index:1; flex-shrink:0;
  height:var(--lv-ios-browser-h); padding:var(--la-s2) var(--lv-ios-browser-pad) 0;
  gap:var(--la-s3); justify-content:space-between; align-items:flex-start;
  background:var(--lv-ios-bar); color:var(--la-paper);
  -webkit-backdrop-filter:var(--lv-ios-blur); backdrop-filter:var(--lv-ios-blur);
}
/* The home indicator — the round thingy. Centred in the strip below the address bar,
   in the paper ink, because it is drawn by the phone and not by the page. */
.lv-ios__home {
  position:absolute; left:50%; bottom:var(--la-s2); transform:translateX(-50%);
  width:var(--lv-ios-home-w); height:var(--lv-ios-home-h);
  border-radius:999px; background:color-mix(in oklab, var(--la-paper), transparent 15%);
}
.lv-ios__glyph {
  display:inline-flex; align-items:center; justify-content:center;
  width:var(--lv-ios-pill-h); height:var(--lv-ios-pill-h);
  border-radius:999px; background:var(--lv-ios-chip); flex-shrink:0;
}
.lv-ios__glyph svg { width:20px; height:20px; }
.lv-ios__pill {
  flex:1; min-width:0; height:var(--lv-ios-pill-h);
  display:inline-flex; align-items:center; justify-content:center; gap:var(--la-s2);
  padding:0 var(--la-s3); border-radius:999px; background:var(--lv-ios-chip);
}
.lv-ios__host {
  font-family:var(--lv-font-os); font-size:var(--lv-ios-host); font-weight:500;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.lv-ios__reader, .lv-ios__reload { width:16px; height:16px; flex-shrink:0; opacity:.75; }

.lv-device:not([data-device="desktop"]) {
  padding:var(--lv-bezel);
  background:var(--lv-body);
  border-radius:var(--lv-body-radius);
  box-shadow:0 18px 50px color-mix(in oklab, var(--lv-ink), transparent 82%);
}
.lv-device:not([data-device="desktop"]) .lv-screen { border-radius:var(--lv-screen-radius); }

/* The speaker: one short line centred in the top edge, and only where there is a
   body to hold it. It is the whole difference between a rounded rectangle and a
   phone, which is why it is the only detail here. */
.lv-device__speaker { display:none; }
.lv-device:not([data-device="desktop"]) .lv-device__speaker {
  display:block; position:absolute; left:50%; transform:translateX(-50%);
  top:calc((var(--lv-bezel) - var(--lv-speaker-h)) / 2);
  width:var(--lv-speaker-w); height:var(--lv-speaker-h);
  border-radius:999px;
  background:color-mix(in oklab, var(--la-paper), var(--lv-body) 60%);
}

/* Nothing loaded yet, or the list could not be read: the bar says so instead of
   silently framing nothing. */
.lv-msg { padding:var(--la-s5); color:var(--lv-ink); font-family:var(--lv-font-display); }

/* Narrow host: the bar wraps rather than shrinking its controls. The builder scales
   its toolbar with zoom because it owns its width; an embed does not, and a scaled-down
   bar inside someone else's page reads as broken. */
@media (max-width: 620px) {
  .lv-bar { height:auto; flex-wrap:wrap; justify-content:flex-start;
            gap:var(--la-s3); padding:var(--la-s3) var(--la-s4); }
}

/* The mark in the admin bar: a plain wordmark in the kit's ink. It is not the coral
   pill from the viewer — admin is a tool, not a page anyone is being sold to. */
.lv-logo { display:inline-flex; align-items:center; color:var(--la-ink); flex-shrink:0; }
.lv-logo svg { height:var(--lv-logo-h); width:auto; display:block; }

/* ============================================================
   ADMIN — the same kit, list-shaped. admin.html only.
   ============================================================ */
/* ---- THE TOUR ----
   A scrim with six holes in it. The holes are a mask: white shows the dark, black
   makes a hole, and because black-at-nothing is the same as no hole at all, a hole
   OPENING is the same fade as its ring arriving. One animation, four elements, one
   delay — which is why every piece of a stop carries the same --i. */
.lv-tour {
  position:absolute; inset:0; z-index:var(--lv-tour-z);
}
.lv-tour[hidden] { display:none; }
.lv-tour__cut { position:absolute; inset:0; width:100%; height:100%; display:block; }
/* THE SITE GOES SOFT, THE BAR DOES NOT. Everything the tour is naming lives on the strip
   at the top; blurring that would blur the very thing being pointed at. So the blur starts
   under the bar, and js/tour.js measures where that is rather than restating it here. */
.lv-tour__blur {
  position:absolute; left:0; right:0; bottom:0;
  -webkit-backdrop-filter:blur(var(--lv-tour-blur)); backdrop-filter:blur(var(--lv-tour-blur));
  animation:lv-tour-in var(--lv-tour-fade) ease both;
}

.lv-tour__scrim { fill:var(--lv-tour-scrim); animation:lv-tour-in var(--lv-tour-fade) ease both; }

/* THE SEQUENCE, and all of it. Nothing else in this file knows how many stops there
   are or what order they are in — the index arrives on the element and this turns it
   into a wait. `both`, so a piece is invisible until its turn and stays put afterwards.

   Each stop tells its own small story in three beats: the light opens, the line reaches
   out of it, the word lands at the end of the line. Same --i, three offsets. */
.lv-tour__hole,
.lv-tour__ring,
.lv-tour__tick,
.lv-tour__label {
  animation:lv-tour-in var(--lv-tour-fade) ease both;
  animation-delay:calc(var(--lv-tour-fade) + var(--i) * var(--lv-tour-step));
}
.lv-tour__tick {
  animation-delay:calc(var(--lv-tour-fade) + var(--i) * var(--lv-tour-step) + var(--lv-tour-beat));
}
.lv-tour__label {
  animation-delay:calc(var(--lv-tour-fade) + var(--i) * var(--lv-tour-step) + 2 * var(--lv-tour-beat));
}
@keyframes lv-tour-in { from { opacity:0; } to { opacity:1; } }

.lv-tour__ring { fill:none; stroke:var(--lv-tour-ring); stroke-width:var(--lv-tour-ring-w); }
.lv-tour__tick {
  stroke:var(--lv-tour-ring); stroke-width:var(--lv-tour-tick-w);
  stroke-linecap:round;
  stroke-dasharray:var(--lv-tour-dash) var(--lv-tour-dash-gap);
  opacity:.9;
  animation-name:lv-tour-in-soft;   /* its own fade: the shared one ends at full */
}
@keyframes lv-tour-in-soft { from { opacity:0; } to { opacity:.9; } }

.lv-tour__labels { position:absolute; inset:0; pointer-events:none; }
/* Positioned by its CORNER, not its centre — js/tour.js puts the near edge of the word
   where the dots stop, so the lead arrives at the label rather than pointing under it. */
.lv-tour__label {
  position:absolute;
  font-family:var(--la-font); font-weight:700; font-size:var(--lv-tour-label);
  color:var(--lv-tour-ring); line-height:1.1;
  white-space:nowrap;
  /* THE WORDS CARRY THEIR OWN DARK. At a half-strength scrim the site underneath is
     still bright — sky, snow, a white hero — and white words on it disappear. A shadow
     is the only thing that holds at every one of them, and it costs nothing when the
     ground behind is already dark. */
  text-shadow:var(--lv-tour-halo);
}

/* What the tour actually says, once, in the middle of the site it is covering. */
/* WHAT THE TOUR SAYS. Three pieces, and they do not arrive together: the title opens the
   whole thing, before a single light; the sentence and the button close it, after the last
   word has landed. `--n` is how many slots the lights take, and only js/tour.js knows it. */
.lv-tour__say {
  position:absolute; left:50%; top:50%; transform:translate(-50%, calc(-50% - 100px));
  width:min(76ch, calc(100% - var(--la-s5) * 2));
  display:flex; flex-direction:column; align-items:center; gap:var(--la-s4);
  text-align:center;
}
.lv-tour__title {
  margin:0 0 var(--la-s4); color:var(--lv-tour-ring);
  font-family:var(--lv-font-display); font-weight:700; font-size:var(--lv-tour-title);
  line-height:1.15;
  text-shadow:var(--lv-tour-halo);
  /* It opens the tour, so it arrives before anything is lit and takes longer doing it. */
  animation:lv-tour-in var(--lv-tour-title-fade) ease both;
  animation-delay:0s;
}
.lv-tour__line,
.lv-tour__ok {
  animation:lv-tour-in var(--lv-tour-fade) ease both;
  animation-delay:calc(var(--lv-tour-fade) + var(--n) * var(--lv-tour-step));
}
.lv-tour__line {
  margin:0; color:var(--lv-tour-ring);
  font-family:var(--lv-font-display); font-weight:500; font-size:var(--lv-tour-say);
  line-height:1.45;
  text-shadow:var(--lv-tour-halo);
}
.lv-tour__ok {
  font-family:var(--lv-font-label); font-weight:700; font-size:var(--lv-tour-ok-text);
  letter-spacing:.12em; text-transform:uppercase;
  color:var(--la-on-coral); background:var(--la-coral);
  border:0; border-radius:999px; padding:var(--la-s3) var(--la-s5); min-width:10ch;
  cursor:pointer; transition:background var(--la-trans);
}
.lv-tour__ok:hover { background:color-mix(in oklab, var(--la-coral), black var(--la-press)); }

/* A tour is the one thing on this page that is pure motion, so it is the one thing that
   has to answer this: everything arrives at once, still in order, just not staged. */
@media (prefers-reduced-motion: reduce) {
  .lv-tour__scrim, .lv-tour__hole, .lv-tour__ring, .lv-tour__tick,
  .lv-tour__label, .lv-tour__title, .lv-tour__line,
  .lv-tour__ok { animation-duration:1ms; animation-delay:0s; }
}

.lv-admin { flex:1; min-height:0; overflow:auto; padding:var(--la-s5); background:var(--la-bg); }
.lv-admin__inner { max-width:900px; margin:0 auto; display:flex; flex-direction:column; gap:var(--la-s4); }

.la-input {
  font-family:var(--la-font); font-size:var(--la-text-field); color:var(--la-ink);
  background:var(--la-paper); border:var(--la-frame) solid var(--la-line);
  border-radius:var(--la-radius); padding:var(--la-pad-input); min-height:var(--la-control-h);
  width:100%;
}
.la-input::placeholder { color:var(--la-placeholder); }
.la-input:focus { outline:none; border-color:var(--la-teal); }

.lv-card { background:var(--la-paper); border:var(--la-frame) solid var(--la-line);
           border-radius:var(--la-radius); padding:var(--la-s3); display:flex; gap:var(--la-s3);
           align-items:flex-start; }
.lv-card[data-hidden="true"] { opacity:var(--la-disabled-fade); }
.lv-card__fields { flex:1; min-width:0; display:grid; grid-template-columns:1fr 1fr; gap:var(--la-s2); }
.lv-card__fields .lv-span { grid-column:1 / -1; }
.lv-card__acts { display:flex; flex-direction:column; gap:var(--la-s1); flex-shrink:0; }
.lv-label { font-family:var(--la-font); font-size:var(--la-text-label); font-weight:var(--la-weight);
            text-transform:uppercase; letter-spacing:var(--la-track-label); color:var(--la-muted);
            display:flex; flex-direction:column; gap:var(--la-s1); min-width:0; }
.lv-err { color:var(--la-coral); text-transform:none; letter-spacing:0; }
@media (max-width: 700px) { .lv-card__fields { grid-template-columns:1fr; } }

/* Live client websites lead; demonstration designs remain a separate group. */
.lv-grid__heading { grid-column: 1 / -1; text-align: center; margin: 0; }
.lv-grid__heading h2 { font: 400 clamp(30px,4vw,44px)/1.2 'Lobster',cursive; margin: 0 0 12px; }
.lv-grid__heading p { margin: 0; font-size: 17px; line-height: 1.5; color: var(--lv-ink-soft); }
.lv-grid__heading:not(:first-child) { margin-top: 32px; padding-top: 40px; border-top: 1px solid var(--lv-hair); }
