/* ============================================================================
   Animal Island skin — everything here is scoped to :root[data-skin="island"],
   so this file is inert until the leaf button in the topbar turns the skin on.

   Design tokens, fonts and artwork come from Animal Island UI by guokaigdg
   (https://github.com/guokaigdg/animal-island-ui), licensed CC BY-NC 4.0 —
   full text in /assets/island/LICENSE-animal-island-ui.txt. Used here
   non-commercially.

   Note: some of that artwork (mascot.png, cursor.png) depicts Animal Crossing
   characters and UI, which are Nintendo's and not the upstream author's to
   license. They are used on this personal, non-commercial hobby site only —
   if this site ever becomes commercial, these files have to go.

   Nunito is SIL OFL; Noto Sans SC is SIL OFL. Both are self-hosted rather
   than pulled from Google Fonts, which is unreachable from mainland China.
   ========================================================================= */

/* ---------- fonts ---------------------------------------------------------
   Nunito carries Latin (it is first in the stack); Noto Sans SC is limited to
   CJK by unicode-range so the two never compete for the same glyphs.        */
@font-face {
  font-family: 'Nunito'; font-style: normal; font-weight: 500; font-display: swap;
  src: url('/assets/island/fonts/nunito-latin-500-normal.woff2') format('woff2');
}
@font-face {
  font-family: 'Nunito'; font-style: normal; font-weight: 700; font-display: swap;
  src: url('/assets/island/fonts/nunito-latin-700-normal.woff2') format('woff2');
}
@font-face {
  font-family: 'Nunito'; font-style: normal; font-weight: 900; font-display: swap;
  src: url('/assets/island/fonts/nunito-latin-900-normal.woff2') format('woff2');
}
@font-face {
  font-family: 'Noto Sans SC'; font-style: normal; font-weight: 500; font-display: swap;
  src: url('/assets/island/fonts/noto-sans-sc-chinese-simplified-500-normal.woff2') format('woff2');
  unicode-range: U+2E80-9FFF, U+3000-303F, U+31C0-31EF, U+3200-32FF, U+FF00-FFEF;
}
@font-face {
  font-family: 'Noto Sans SC'; font-style: normal; font-weight: 700; font-display: swap;
  src: url('/assets/island/fonts/noto-sans-sc-chinese-simplified-700-normal.woff2') format('woff2');
  unicode-range: U+2E80-9FFF, U+3000-303F, U+31C0-31EF, U+3200-32FF, U+FF00-FFEF;
}

/* ---------- tokens --------------------------------------------------------
   These two blocks must define the SAME token list. They have competing
   specificity, so a token present only in the light block would leak its
   light value into dark mode.                                              */
:root[data-skin="island"] {
  --background: #f8f8f0;
  --card: #f7f3df;
  --foreground: #6a5238;
  --muted-foreground: #7a6a54;
  --border: #c4b89e;
  --primary: #19c8b9;
  --primary-foreground: #0c4a45;
  --secondary: #f0e8d8;
  --ring: #a89878;
  --sift: #6fba2c;
  --sift-bg: #eef7e2;
  --loftr: #e0a90c;
  --loftr-bg: #fdf4d9;
  --template: #8b7ad6;
  --template-bg: #efeafa;
  --destructive: #d64b4b;
  --radius: 18px;
  --shadow-sm: 0 2px 4px 0 rgb(61 52 40 / 0.06);
  --shadow-md: 0 3px 10px 0 rgb(61 52 40 / 0.1), 0 8px 24px 0 rgb(61 52 40 / 0.1);
  --island-depth: #bdaea0;
  --island-depth-soft: #d4c9b4;
  --island-grass: #7dc395;
  --island-panel-edge: #fafafa;
}
:root[data-skin="island"][data-theme="dark"] {
  --background: #26211a;
  --card: #332c22;
  --foreground: #f2e7d2;
  --muted-foreground: #b9a98d;
  --border: #4a4030;
  --primary: #19c8b9;
  --primary-foreground: #0c4a45;
  --secondary: #3d3527;
  --ring: #6b5c45;
  --sift: #8fd44f;
  --sift-bg: rgb(143 212 79 / 0.13);
  --loftr: #f7cf46;
  --loftr-bg: rgb(247 207 70 / 0.13);
  --template: #a998e8;
  --template-bg: rgb(169 152 232 / 0.15);
  --destructive: #f08080;
  --radius: 18px;
  --shadow-sm: 0 2px 4px 0 rgb(0 0 0 / 0.25);
  --shadow-md: 0 3px 10px 0 rgb(0 0 0 / 0.3);
  --island-depth: #191410;
  --island-depth-soft: #221c15;
  --island-grass: #2c5443;
  --island-panel-edge: #4a4030;
}

/* ---------- type + the grass backdrop ----------------------------------- */
:root[data-skin="island"] body {
  font-family: Nunito, 'Noto Sans SC', -apple-system, 'PingFang SC',
               'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
  font-weight: 500;
  letter-spacing: 0.01em;
  background: url('/assets/island/bg-pattern.jpg') center / auto repeat;
  cursor: url('/assets/island/cursor.png') 4 0, default;
}
/* the tile is a light-mode green; darken it rather than ship a second image */
:root[data-skin="island"][data-theme="dark"] body {
  background-color: #24352c;
  background-blend-mode: multiply;
}
:root[data-skin="island"] a,
:root[data-skin="island"] button,
:root[data-skin="island"] label,
:root[data-skin="island"] select,
:root[data-skin="island"] summary,
:root[data-skin="island"] [role="button"] {
  cursor: url('/assets/island/cursor.png') 4 0, pointer;
}
:root[data-skin="island"] input[type=text],
:root[data-skin="island"] textarea { cursor: text; }

:root[data-skin="island"] h1,
:root[data-skin="island"] .card-title,
:root[data-skin="island"] .proj-head h1,
:root[data-skin="island"] .wordmark { font-weight: 900; letter-spacing: 0.01em; }

/* ---------- content sits on a cream panel, never straight on the grass --- */
:root[data-skin="island"] .page {
  background: var(--card);
  border: 3px solid var(--island-panel-edge);
  border-radius: 24px;
  box-shadow: 0 4px 16px rgb(61 52 40 / 0.16);
  margin: 14px auto 40px;
  padding: 24px 22px 28px;
}
/* nested cards need to separate from the panel they now sit on */
:root[data-skin="island"] .card,
:root[data-skin="island"] .proj-card,
:root[data-skin="island"] .exp-panel,
:root[data-skin="island"] .pv-card,
:root[data-skin="island"] #canvasWrap { border-width: 2px; }
:root[data-skin="island"] .card { background: var(--background); }

/* ---------- buttons: the game-UI drop step ------------------------------ */
:root[data-skin="island"] .btn {
  border-radius: 50px; border-width: 2px;
  font-weight: 700; letter-spacing: 0.02em;
  transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1),
              box-shadow 0.15s cubic-bezier(0.4, 0, 0.2, 1), background 0.15s;
}
:root[data-skin="island"] .btn.primary { box-shadow: 0 5px 0 0 var(--island-depth); }
:root[data-skin="island"] .btn.primary:hover {
  opacity: 1; transform: translateY(-1px); box-shadow: 0 6px 0 0 var(--island-depth);
}
:root[data-skin="island"] .btn.primary:active {
  transform: translateY(2px); box-shadow: 0 1px 0 0 var(--island-depth);
}
:root[data-skin="island"] .btn.ghost { box-shadow: 0 3px 0 0 var(--island-depth-soft); }
:root[data-skin="island"] .btn.ghost:hover { transform: translateY(-1px); }
:root[data-skin="island"] .btn.ghost:active {
  transform: translateY(2px); box-shadow: 0 1px 0 0 var(--island-depth-soft);
}

/* ---------- inputs and selects: 2.5px rim + 3D lip ---------------------- */
:root[data-skin="island"] select,
:root[data-skin="island"] input[type=text],
:root[data-skin="island"] .lang-sel {
  border-width: 2.5px; border-radius: 12px; font-weight: 600;
  box-shadow: 0 3px 0 0 var(--island-depth-soft);
  transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.15s;
}
:root[data-skin="island"] select:focus,
:root[data-skin="island"] input[type=text]:focus {
  outline: none; border-color: var(--primary);
  box-shadow: 0 3px 0 0 var(--island-depth-soft), 0 0 0 3px rgb(25 200 185 / 0.25);
}
:root[data-skin="island"] .theme-btn {
  border-width: 2px; border-radius: 50%;
  box-shadow: 0 3px 0 0 var(--island-depth-soft);
}
:root[data-skin="island"] .theme-btn:active {
  transform: translateY(2px); box-shadow: 0 1px 0 0 var(--island-depth-soft);
}
:root[data-skin="island"] .dropzone { border-width: 2.5px; border-radius: 16px; }
:root[data-skin="island"] .step-no {
  border-width: 2px; border-radius: 50px; font-weight: 700;
}
:root[data-skin="island"] .job,
:root[data-skin="island"] .cand { border-width: 2px; border-radius: 14px; }
:root[data-skin="island"] .proj-card:hover { transform: translateY(-2px); }
:root[data-skin="island"] .summary img,
:root[data-skin="island"] #pieceThumb { border-width: 2px; border-radius: 12px; }
:root[data-skin="island"] .badge { font-family: Nunito, sans-serif; font-weight: 900; }

/* ---------- custom dropdown (island.js builds the markup) ---------------
   Inert by default: the trigger and menu stay hidden and the native <select>
   shows through, so turning the skin off needs no teardown.               */
.isl-select { position: relative; display: inline-block; }
.isl-trigger, .isl-list { display: none; }

:root[data-skin="island"] .isl-select > select { display: none; }
:root[data-skin="island"] .isl-trigger {
  display: inline-flex; align-items: center; justify-content: space-between;
  gap: 10px; min-width: 104px; width: 100%;
  font: inherit; font-size: 13px; font-weight: 700;
  color: var(--foreground); background: var(--background);
  border: 2.5px solid var(--border); border-radius: 12px;
  padding: 7px 12px; cursor: url('/assets/island/cursor.png') 4 0, pointer;
  box-shadow: 0 3px 0 0 var(--island-depth-soft);
  transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.15s;
}
:root[data-skin="island"] .isl-trigger:hover { border-color: var(--ring); }
:root[data-skin="island"] .isl-select.open .isl-trigger {
  border-color: var(--primary); transform: translateY(1px);
  box-shadow: 0 2px 0 0 var(--island-depth-soft);
}
:root[data-skin="island"] .isl-arrow {
  display: flex; color: var(--muted-foreground); transition: transform 0.2s ease, color 0.2s ease;
}
:root[data-skin="island"] .isl-select.open .isl-arrow {
  transform: rotate(180deg); color: var(--primary);
}
:root[data-skin="island"] .isl-select.open .isl-list {
  display: block; position: absolute; top: calc(100% + 8px); left: 0; z-index: 100;
  min-width: 100%; width: max-content; max-width: min(84vw, 320px);
  background: #ffeea0; border-radius: 26px; padding: 12px 0;
  box-shadow: 0 8px 24px 0 rgb(61 52 40 / 0.24);
  animation: isl-drop 0.18s ease forwards;
  /* the piece-count list is 9 long and would otherwise run off the card */
  max-height: min(56vh, 320px); overflow-y: auto; overscroll-behavior: contain;
}
:root[data-skin="island"] .isl-select.flip-right.open .isl-list { left: auto; right: 0; }
@keyframes isl-drop {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
:root[data-skin="island"] .isl-opt {
  position: relative; display: flex; align-items: center; justify-content: center;
  padding: 9px 26px 9px 30px; font-size: 13.5px; font-weight: 500;
  color: #725d42; white-space: nowrap;
  cursor: url('/assets/island/cursor.png') 4 0, pointer;
}
:root[data-skin="island"] .isl-opt.active { font-weight: 800; }
:root[data-skin="island"] .isl-opt:hover { font-weight: 800; }
/* the pointing hand slides in beside whichever row you are on */
:root[data-skin="island"] .isl-opt:hover::before {
  content: ""; position: absolute; left: 2px; top: 50%;
  width: 30px; height: 30px; transform: translateY(-50%);
  background: url('/assets/island/cursor-select.svg') center / contain no-repeat;
  animation: isl-hand 0.42s ease-out forwards;
}
@keyframes isl-hand {
  0%   { opacity: 0; transform: translateY(-50%) translateX(-16px) rotate(-15deg); }
  60%  { opacity: 1; transform: translateY(-50%) translateX(4px) rotate(4deg); }
  100% { opacity: 1; transform: translateY(-50%) translateX(0) rotate(0); }
}
/* the highlight bar behind the current choice */
:root[data-skin="island"] .isl-opt.active::after {
  content: ""; position: absolute; inset: 50% 16px auto 16px; height: 15px;
  transform: translateY(-50%); background: #ffcc00; border-radius: 8px;
  opacity: 0.42; z-index: -1;
}

/* ---------- round checkboxes with the splash burst -----------------------
   island.js wraps each box so the splash ring and the tick have elements of
   their own; ::before/::after on an <input> are not reliable cross-browser. */
:root[data-skin="island"] .isl-cbx { position: relative; width: 21px; height: 21px; flex: none; }
:root[data-skin="island"] .exp-panel input[type="checkbox"],
:root[data-skin="island"] .isl-cbx input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  position: absolute; inset: 0; margin: 0; width: 21px; height: 21px;
  border: 2px solid var(--border); border-radius: 50%;
  background: var(--card);
  cursor: url('/assets/island/cursor.png') 4 0, pointer;
  transition: border-color 0.25s cubic-bezier(0.4, 0, 0.2, 1),
              background 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
:root[data-skin="island"] .isl-cbx input:hover { border-color: var(--ring); }
:root[data-skin="island"] .isl-cbx input:checked {
  background: var(--primary); border-color: #11a89b;
}
:root[data-skin="island"] .isl-splash {
  position: absolute; inset: 0; border-radius: 50%;
  pointer-events: none; transform: translate3d(0, 0, 0);
}
:root[data-skin="island"] .isl-cbx input:checked ~ .isl-splash {
  animation: isl-splash 0.6s ease forwards;
}
/* six dots thrown outward and faded, drawn entirely with box-shadow */
@keyframes isl-splash {
  40% {
    background: var(--primary);
    box-shadow: 0 -18px 0 -8px var(--primary), 16px -8px 0 -8px var(--primary),
                16px 8px 0 -8px var(--primary), 0 18px 0 -8px var(--primary),
                -16px 8px 0 -8px var(--primary), -16px -8px 0 -8px var(--primary);
  }
  100% {
    background: var(--primary);
    box-shadow: 0 -36px 0 -10px transparent, 32px -16px 0 -10px transparent,
                32px 16px 0 -10px transparent, 0 36px 0 -10px transparent,
                -32px 16px 0 -10px transparent, -32px -16px 0 -10px transparent;
  }
}
:root[data-skin="island"] .isl-check {
  position: absolute; top: 50%; left: 50%; width: 12px; height: 11px;
  transform: translate(-50%, -54%); pointer-events: none; z-index: 1;
}
:root[data-skin="island"] .isl-check path {
  stroke: #fff; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;
  fill: none; stroke-dasharray: 19; stroke-dashoffset: 19;
  transition: stroke-dashoffset 0.3s ease 0.2s;
}
:root[data-skin="island"] .isl-cbx input:checked ~ .isl-check path { stroke-dashoffset: 0; }
:root[data-skin="island"] .exp-panel { border-radius: 20px; }
:root[data-skin="island"] .exp-title { font-weight: 800; }

/* ---------- ribbon title ------------------------------------------------
   Dovetails are clipped squares behind the plate, the fold shadows are CSS
   border triangles, and the plate itself gets a hair of perspective. Built
   by island.js around the existing heading so the markup stays plain. */
/* The wrapper is always a plain inline box and the editable name really does
   live inside it, so with the skin off the input is still an ordinary input —
   only the decoration layers below drop out. */
.isl-ribbon { display: inline-flex; align-items: center; }
.isl-ribbon > .isl-rb,
.isl-ribbon > .isl-rk,
.isl-ribbon > .isl-rf,
.isl-ribbon > .isl-rt { display: none; }
:root[data-skin="island"] .isl-ribbon > .isl-rb,
:root[data-skin="island"] .isl-ribbon > .isl-rk,
:root[data-skin="island"] .isl-ribbon > .isl-rf { display: block; }
:root[data-skin="island"] .isl-ribbon > .isl-rt { display: inline-flex; }
:root[data-skin="island"] .isl-ribbon {
  --rf: #27d039; --rb: #20992a; --rk: #115017; --rt: #fff;
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  height: 2em; padding: 0 1.6em; font-size: 21px; font-weight: 900;
  letter-spacing: 0.04em; white-space: nowrap; color: var(--rt);
  filter: drop-shadow(0 0.08em 0.12em rgb(0 0 0 / 0.18));
}
:root[data-skin="island"] .isl-rb {
  position: absolute; font-size: inherit; bottom: -0.4em;
  width: 1.7em; height: 1.7em; background: var(--rb); z-index: 1;
}
:root[data-skin="island"] .isl-rb-l {
  left: -0.6em; border-radius: 0.08em 0 0 0.08em;
  clip-path: polygon(100% 0%, 100% 100%, 0% 100%, 30% 50%, 0% 0%);
}
:root[data-skin="island"] .isl-rb-r {
  right: -0.6em; border-radius: 0 0.08em 0.08em 0;
  clip-path: polygon(0% 0%, 100% 0%, 70% 50%, 100% 100%, 0% 100%);
}
:root[data-skin="island"] .isl-rk {
  position: absolute; font-size: inherit; width: 0; height: 0;
  border-style: solid; z-index: 2; top: calc(100% - 0.05em);
}
:root[data-skin="island"] .isl-rk-l {
  left: 0.15em; border-width: 0 0.95em 0.45em 0;
  border-color: transparent var(--rk) transparent transparent;
}
:root[data-skin="island"] .isl-rk-r {
  right: 0.16em; border-width: 0 0 0.45em 0.95em;
  border-color: transparent transparent transparent var(--rk);
}
:root[data-skin="island"] .isl-rf {
  position: absolute; font-size: inherit; inset: 0 0.1em;
  background: var(--rf); border-radius: 0.2em; z-index: 3;
  transform: perspective(11.5em) rotateX(3deg);
  box-shadow: inset 0 -0.06em 0 rgb(0 0 0 / 0.05); pointer-events: none;
}
:root[data-skin="island"] .isl-rt {
  position: relative; z-index: 4; display: inline-flex; align-items: center;
  height: 2em; padding-top: 0.11em; color: var(--rt);
  text-shadow: 0 0.04em 0.08em rgb(0 0 0 / 0.16);
}
/* the puzzle name stays a real input, just wearing the ribbon */
:root[data-skin="island"] .isl-ribbon > #projName {
  position: relative; z-index: 4; height: 2em; margin: 0; padding: 0 0 0 0;
  background: none; border: none; outline: none; width: auto; max-width: none;
  font: inherit; color: var(--rt); text-align: center;
  text-shadow: 0 0.04em 0.08em rgb(0 0 0 / 0.16);
}
:root[data-skin="island"] .isl-ribbon > #projName:hover,
:root[data-skin="island"] .isl-ribbon > #projName:focus { border: none; background: none; }
/* the plain heading hides while its ribbon stands in */
:root[data-skin="island"] .isl-ribbon-src { display: none; }
/* .proj-head is a flex row and the h1 it replaces carried `flex: 1`, so
   without this the New-project button slides up against the ribbon */
:root[data-skin="island"] .proj-head .isl-ribbon { margin: 4px auto 4px 10px; }
/* the dovetails hang below the plate, so the grid underneath needs clearance */
:root[data-skin="island"] .proj-head { margin-bottom: 34px; }
:root[data-skin="island"] .title-row .isl-ribbon { margin: 4px 0 4px 10px; }

/* ---------- loading curtain (landing → app) -----------------------------
   The GSAP timeline upstream is a set of yoyo tweens; every one of them is a
   plain alternating keyframe, so the animation library is not needed.      */
/* Painted by the inline head script before first paint, so a page arrived at
   through the curtain never flashes its own content first. island.js clears
   it as soon as the real curtain is up. */
html.isl-arriving::before {
  content: ""; position: fixed; inset: 0; background: #000; z-index: 9998;
  /* island.js lifts this within a frame, but if it never runs the visitor
     must not be stranded on a black screen — so it also times itself out */
  animation: isl-arriving-bail 0.4s ease 2.5s forwards;
}
@keyframes isl-arriving-bail { to { opacity: 0; visibility: hidden; } }
.isl-curtain { display: none; }
.isl-curtain.on {
  display: flex; position: fixed; inset: 0; z-index: 9999;
  align-items: flex-end; justify-content: flex-end;
  padding: 0 20px 20px 0; overflow: hidden;
  background: #000;
  animation: isl-curtain-in 0.28s ease forwards;
}
@keyframes isl-curtain-in { from { opacity: 0; } to { opacity: 1; } }
/* On arrival the curtain replaces a black that is already on screen, so it has
   to be opaque on its very first painted frame. Fading it in there let the
   page show through the half-transparent sheet — that was the flash of the
   next page before the circle opened. */
.isl-curtain.instant { animation: none; opacity: 1; }
.isl-curtain svg { max-width: 180px; max-height: 200px; width: 42vw; height: auto; overflow: visible; }

/* The reveal opens a transparent hole in the black so the arriving page is
   visible through it from the first frame — a white circle covering the
   screen first read as a flash, not as a transition.
   The hole is a small circle whose huge box-shadow spread does the blacking
   out, so scaling the element widens the hole while the shadow keeps filling
   the viewport. Pure transform, so it stays on the compositor — animating a
   radial-gradient mask instead repaints the whole screen every frame. */
.isl-iris {
  position: fixed; left: 50%; top: 50%; width: 8px; height: 8px;
  margin: -4px 0 0 -4px; border-radius: 50%;
  box-shadow: 0 0 0 3000px #000;
  transform: scale(1); will-change: transform; pointer-events: none;
}
.isl-curtain.reveal { background: transparent; }
.isl-curtain.reveal .isl-iris {
  animation: isl-iris 0.9s cubic-bezier(0.45, 0, 0.25, 1) forwards;
}
@keyframes isl-iris { to { transform: scale(400); } }
.isl-curtain.reveal svg { opacity: 0; transition: opacity 0.25s; }
.isl-curtain #whole-island { transform-origin: bottom center; animation: isl-bob 2s ease-in-out infinite; }
.isl-curtain #tree  { transform-origin: bottom center; animation: isl-sway 4s ease-in-out infinite; }
.isl-curtain #leaf1 { transform-origin: center right; animation: isl-lift 2s ease-in-out infinite; }
.isl-curtain #leaf2 { transform-origin: bottom right; animation: isl-leaf2 2s ease-in-out infinite; }
.isl-curtain #leaf3 { transform-origin: bottom center; animation: isl-leaf3 2s ease-in-out infinite; }
.isl-curtain #leaf4 { transform-origin: bottom left; animation: isl-leaf4 2s ease-in-out infinite; }
.isl-curtain #leaf5 { transform-origin: top left; animation: isl-lift 2s ease-in-out infinite; }
.isl-curtain #water-circle1 { transform-origin: center; animation: isl-w1 2s ease-in-out infinite; }
.isl-curtain #water-circle2 { transform-origin: center; animation: isl-w2 2s ease-in-out infinite; }
@keyframes isl-bob   { 0%,100% { transform: none; } 50% { transform: translateY(-15px) rotate(1deg); } }
@keyframes isl-sway  { 0%,100% { transform: rotate(-6deg); } 50% { transform: rotate(5deg); } }
@keyframes isl-lift  { 0%,100% { transform: none; } 50% { transform: translateY(-3px); } }
@keyframes isl-leaf2 { 0%,100% { transform: rotate(3deg); } 50% { transform: rotate(-4deg) translate(-3px, -3px); } }
@keyframes isl-leaf3 { 0%,100% { transform: none; } 50% { transform: rotate(-6deg); } }
@keyframes isl-leaf4 { 0%,100% { transform: none; } 50% { transform: rotate(-6deg) translateY(-3px); } }
@keyframes isl-w1    { 0%,100% { transform: scaleX(1); } 50% { transform: scaleX(1.2); } }
@keyframes isl-w2    { 0%,100% { transform: scaleX(1); } 50% { transform: scaleX(0.8); } }
@media (prefers-reduced-motion: reduce) {
  .isl-curtain.on, .isl-curtain * { animation: none !important; }
}

/* ---------- crop dialog ------------------------------------------------- */
.pv-title { display: none; }
:root[data-skin="island"] .pv-overlay { background: rgb(0 0 0 / 0.45); }
:root[data-skin="island"] .pv-card {
  border: 3px solid var(--island-panel-edge); border-radius: 26px;
  padding: 0 16px 16px; overflow: hidden;
  animation: isl-zoom 0.3s cubic-bezier(0.34, 1.3, 0.64, 1);
}
@keyframes isl-zoom {
  from { opacity: 0; transform: scale(0.92); }
  to   { opacity: 1; transform: none; }
}
:root[data-skin="island"] .pv-title {
  display: block; margin: 0 -16px 14px; padding: 13px 20px;
  background: #ffcc00; color: #6a4a12;
  font-size: 16px; font-weight: 900; letter-spacing: 0.02em; text-align: center;
  box-shadow: inset 0 -3px 0 rgb(0 0 0 / 0.08);
}

/* ---------- progress ----------------------------------------------------
   Nothing on the server reports how far along it is, so this is deliberately
   indeterminate — moving stripes say "working" without inventing a number. */
:root[data-skin="island"] .status.busy { flex-wrap: wrap; }
:root[data-skin="island"] .status.busy::after {
  content: ""; flex: 0 0 100%; height: 18px; margin-top: 4px;
  border: 2px solid var(--border); border-radius: 999px;
  background-color: #0ec4b6;
  background-image: repeating-linear-gradient(-45deg,
    #0ec4b6 0, #0ec4b6 10px, #01b0a7 10px, #01b0a7 20px);
  background-size: 28.28px 28.28px;
  box-shadow: inset 0 2px 4px rgb(114 93 66 / 0.18);
  animation: isl-stripe 1s linear infinite;
}
@keyframes isl-stripe {
  from { background-position: 0 0; }
  to   { background-position: -28.28px 0; }
}

/* ---------- error toast (island.js raises it) --------------------------- */
.isl-toasts {
  position: fixed; top: 18px; left: 50%; transform: translateX(-50%);
  z-index: 2000; display: flex; flex-direction: column; gap: 10px;
  pointer-events: none; max-width: calc(100vw - 32px);
}
.isl-toast { display: none; }
:root[data-skin="island"] .isl-toast {
  display: flex; align-items: flex-start; gap: 10px;
  background: var(--card); color: var(--foreground);
  border: 2.5px solid var(--destructive); border-radius: 18px;
  box-shadow: 0 6px 20px rgb(61 52 40 / 0.28), 0 4px 0 0 var(--island-depth);
  padding: 12px 16px; font-size: 13.5px; font-weight: 600; line-height: 1.4;
  max-width: 420px; pointer-events: auto;
  animation: isl-toast-in 0.32s cubic-bezier(0.34, 1.3, 0.64, 1);
}
:root[data-skin="island"] .isl-toast.out {
  animation: isl-toast-out 0.28s ease forwards;
}
:root[data-skin="island"] .isl-toast::before {
  content: "!"; flex: none;
  width: 21px; height: 21px; border-radius: 50%;
  background: var(--destructive); color: #fff;
  font-weight: 900; font-size: 14px; text-align: center; line-height: 21px;
}
@keyframes isl-toast-in  { from { opacity: 0; transform: translateY(-14px); } }
@keyframes isl-toast-out { to   { opacity: 0; transform: translateY(-14px); } }

/* ---------- match badges ------------------------------------------------ */
:root[data-skin="island"] .badge {
  border: 1.5px solid currentColor; padding: 3px 11px;
  font-family: inherit; font-weight: 800; letter-spacing: 0.03em;
  box-shadow: 0 2px 0 0 rgb(61 52 40 / 0.13);
}

/* ---------- the leaf replaces the generic spinner ----------------------- */
:root[data-skin="island"] .spinner {
  border: none; border-radius: 0; animation: none;
  background: url('/assets/island/leaf.png') center / contain no-repeat;
  animation: island-leaf 1.1s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
@keyframes island-leaf {
  0%   { transform: rotate(-18deg) scale(0.9); opacity: 0.7; }
  50%  { transform: rotate(18deg)  scale(1.1); opacity: 1; }
  100% { transform: rotate(-18deg) scale(0.9); opacity: 0.7; }
}

/* ---------- footer gets the shoreline ---------------------------------- */
:root[data-skin="island"] footer {
  position: relative; padding-bottom: 74px; margin-top: 32px;
}
:root[data-skin="island"] footer::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 64px;
  background: url('/assets/island/footer-sea.svg') repeat-x left bottom / auto 100%;
  pointer-events: none;
}
/* the shoreline is drawn for daylight; dim it so it reads as night, not neon */
:root[data-skin="island"][data-theme="dark"] footer::after { opacity: 0.45; }

/* ---------- the mascot, dropped into empty states ----------------------- */
:root[data-skin="island"] .empty-state::before,
:root[data-skin="island"] .island-mascot {
  content: ""; display: block; height: 132px; margin: 0 auto 14px;
  background: url('/assets/island/mascot.png') center / contain no-repeat;
}

/* ---------- landing page ------------------------------------------------
   The whole page is the drifting leaf field; nothing gets an outer cream
   panel any more. Section headings sit straight on the grass in cream with a
   shadow, and only the innermost boxes (demo, step cards) stay cream — one
   layer of box instead of two.                                            */
:root[data-skin="island"] body.home {
  background: url('/assets/island/bg-leaf.webp') 0 0 / auto repeat, var(--island-grass);
  animation: island-bg-scroll 80s linear infinite;
}
@keyframes island-bg-scroll {
  from { background-position: 0 0; }
  to   { background-position: 0 -1000px; }
}
/* The bar floats on the grass with only the skin toggle left — everything
   else is noise on a one-screen intro. Pinned rather than sticky: with the
   plate gone there is no edge to tell you it is still there, so it has to
   simply stay put. */
:root[data-skin="island"] body.home nav {
  position: fixed; top: 0; left: 0; right: 0;
  background: none; border-bottom: none; backdrop-filter: none;
}
:root[data-skin="island"] body.home nav .wordmark,
:root[data-skin="island"] body.home nav a.link,
:root[data-skin="island"] body.home nav .btn,
:root[data-skin="island"] body.home nav #themeToggle,
:root[data-skin="island"] body.home nav .isl-select,
:root[data-skin="island"] body.home nav .lang-sel { display: none; }
:root[data-skin="island"] body.home .nav-inner { justify-content: flex-end; }

/* first screen is the title and nothing else */
:root[data-skin="island"] .hero {
  position: relative;
  /* the pinned nav is out of flow now, so the hero owns the whole first screen */
  min-height: 100vh;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 24px 24px 96px; margin: 0 auto;
}
/* the demo link drops out of the button row and sits at the foot of the
   screen as a bare white label, the way the upstream site does it */
:root[data-skin="island"] .hero .btn.ghost {
  position: absolute; left: 50%; bottom: 26px; transform: translateX(-50%);
  background: none; border: none; box-shadow: none; padding: 6px 10px;
  color: #fffdf5; font-weight: 700; white-space: nowrap;
  text-shadow: 0 2px 4px rgb(30 60 45 / 0.55);
}
/* label and arrow breathe together, so keep the pulse on the link itself and
   let the arrow ride along rather than animating on its own */
:root[data-skin="island"] .hero .btn.ghost {
  animation: isl-down 1.6s ease-in-out infinite;
}
:root[data-skin="island"] .hero .btn.ghost:hover { animation-play-state: paused; }
@keyframes isl-down {
  0%, 100% { opacity: 0.45; transform: translateX(-50%) translateY(0); }
  50%      { opacity: 1;    transform: translateX(-50%) translateY(5px); }
}
/* Only the headline stays white; everything else on the grass takes the same
   muted tone the footer already uses, so the page reads as one palette. */
:root[data-skin="island"] body.home h2,
:root[data-skin="island"] body.home .sub,
:root[data-skin="island"] body.home .sec-desc {
  color: var(--muted-foreground);
  text-shadow: none;
}
:root[data-skin="island"] body.home .sec-label {
  color: #fffdf5; text-shadow: 0 1px 3px rgb(30 60 45 / 0.45);
}
:root[data-skin="island"] body.home h1 {
  color: #fffdf5;
  text-shadow: 0 2px 4px rgb(30 60 45 / 0.55);
  font-size: clamp(42px, 7.5vw, 68px);
  /* Rounded Chinese face where the OS has one — 幼圆 on Windows, 圆体 on Apple.
     Neither is guaranteed (圆体 is an optional download on macOS), so some
     visitors fall through to Noto Sans SC and get a non-rounded headline. */
  font-family: "YouYuan", "幼圆", "Yuanti SC", "圆体-简", "YuanTi TC",
               Nunito, "Noto Sans SC", -apple-system, "PingFang SC", sans-serif;
}
:root[data-skin="island"] body.home .hero .sub {
  font-size: clamp(17px, 2.2vw, 22px); font-weight: 700;
}
/* .pill is inline-block, so in the centred closing section it sat on the same
   line as the button next to it — it has to be its own block to stack above */
:root[data-skin="island"] body.home .pill {
  display: block; width: fit-content; margin: 0 auto 18px;
  background: rgb(255 255 255 / 0.82); border: 2px solid rgb(255 255 255 / 0.9);
  color: #6a5238; font-family: inherit; font-weight: 700;
}
:root[data-skin="island"] body.home h1 .accent { color: #ffe98a; }

/* the headline CTA is the one thing the eye should land on */
:root[data-skin="island"] .hero .btn.primary {
  background: var(--card); color: #244a37;
  font-size: 17px; padding: 14px 34px;
}
:root[data-skin="island"] .cardlet,
:root[data-skin="island"] .demo-card { border-width: 2px; }

/* with the outer panel gone, sections are told apart by a carved divider */
:root[data-skin="island"] body.home section.wrap + section.wrap::before,
:root[data-skin="island"] body.home section.wrap ~ .fin::before {
  content: ""; display: block; height: 26px; margin: 0 auto 34px;
  max-width: 460px;
  background: url('/assets/island/divider.svg') center / contain no-repeat;
  opacity: 0.85;
}

/* icon over title over copy, all centred; the step numbers are gone from the
   markup so the card is a plain stack again */
:root[data-skin="island"] .cardlet {
  display: flex; flex-direction: column; align-items: center; text-align: center;
}
:root[data-skin="island"] .cardlet h3 { margin: 4px 0 0; }
:root[data-skin="island"] .cardlet p  { margin-top: 8px; }

/* step icons — markup is always present, only this skin gives it a picture */
.isl-ico { display: none; }
:root[data-skin="island"] .isl-ico {
  display: block; width: 44px; height: 44px; margin: 12px 0 4px;
  background: center / contain no-repeat;
}
:root[data-skin="island"] .isl-ico[data-ico="camera"] { background-image: url('/assets/island/icons/icon-camera.svg'); }
:root[data-skin="island"] .isl-ico[data-ico="design"] { background-image: url('/assets/island/icons/icon-design.svg'); }
:root[data-skin="island"] .isl-ico[data-ico="map"]    { background-image: url('/assets/island/icons/icon-map.svg'); }

/* the surviving inner boxes go cream, and gain the chunky rim + drop shadow
   the outer panel used to carry */
:root[data-skin="island"] body.home .demo-card,
:root[data-skin="island"] body.home .cardlet {
  background: var(--card);
  border: 3px solid var(--island-panel-edge);
  border-radius: 22px;
  box-shadow: 0 4px 16px rgb(61 52 40 / 0.18);
}
/* the closing pitch is copy on grass — no box at all */
:root[data-skin="island"] body.home .fin { background: none; border: none; }

/* on narrow screens the panel margin would waste most of the width */
@media (max-width: 560px) {
  :root[data-skin="island"] .page {
    margin: 6px auto 24px; padding: 18px 14px 22px; border-radius: 18px;
  }
  :root[data-skin="island"] .hero { padding: 20px 14px; }
}
