* { box-sizing: border-box; margin: 0; }
html, body { height: 100%; }
/* chalk handwriting — bundled so the board looks the same on every screen */
@font-face {
  font-family: "Caveat Brush";
  src: url("fonts/CaveatBrush-Regular.ttf") format("truetype");
  font-display: swap;
}
/* collegiate sports block — pick it per board in venue config ("Graduate") */
@font-face {
  font-family: "Graduate";
  src: url("fonts/Graduate-Regular.ttf") format("truetype");
  font-display: swap;
}
body {
  background: #2e332e;
  font-family: var(--font), "Chalkboard SE", "Comic Sans MS", cursive;
  overflow: hidden;
  cursor: none; /* kiosk: hide the cursor */
}
/* Launch splash: chalkboard texture + centered wordmark. Dismissed by
   dismissSplash() in tv.js on the first board render. */
#splash {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #2e332e url(chalkboard-splash.png) center / cover no-repeat;
  transition: opacity 0.6s ease;
}
#splash img {
  width: 36vmin;
}
#splash.hide {
  opacity: 0;
  pointer-events: none;
}
.tv {
  /* fill the chalkboard area inside the wooden frame rails */
  position: absolute;
  inset: 2.67vw;
  transition: bottom 0.4s ease; /* smooth shift when the ticker appears */
  background:
    radial-gradient(ellipse at 20% 10%, rgba(255,255,255,0.03), transparent 50%),
    radial-gradient(ellipse at 80% 90%, rgba(255,255,255,0.02), transparent 50%),
    url(chalkboard-bg.png) center / cover no-repeat,
    #2e332e;
  box-shadow:
    inset 0 0 12vmin rgba(0,0,0,0.55),
    inset 0 0 2.5vw rgba(0,0,0,0.65); /* 3D: chalk surface recessed behind the frame */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 4vmin;
}
:root {
  --chalk: #fdfdf3;
  /* per-board overrides from the API — each defaults to the chalk color */
  --venue-color: var(--chalk);
  --message-color: var(--chalk);
  /* per-board fonts from the API — the venue picks them, both default to Caveat Brush */
  --font: "Caveat Brush";
  --venue-font: "Caveat Brush";
}
.brand {
  position: absolute;
  top: 2vmin; /* moved up 20px */
  width: 111.11vmin; /* doubled */
  height: 8.33vmin; /* bottom up 10px — top stays at 3.85vmin */
  display: flex;
  align-items: center;
  justify-content: center;
  letter-spacing: 0.4em;
  white-space: nowrap; /* venue stays single-line while it auto-fits */
  font-family: var(--venue-font), "Chalkboard SE", "Comic Sans MS", cursive;
  font-size: 6.67vmin; /* doubled to match the doubled box */
  color: var(--venue-color);
  text-transform: uppercase;
}
.tv-message {
  position: absolute;
  /* vertical band: 3.7vmin below the bottom of the (doubled) top-left corner -> top of the QR corner.
     .tv is inset 2.67vw from the viewport, corners scale with vmin. */
  top: calc(17.13vmin - 2.67vw); /* top edge up 55px from original */
  /* corrected: v96 intent was 20px taller + down 40px (bottom edge down, not up) */
  bottom: calc(23.14vmin - 2.67vw); /* bottom edge up 30px from original */
  /* 10.37vmin from the viewport's left/right edges: widened another 100px (50px per side) */
  left: calc(10.37vmin - 2.67vw);
  right: calc(10.37vmin - 2.67vw);
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  font-size: 6.67vmin; /* starting size — fitMessage() in tv.js auto-sizes from here */
  font-weight: 400;
  line-height: 1.0;
  /* chalk writing */
  color: var(--message-color);
  text-shadow:
    0 0 0.09vmin rgba(253,253,243,0.9),
    0.09vmin 0.09vmin 0 rgba(253,253,243,0.15);
  transform: rotate(-0.5deg);
}
/* inner span is what fitMessage() measures — a flex item's own box is
   reliable, while a centered flex container's scroll size is not */
.tv-message > span {
  overflow-wrap: break-word;
  text-align: center;
}
.tv-footer {
  position: absolute;
  bottom: 5.07vmin; /* bottom edge up 10px from the original 6vmin */
  width: 111.11vmin; /* same as the venue box */
  height: 8.33vmin; /* same as the venue box (border-box: padding included) */
  display: flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap; /* author line stays single-line while it scales */
  font-size: 2.41vmin;
  color: rgba(253,253,243,0.75);
  text-shadow: 0 0 0.09vmin rgba(253,253,243,0.6);
}
.dot { margin: 0 0.75em; }
/* Footer styling: the author wears the message color; the "next message"
/  countdown matches the venue name's color and font. */
#tv-author { color: var(--message-color); }
#tv-next { color: var(--venue-color); font-family: var(--venue-font); }
/* wooden frame — 1 physical inch on a 43" 16:9 TV
   (screen is ~37.5" wide, so 1" ≈ 2.67vw); real wood-grain texture,
   grain runs along each rail */
.frame {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  transition: bottom 0.4s ease; /* smooth shift when the ticker appears */
}
.rail { position: absolute; }
/* 3D: each rail casts a drop shadow onto the recessed chalkboard */
.rail.top {
  top: 0; left: 0; right: 0; height: 2.67vw;
  background: url(wood-h.png) repeat-x center;
  box-shadow: 0 1.2vw 1.8vw rgba(0,0,0,0.55);
}
.rail.bottom {
  bottom: 0; left: 0; right: 0; height: 2.67vw;
  background: url(wood-h.png) repeat-x center;
  box-shadow: 0 -1.2vw 1.8vw rgba(0,0,0,0.55);
}
.rail.left {
  top: 0; bottom: 0; left: 0; width: 2.67vw;
  background: url(wood-v.png) repeat-y center;
  box-shadow: 1.2vw 0 1.8vw rgba(0,0,0,0.55);
}
.rail.right {
  top: 0; bottom: 0; right: 0; width: 2.67vw;
  background: url(wood-v.png) repeat-y center;
  box-shadow: -1.2vw 0 1.8vw rgba(0,0,0,0.55);
}
/* wooden square in the top-left corner — 18.52vmin on a side, carries the
   embossed ChalkBox logo. Anchored to the viewport, wood grain texture so it
   reads as part of the frame itself */
.corner {
  position: fixed;
  width: 13.89vmin;
  height: 13.89vmin;
  z-index: 10;
  pointer-events: none;
  background: url(wood-h.png) no-repeat center;
  background-size: cover;
}
.corner.tl {
  top: 0;
  left: 0;
  width: calc(18.52vmin - 10px);
  height: 18.52vmin;
  border-bottom-right-radius: 3.7vmin;
  box-shadow: 1.73vmin 1.73vmin 2.47vmin rgba(0,0,0,0.55); /* 3D: raised wood over the chalk */
}
/* wooden square in the bottom-right corner — 27.78vmin on a side (doubled), outer edges
   flush with the screen; the inner (top-left) corner radius matches the
   padding around the QR code */
.corner-sq {
  position: fixed;
  bottom: 0;
  right: 0;
  width: 27.78vmin;
  height: 27.78vmin;
  z-index: 10;
  pointer-events: none;
  transition: bottom 0.4s ease; /* moves with the frame when ticker appears */
  background: url(wood-h.png) no-repeat center;
  background-size: cover;
  border-top-left-radius: 2.78vmin;
  box-shadow: -1.3vmin -1.3vmin 1.85vmin rgba(0,0,0,0.55); /* 3D: raised wood over the chalk */
}
/* frame styles — applied as frame-<name> on <body> by tv.js from the board's
   "frame" config. "wood" needs no overrides (base styles above are the wood
   frame). Corner pieces follow the rail texture so the frame reads as one. */
/* Hide rails/corners until the first /api/state lands, so the default wood
   never flashes on load when the board uses another frame. */
body.frame-pending .rail, body.frame-pending .corner, body.frame-pending .corner-sq {
  visibility: hidden;
}
body.frame-neon .rail.top, body.frame-neon .rail.bottom {
  background-image: linear-gradient(to bottom, #04040c 0%, #141428 40%, #00e5ff 50%, #141428 60%, #04040c 100%);
  background-repeat: no-repeat;
  box-shadow: 0 0 2.2vw rgba(0,229,255,0.75), 0 1.2vw 1.8vw rgba(0,0,0,0.55);
}
body.frame-neon .rail.bottom {
  box-shadow: 0 0 2.2vw rgba(0,229,255,0.75), 0 -1.2vw 1.8vw rgba(0,0,0,0.55);
}
body.frame-neon .rail.left, body.frame-neon .rail.right {
  background-image: linear-gradient(to right, #04040c 0%, #141428 40%, #00e5ff 50%, #141428 60%, #04040c 100%);
  background-repeat: no-repeat;
  box-shadow: 0 0 2.2vw rgba(0,229,255,0.75), 1.2vw 0 1.8vw rgba(0,0,0,0.55);
}
body.frame-neon .rail.right {
  box-shadow: 0 0 2.2vw rgba(0,229,255,0.75), -1.2vw 0 1.8vw rgba(0,0,0,0.55);
}
body.frame-neon .corner.tl {
  background-image: linear-gradient(to bottom, #04040c 0%, #141428 40%, #00e5ff 50%, #141428 60%, #04040c 100%);
  box-shadow: 0 0 2.2vw rgba(0,229,255,0.6), 1.73vmin 1.73vmin 2.47vmin rgba(0,0,0,0.55);
}
body.frame-neon .corner-sq {
  background-image: radial-gradient(circle at 50% 50%, #101024 0%, #04040c 75%);
  box-shadow: 0 0 2.2vw rgba(0,229,255,0.6), -1.3vmin -1.3vmin 1.85vmin rgba(0,0,0,0.55);
}
body.frame-metal .rail.top, body.frame-metal .rail.bottom {
  background-image: repeating-linear-gradient(to bottom, #6f747d 0, #a9afb8 2px, #d8dde4 3px, #878d97 5px);
  background-repeat: no-repeat;
}
body.frame-metal .rail.left, body.frame-metal .rail.right {
  background-image: repeating-linear-gradient(to right, #6f747d 0, #a9afb8 2px, #d8dde4 3px, #878d97 5px);
  background-repeat: no-repeat;
}
body.frame-metal .corner.tl {
  background-image: repeating-linear-gradient(to bottom, #6f747d 0, #a9afb8 2px, #d8dde4 3px, #878d97 5px);
}
body.frame-metal .corner-sq {
  background-image: repeating-linear-gradient(135deg, #6f747d 0, #a9afb8 2px, #d8dde4 3px, #878d97 5px);
}
body.frame-black .rail.top, body.frame-black .rail.bottom,
body.frame-black .rail.left, body.frame-black .rail.right,
body.frame-black .corner.tl, body.frame-black .corner-sq {
  background-image: linear-gradient(180deg, #262628 0%, #0e0e0f 100%);
  background-repeat: no-repeat;
}
body.frame-gold .rail.top, body.frame-gold .rail.bottom {
  background-image: linear-gradient(to bottom, #5e430d 0%, #b8860b 25%, #f9e38c 50%, #b8860b 75%, #5e430d 100%);
  background-repeat: no-repeat;
}
body.frame-gold .rail.left, body.frame-gold .rail.right {
  background-image: linear-gradient(to right, #5e430d 0%, #b8860b 25%, #f9e38c 50%, #b8860b 75%, #5e430d 100%);
  background-repeat: no-repeat;
}
body.frame-gold .corner.tl {
  background-image: linear-gradient(to bottom, #5e430d 0%, #b8860b 25%, #f9e38c 50%, #b8860b 75%, #5e430d 100%);
}
body.frame-gold .corner-sq {
  background-image: linear-gradient(135deg, #5e430d 0%, #b8860b 25%, #f9e38c 50%, #b8860b 75%, #5e430d 100%);
}
/* "diamond" — diamond tread plate (procedural tile, diamond-plate.png) */
body.frame-diamond .rail.top, body.frame-diamond .rail.bottom {
  background-image: url(diamond-plate.png);
  background-repeat: repeat;
}
body.frame-diamond .rail.left, body.frame-diamond .rail.right {
  background-image: url(diamond-plate.png);
  background-repeat: repeat;
}
body.frame-diamond .corner.tl, body.frame-diamond .corner-sq {
  background-image: url(diamond-plate.png);
  background-size: auto;
}
/* "red" — gloss lacquered red */
body.frame-red .rail.top, body.frame-red .rail.bottom {
  background-image: linear-gradient(to bottom, #420707 0%, #a3121a 30%, #e5383b 44%, #ff9a9d 50%, #e5383b 56%, #a3121a 70%, #420707 100%);
  background-repeat: no-repeat;
}
body.frame-red .rail.left, body.frame-red .rail.right {
  background-image: linear-gradient(to right, #420707 0%, #a3121a 30%, #e5383b 44%, #ff9a9d 50%, #e5383b 56%, #a3121a 70%, #420707 100%);
  background-repeat: no-repeat;
}
body.frame-red .corner.tl {
  background-image: linear-gradient(to bottom, #420707 0%, #a3121a 30%, #e5383b 44%, #ff9a9d 50%, #e5383b 56%, #a3121a 70%, #420707 100%);
}
body.frame-red .corner-sq {
  background-image: linear-gradient(135deg, #420707 0%, #a3121a 30%, #e5383b 44%, #ff9a9d 50%, #e5383b 56%, #a3121a 70%, #420707 100%);
}
/* "white" — matte white */
body.frame-white .rail.top, body.frame-white .rail.bottom,
body.frame-white .rail.left, body.frame-white .rail.right {
  background-image: linear-gradient(to bottom, #d9d6d0 0%, #efede8 50%, #d9d6d0 100%);
  background-repeat: no-repeat;
}
body.frame-white .corner.tl, body.frame-white .corner-sq {
  background-image: linear-gradient(135deg, #d9d6d0 0%, #efede8 50%, #d9d6d0 100%);
}
/* "bamboo" — segmented stalks with node rings */
body.frame-bamboo .rail.top, body.frame-bamboo .rail.bottom {
  background-color: #d9b166;
  background-image:
    repeating-linear-gradient(to right, transparent 0px, transparent 106px, rgba(101,67,33,0.75) 112px, rgba(101,67,33,0.75) 116px, rgba(255,244,214,0.55) 118px, rgba(255,244,214,0.55) 122px, transparent 126px),
    repeating-linear-gradient(to bottom, rgba(101,67,33,0.07) 0px, rgba(101,67,33,0.07) 1px, transparent 1px, transparent 4px),
    linear-gradient(to bottom, #e9cc8a 0%, #d9b166 38%, #c0954f 55%, #d9b166 72%, #e9cc8a 100%);
  background-repeat: repeat;
}
body.frame-bamboo .rail.left, body.frame-bamboo .rail.right {
  background-color: #d9b166;
  background-image:
    repeating-linear-gradient(to bottom, transparent 0px, transparent 106px, rgba(101,67,33,0.75) 112px, rgba(101,67,33,0.75) 116px, rgba(255,244,214,0.55) 118px, rgba(255,244,214,0.55) 122px, transparent 126px),
    repeating-linear-gradient(to right, rgba(101,67,33,0.07) 0px, rgba(101,67,33,0.07) 1px, transparent 1px, transparent 4px),
    linear-gradient(to right, #e9cc8a 0%, #d9b166 38%, #c0954f 55%, #d9b166 72%, #e9cc8a 100%);
  background-repeat: repeat;
}
body.frame-bamboo .corner.tl, body.frame-bamboo .corner-sq {
  background-color: #d9b166;
  background-image:
    repeating-linear-gradient(to right, transparent 0px, transparent 106px, rgba(101,67,33,0.75) 112px, rgba(101,67,33,0.75) 116px, rgba(255,244,214,0.55) 118px, rgba(255,244,214,0.55) 122px, transparent 126px),
    linear-gradient(135deg, #e9cc8a 0%, #d9b166 50%, #c0954f 100%);
}
/* "natural" — unstained light wood (procedural textures) */
body.frame-natural .rail.top, body.frame-natural .rail.bottom {
  background: url(natural-h.png) repeat-x center;
}
body.frame-natural .rail.left, body.frame-natural .rail.right {
  background: url(natural-v.png) repeat-y center;
}
body.frame-natural .corner.tl, body.frame-natural .corner-sq {
  background-image: url(natural-h.png);
}
/* "stone" — cut limestone blocks, running bond (procedural textures) */
body.frame-stone .rail.top, body.frame-stone .rail.bottom {
  background: url(stone-h.png) repeat-x center;
}
body.frame-stone .rail.left, body.frame-stone .rail.right {
  background: url(stone-v.png) repeat-y center;
}
body.frame-stone .corner.tl, body.frame-stone .corner-sq {
  background-image: url(stone-h.png);
}
/* "granite" — polished speckled slab (procedural textures) */
body.frame-granite .rail.top, body.frame-granite .rail.bottom {
  background: url(granite-h.png) repeat-x center;
}
body.frame-granite .rail.left, body.frame-granite .rail.right {
  background: url(granite-v.png) repeat-y center;
}
body.frame-granite .corner.tl, body.frame-granite .corner-sq {
  background-image: url(granite-h.png);
}
/* "gunmetal" — dark brushed gunmetal (procedural textures) */
body.frame-gunmetal .rail.top, body.frame-gunmetal .rail.bottom {
  background: url(gunmetal-h.png) repeat-x center;
}
body.frame-gunmetal .rail.left, body.frame-gunmetal .rail.right {
  background: url(gunmetal-v.png) repeat-y center;
}
body.frame-gunmetal .corner.tl, body.frame-gunmetal .corner-sq {
  background-image: url(gunmetal-h.png);
}
/* "stainless" — bright fine-brushed stainless steel (procedural textures) */
body.frame-stainless .rail.top, body.frame-stainless .rail.bottom {
  background: url(stainless-h.png) repeat-x center;
}
body.frame-stainless .rail.left, body.frame-stainless .rail.right {
  background: url(stainless-v.png) repeat-y center;
}
body.frame-stainless .corner.tl, body.frame-stainless .corner-sq {
  background-image: url(stainless-h.png);
}
/* the QR code — white backing so it scans reliably on the wood; anchored down
   and right, its edges aligned with the frame's inner edge */
.qr-card {
  position: absolute;
  bottom: 2.78vmin;
  right: 2.78vmin;
  width: 20.74vmin;
  height: 20.74vmin;
}
.qr-card svg {
  width: 100%;
  height: 100%;
  display: block;
  border-radius: 0.37vmin;
  box-shadow: -0.37vmin 0.74vmin 1.3vmin rgba(0,0,0,0.55); /* 3D: card raised off the wood */
}
/* ChalkBox logo embossed (raised) in the top-left wooden square, centered —
   fixed with the corner. The hard-offset dark drop-shadow mimics the old
   two-layer carved SVG (dark copy peeking out bottom-right). */
.carve-logo {
  position: fixed;
  top: 1.93vmin; /* centered in the 18.52vmin block: (18.52 - 14.67) / 2 */
  left: calc(2.87vmin - 10px); /* centered, then nudged 10px left per JoeHowe */
  height: 14.67vmin;
  width: auto;
  z-index: 11;
  pointer-events: none;
  filter: drop-shadow(0.47vmin 0.47vmin 0 rgba(31,18,6,0.85));
}
/* (message-change flash removed — the board now swaps text cleanly) */

/* ================= DISPLAY THEMES =================
   The TV display theme is chosen per venue with the "theme" field in
   server/boards.json (exposed via /api/state, applied by tv.js as a
   theme-<name> class on <body>). The styles above ARE the default
   "chalkboard" theme — a board with no theme, or "theme": "chalkboard",
   gets them exactly as-is.

   A new theme only needs an override block below. Example:

     body.theme-neon { background: #05060f; }
     body.theme-neon .tv-message {
       color: #7df9ff;
       text-shadow: 0 0 18px #7df9ff;
     }
     body.theme-neon .corner,
     body.theme-neon .corner-sq { display: none; }  // no wooden frame

   "chalkboard-classic" keeps the previous pure-CSS chalkboard background
   (the texture image is the default since 2026-10-01). A venue opts back
   in with "theme": "chalkboard-classic" in server/boards.json.
*/
body.theme-chalkboard-classic .tv {
  background:
    radial-gradient(ellipse at 20% 10%, rgba(255,255,255,0.03), transparent 50%),
    radial-gradient(ellipse at 80% 90%, rgba(255,255,255,0.02), transparent 50%),
    repeating-linear-gradient(0deg, rgba(255,255,255,0.008) 0 2px, transparent 2px 4px),
    #2e332e;
}

/* ---- board surface styles ----
   Per-board chalkboard surface, applied as board-<preset> on <body> by tv.js
   from the board's "board_style" config ({preset, color}).
   "classic" needs no overrides — the base .tv styles above ARE the classic
   slate + texture look. A custom color wins over any preset via --board-color
   (tv.js sets it when board_style.color is present). */
body.board-slate .tv {
  background: #2e332e;
}
body.board-black .tv {
  background: #161616;
}
body.board-whiteboard .tv {
  background:
    radial-gradient(ellipse at 20% 10%, rgba(0,0,0,0.02), transparent 50%),
    #f7f7f5;
}
body.board-kraft .tv {
  background:
    radial-gradient(ellipse at 20% 10%, rgba(255,255,255,0.06), transparent 50%),
    radial-gradient(ellipse at 80% 90%, rgba(0,0,0,0.08), transparent 50%),
    #a97e4f;
}
body.board-custom .tv {
  background: var(--board-color, #2e332e);
}

/* Optional news-channel ticker: an add-on strip BELOW the framed board
   (not inside the border). The whole board assembly shifts up via
   body.has-ticker; the QR square moves with it and stays intact.
   Hidden unless the board has ticker text. */
:root {
  --ticker-h: 11.29vmin;
}
.ticker {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--ticker-h);
  z-index: 40; /* above the frame rails, below the splash (60) */
  background: #b3122e; /* broadcast red */
  color: #fff;
  font-family: Arial, Helvetica, sans-serif;
  font-weight: 700;
  font-size: 5.81vmin;
  line-height: 1;
  display: flex;
  align-items: center;
  overflow: hidden;
  white-space: nowrap;
  box-shadow: 0 -0.5vmin 2vmin rgba(0, 0, 0, 0.5);
}
.ticker[hidden] {
  display: none;
}
/* Board assembly shifts up to make room for the add-on strip. */
body.has-ticker .frame {
  bottom: var(--ticker-h);
}
body.has-ticker .tv {
  bottom: calc(2.67vw + var(--ticker-h));
}
body.has-ticker .corner-sq {
  bottom: var(--ticker-h);
}
.ticker-inner {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  animation-name: ticker-scroll;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  will-change: transform;
}
.ticker-item {
  padding: 0 3vw; /* breathing room around each item */
}
.ticker-sep {
  opacity: 0.9;
}
@keyframes ticker-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); } /* one unit width: seamless loop */
}
