/* ===========================================================================
   dobby.css — the house-elf guide. Purely additive.

   Nothing in this file may reach outside #dobby-layer. It is loaded onto both
   index.html (1888) and present.html (2026), and those two pages disagree
   about almost everything — the box model most of all. index.html inherits
   style.css, which is CONTENT-BOX; present.css is border-box. So the box model
   is declared here, scoped to this subtree only, exactly the way #cs-warp does
   it. Nothing outside the layer is selected, no element outside it is touched.

   The whole creature is <div>s. No images, no SVG, no font: it has to render
   identically under both stylesheets and under a CSP with img-src 'self' data:
   and no third party at all.

   ---------------------------------------------------------------------------
   HE IS DRAWN IN THREE-QUARTER VIEW, FACING RIGHT.

   The first version was symmetrical and face-on, and it could not work: a nose
   seen head-on is a bump with no length, so every attempt to give him the long
   canonical nose was invisible no matter what it measured. The same is true of
   the hats and the socks — a stack of hats read straight down has no stack in
   it. Turned three-quarters, all three read for free.

   The convention throughout: he faces RIGHT, so
     - the NEAR ear is on the left, large, beside the skull
     - the FAR ear is on the right, small, BEHIND the skull
     - the nose leaves the right edge of the face in profile
     - both eyes sit right of centre, the far one narrower
   .d-body carries a scaleX(-1) when he walks the other way, which mirrors the
   whole arrangement correctly and is why none of it is duplicated.
   ========================================================================= */

#dobby-layer,
#dobby-layer *,
#dobby-layer *::before,
#dobby-layer *::after { box-sizing: border-box; }

#dobby-layer {
  position: fixed;
  inset: 0;
  /* Above everything, the front door included — #cs-warp sits at 9999 and is
     what a visitor sees first, so anything lower means he is simply not there
     for the whole opening. The layer is pointer-events:none, so being on top
     of the door costs the door nothing. */
  z-index: 10000;
  pointer-events: none;      /* the layer itself must never eat a click … */
  overflow: hidden;          /* … and must never widen the document */
  contain: layout style;

  /* Warmer and several shades deeper than the first pass, which came out so
     fair he read as porcelain. */
  --d-skin:      #c9a47e;
  --d-skin-hi:   #e0c39c;
  --d-skin-lo:   #9b7550;
  --d-blush:     #d4756e;
  --d-ear-in:    #d99b90;
  --d-cloth:     #efe7d6;
  --d-cloth-lo:  #cfc3ab;
  --d-eye:       #fbf7ee;
  --d-iris:      #3aa76b;
  --d-ink:       #2a2118;
  --d-bubble:    #fbf5e6;
  --d-bubble-ink:#2a2118;
  --d-bubble-ed: #c9b48a;
  --d-accent:    #b98b2e;

  /* the hat pile, bottom of the stack upward */
  --d-hat1: #35707a;
  --d-hat2: #9d2b3f;
  --d-hat3: #2c3f73;
  --d-hat4: #4a4a52;
  --d-sock1: #37697f;
  --d-sock2: #c9bfae;

  font-family: 'IM Fell English', Georgia, 'Times New Roman', serif;
}

/* The 2026 deck is a different world and gets a cooler, glassier bubble.
   The elf itself is the same creature — that is the point of him. */
#dobby-layer[data-era='present'] {
  --d-bubble:    rgba(18, 21, 38, 0.94);
  --d-bubble-ink:#e8ecfb;
  --d-bubble-ed: rgba(139, 124, 246, 0.55);
  --d-accent:    #8b7cf6;
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
:root[data-theme='light'] #dobby-layer[data-era='present'] {
  --d-bubble:    #fffdf7;
  --d-bubble-ink:#221f2e;
  --d-bubble-ed: rgba(90, 74, 200, 0.4);
}

#dobby-layer[hidden] { display: none !important; }

/* ---------------------------------------------------------------- the elf */

.d-elf {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--d-w, 84px);
  height: var(--d-h, 112px);
  /* x/y are the ONLY thing JS writes each frame — one composited transform */
  transform: translate3d(var(--x, 40px), var(--y, 0px), 0);
  pointer-events: none;      /* only .d-body and the bubble take the pointer */
  will-change: transform;
  touch-action: none;        /* he is draggable; the page must not pan instead */
}

/* Everything that flips with his heading lives in here, so the speech bubble
   above him never ends up mirrored. */
.d-body {
  position: absolute;
  inset: 0;
  pointer-events: none;      /* the drawing is not the target — .d-hit is */
  cursor: pointer;
  transform-origin: 50% 100%;
  transition: transform .22s ease;
}
.d-elf[data-face='left'] .d-body { transform: scaleX(-1); }
.d-hit:active { cursor: grabbing; }

/* THE POINTER TARGET.

   His drawn box is 84x112 and most of it is air: the ears and the hats reach
   out past the silhouette, and the corners are empty. As a hit rectangle at
   z-index 10000 that box sat over the front door's era cards and ate taps
   meant for them — on a phone, where the cards are nearly full width, it was
   unmissable.

   So the pointer target is this instead: roughly the column his head, torso
   and legs actually occupy, inset from every side, with a rounded shape so
   the remaining corners are not his either. He is still easy to hit — it is
   larger than the 44px recommended minimum at his default size — he simply
   no longer claims ground he is not standing on.

   It carries the role, the label and the focus ring, because it is the thing
   a visitor presses. */
.d-hit {
  position: absolute;
  left: 13%;
  right: 13%;
  top: 9%;
  bottom: 0;
  border-radius: 42% 42% 26% 26% / 34% 34% 18% 18%;
  pointer-events: auto;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: none;
}
.d-hit:focus { outline: none; }
.d-hit:focus-visible {
  outline: 2px solid var(--d-accent);
  outline-offset: 3px;
}

/* a soft contact shadow, so he sits on the page rather than floating over it */
.d-shadow {
  position: absolute;
  left: 46%; bottom: -2px;
  width: 52%; height: 8px;
  margin-left: -26%;
  border-radius: 50%;
  background: radial-gradient(ellipse at 50% 50%, rgba(0,0,0,.34), rgba(0,0,0,0) 70%);
  animation: d-shadow 3.4s ease-in-out infinite;
}
@keyframes d-shadow {
  0%,100% { transform: scale(1);    opacity: .55; }
  50%     { transform: scale(.88);  opacity: .40; }
}

/* --- legs, and the odd socks -------------------------------------------- */

/* In three-quarter view the legs are not side by side: the far one is set back
   and up a little, which is most of what stops him reading as a paper doll. */
.d-legs { position: absolute; left: 50%; bottom: 3px; width: 46%; height: 15%; margin-left: -21%; }
.d-leg {
  position: absolute; bottom: 20%;
  width: 30%; height: 80%;
  background: linear-gradient(180deg, var(--d-skin), var(--d-skin-lo));
  border-radius: 44% 44% 30% 30%;
  transform-origin: 50% 0;
}
.d-leg.l { left: 4%; }
.d-leg.r { right: 2%; bottom: 30%; filter: brightness(.88); }   /* the far leg */

/* Bare feet, too big for him, pointing the way he faces. */
.d-leg::after {
  content: ''; position: absolute; bottom: -30%; left: -30%;
  width: 186%; height: 48%;
  background: linear-gradient(180deg, var(--d-skin), var(--d-skin-lo));
  border-radius: 46% 62% 50% 44% / 66% 66% 34% 34%;
}

/* The socks. Mismatched on purpose — a house-elf with a matching pair has
   been given them as a set, which rather misses the point. They cover the
   ankle and the top of the foot, so the bare toes still show. */
.d-leg::before {
  content: ''; position: absolute; left: -26%; bottom: -18%;
  width: 156%; height: 76%;
  border-radius: 40% 46% 44% 40% / 34% 34% 66% 66%;
  z-index: 1;
}
.d-leg.l::before {
  background:
    repeating-linear-gradient(174deg,
      var(--d-sock1) 0 3px, #dce7ec 3px 5.5px);
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.16);
}
.d-leg.r::before {
  background:
    radial-gradient(circle at 30% 34%, #8e8577 0 1.1px, transparent 1.2px) 0 0/5px 5px,
    radial-gradient(circle at 74% 72%, #8e8577 0 1.1px, transparent 1.2px) 0 0/5px 5px,
    var(--d-sock2);
  box-shadow: inset 0 0 0 1px rgba(0,0,0,.16);
  filter: brightness(.9);
}

/* --- the pillowcase ------------------------------------------------------ */

.d-torso {
  position: absolute;
  left: 47%; bottom: 11%;
  width: 56%; height: 32%;
  margin-left: -28%;
  background: linear-gradient(120deg, #fff 2%, var(--d-cloth) 34%, var(--d-cloth-lo) 88%, #b5a992 100%);
  border-radius: 46% 44% 40% 40% / 54% 56% 44% 44%;
  box-shadow: inset 0 -6px 10px -6px rgba(0,0,0,.35);
}
/* a soft collar, so the smock reads as something he is wearing */
.d-torso::before {
  content: ''; position: absolute; left: 46%; top: -6%;
  width: 44%; height: 30%;
  margin-left: -22%;
  border: 1.4px solid var(--d-cloth-lo);
  border-top: 0;
  border-radius: 0 0 60% 60% / 0 0 100% 100%;
  opacity: .8;
}
/* The knot over the chest — what turns a smock into a pillowcase somebody
   tied on themselves. Off to the near side, where the turn shows it. */
.d-knot {
  position: absolute; left: 4%; top: 14%;
  width: 20%; height: 20%;
  background: radial-gradient(circle at 34% 30%, #fff, var(--d-cloth) 52%, var(--d-cloth-lo));
  border-radius: 50% 50% 46% 46%;
  box-shadow: inset 0 -1px 2px rgba(0,0,0,.18);
  z-index: 2;
}
.d-knot::before, .d-knot::after {
  content: ''; position: absolute; top: 46%;
  width: 62%; height: 42%;
  background: var(--d-cloth-lo);
  border-radius: 60% 20% 40% 70%;
  transform-origin: 0 0;
}
.d-knot::before { left: -34%; transform: rotate(28deg); }
.d-knot::after  { left: 58%;  transform: rotate(-16deg) scaleX(.8); }

/* --- arms ---------------------------------------------------------------- */

.d-arm {
  position: absolute; bottom: 9%;
  width: 14%; height: 27%;
  background: linear-gradient(180deg, var(--d-skin), var(--d-skin-lo));
  border-radius: 50% 50% 46% 46% / 24% 24% 62% 62%;
  transform-origin: 50% 6%;
}
.d-arm::after {                    /* the hand */
  content: ''; position: absolute; left: -20%; bottom: -8%;
  width: 140%; height: 40%;
  background: var(--d-skin-lo);
  border-radius: 50%;
}
/* near arm in front of the smock, far arm behind and dimmer */
.d-arm.l { left: 17%;  transform: rotate(13deg);  z-index: 3; }
.d-arm.r { right: 21%; transform: rotate(-10deg); z-index: 0; filter: brightness(.84); }

/* --- head ---------------------------------------------------------------- */

.d-head {
  position: absolute;
  left: 48%; top: 22%;
  width: 64%; height: 44%;
  margin-left: -32%;
  transform-origin: 50% 100%;
  animation: d-breathe 3.4s ease-in-out infinite;
  z-index: 4;
}
/* squash and stretch, not just a bob — it is what makes him read as soft */
@keyframes d-breathe {
  0%,100% { transform: translateY(0)      rotate(0deg)    scale(1, 1); }
  50%     { transform: translateY(-1.5px) rotate(-1.2deg) scale(1.02, .985); }
}

.d-skull {
  position: absolute;
  /* The skull's BOX is wider than the head, extending to the right. The first
     polygon put the nose tip at 103% of a box the cranium already filled, so
     the nose protruded about one pixel and was invisible. The nose needs room
     of its own to occupy: here the cranium ends around 73% of this box and the
     tip reaches 99%, which is real distance. */
  inset: 0 -24% 0 0;
  background:
    radial-gradient(ellipse at 24% 24%, var(--d-skin-hi), var(--d-skin) 46%, var(--d-skin-lo) 100%);
  /* ONE SILHOUETTE, NOSE INCLUDED.

     Three attempts drew the nose as its own element sat on the face, and all
     three failed the same way: skin on skin has no edge, so a bump inside the
     head's outline reads as a cheek, and a bump pushed outside it reads as a
     separate object floating beside the ear. Neither is a nose.

     A nose is not a thing on a face. In profile it is a feature OF THE
     OUTLINE — the head's own edge goes out and comes back. So the whole head
     is one clipped shape whose right-hand edge does exactly that, and there is
     no seam to give away, because there is no join.

     Read clockwise from the top of the skull: forehead, brow, the dip at the
     bridge, out to the tip, back under the nostril, lip, chin, jaw, and up the
     back of the head. */
  clip-path: polygon(
    16% 5%,  27% 0%,  45% 0%,  60% 6%,  68% 17%, 71% 33%,   /* crown, forehead, brow */
    64% 55%,                                                 /* the dip at the bridge,
                                                                BELOW the eyeline — a
                                                                nose starting level with
                                                                the eyes reads as a beak */
    78% 61%, 88% 66%, 93% 71%, 90% 75%, 82% 77%,             /* ridge out to a rounded
                                                                tip and back; four points
                                                                rather than one, or the
                                                                tip comes to a thorn */
    72% 79%,                                                 /* under the nostril */
    68% 84%,                                                 /* the lip */
    60% 90%, 50% 96%, 38% 99%, 26% 95%, 16% 86%,             /* chin and jaw */
    7% 70%,  3% 48%,  5% 25%,  10% 12%
  );
  z-index: 2;
}

/* There is no .d-nose element, and that is the point.

   Four versions of this file had one — a bump, a wedge, a lump with a bulb,
   and finally a soft "nostril shading" oval. Every one of them read as a tube
   stuck to the side of his head, because a nose drawn as a separate object on
   a same-coloured face is exactly that: a separate object. The last one was
   the worst, and it was the one I could not see until every element was
   colour-coded and it turned out the tube WAS the shading.

   The nose is in the skull's clip-path above, as part of the head's outline.
   Nothing else is needed and anything else added here will look wrong again.
   Adjust the polygon, not this space. */

/* three wispy hairs, escaping from under the hats at the back */
.d-tuft { position: absolute; left: 16%; top: -6%; width: 16%; height: 16%; z-index: 1; }
.d-tuft::before, .d-tuft::after {
  content: ''; position: absolute; bottom: 0;
  width: 2px; height: 100%;
  background: var(--d-skin-lo);
  border-radius: 2px;
  transform-origin: 50% 100%;
}
.d-tuft::before { left: 0;  transform: rotate(-30deg); }
.d-tuft::after  { right: 0; transform: rotate(-8deg); height: 78%; }

/* --- ears like a bat's --------------------------------------------------- */
/* Cut with clip-path, not shaped with border-radius: a border-radius cannot
   make a point, and the point IS the silhouette of a bat ear. Without it you
   get a rounded blob at any dimensions, which is how he ended up reading as
   somebody else's round-eared cartoon animal entirely. */
.d-ear {
  position: absolute;
  background: linear-gradient(140deg, var(--d-skin-hi), var(--d-skin) 44%, var(--d-skin-lo));
  filter: drop-shadow(0 1px 1px rgba(120, 78, 42, .28));
}
.d-ear::after {
  content: ''; position: absolute; inset: 0;
  background: var(--d-ear-in);
  opacity: .38;
}

/* NEAR ear — left, large, beside the skull and in front of it */
.d-ear.l {
  left: -42%; top: -18%;
  width: 52%; height: 92%;
  z-index: 3;
  clip-path: polygon(100% 100%, 100% 44%, 72% 14%, 26% 0%, 0% 24%, 8% 64%, 44% 92%);
  transform-origin: 92% 88%;
  animation: d-ear-near 5.6s ease-in-out infinite;
}
.d-ear.l::after {
  clip-path: polygon(86% 88%, 88% 50%, 66% 26%, 34% 14%, 16% 30%, 22% 60%, 48% 82%);
}

/* FAR ear — right, smaller, BEHIND the skull and turned away, so only its
   upper half clears the head. Dimmer, because it is on the shaded side. */
.d-ear.r {
  right: 9%; top: -4%;
  width: 27%; height: 58%;
  z-index: 0;
  filter: brightness(.8);
  clip-path: polygon(0% 100%, 0% 46%, 26% 16%, 70% 0%, 98% 26%, 88% 62%, 52% 92%);
  transform-origin: 8% 88%;
  animation: d-ear-far 6.3s ease-in-out infinite;
}
.d-ear.r::after {
  clip-path: polygon(14% 88%, 12% 52%, 32% 28%, 64% 15%, 82% 32%, 76% 60%, 50% 82%);
}

@keyframes d-ear-near {
  0%,86%,100% { transform: rotate(0deg); }
  90%         { transform: rotate(-13deg); }
  95%         { transform: rotate(5deg); }
}
@keyframes d-ear-far {
  0%,82%,100% { transform: rotate(0deg); }
  87%         { transform: rotate(11deg); }
  92%         { transform: rotate(-4deg); }
}

/* --- eyes ---------------------------------------------------------------- */
/* Both sit right of centre because the head is turned that way. The far one is
   narrower and crowded towards the edge of the face — foreshortening, and the
   cheapest possible cue that this is a head in three dimensions. */
.d-eye {
  position: absolute; top: 31%;
  background: var(--d-eye);
  border-radius: 50%;
  box-shadow: inset 0 2px 4px rgba(0,0,0,.16),
              0 0 0 1.5px rgba(0,0,0,.16),
              0 2px 3px -1px rgba(120, 78, 42, .34);
  overflow: hidden;
  z-index: 4;
}
.d-eye.l { left: 22%;  width: 30%; height: 33%; }                            /* near */
.d-eye.r { right: 22%; width: 18%; height: 26%; filter: brightness(.92); }   /* far.
   Pulled back off the bridge: sat further right it perched on top of the nose,
   which is anatomically where an eye cannot be and reads immediately as wrong
   even when nobody can say why. */

.d-pupil {
  position: absolute;
  left: 54%; top: 52%;
  width: 72%; height: 72%;
  margin: -36% 0 0 -36%;
  border-radius: 50%;
  background: radial-gradient(circle at 32% 28%, #86e8b4 0 14%, var(--d-iris) 36%, var(--d-ink) 96%);
  /* JS nudges these two so he watches the pointer */
  transform: translate(var(--px, 0px), var(--py, 0px));
  transition: transform .16s ease-out;
}
/* Two catchlights, one big and one small. One highlight is a glass bead; two
   is a wet eye, and a wet eye is the entire difference. */
.d-pupil::after {
  content: ''; position: absolute; left: 15%; top: 11%;
  width: 34%; height: 34%; border-radius: 50%;
  background: rgba(255,255,255,.95);
}
.d-pupil::before {
  content: ''; position: absolute; right: 17%; bottom: 19%;
  width: 17%; height: 17%; border-radius: 50%;
  background: rgba(255,255,255,.62);
}

/* One lid per eye, parked above the eye and dropped for blinks and sleep. */
.d-lid {
  position: absolute; left: -5%; top: -102%;
  width: 110%; height: 104%;
  background: linear-gradient(180deg, var(--d-skin), var(--d-skin-lo));
  border-radius: 0 0 40% 40% / 0 0 26% 26%;
}
.d-elf[data-state='idle']    .d-lid,
.d-elf[data-state='walking'] .d-lid,
.d-elf[data-state='shy']     .d-lid,
.d-elf[data-state='talking'] .d-lid { animation: d-blink 6.2s ease-in-out infinite; }
.d-elf[data-state='walking'] .d-lid { animation-duration: 7.4s; }
@keyframes d-blink {
  0%,93%,100% { transform: translateY(0); }
  95.5%       { transform: translateY(96%); }
}

/* the flush across the cheek — only the near one really shows */
.d-blush {
  position: absolute; top: 60%;
  border-radius: 50%;
  background: radial-gradient(ellipse at 50% 50%, var(--d-blush), rgba(212,117,110,0) 72%);
  z-index: 3;
}
.d-blush.l { left: 16%; width: 22%; height: 12%; opacity: .5; }
.d-blush.r { right: 28%; width: 12%; height: 9%; opacity: .28; }

/* --- the nose, finally in profile ---------------------------------------- */
/* This is the whole reason for the turn. Head-on it was a bump with no length
   and no amount of dimension made it read; leaving the silhouette to the right
   it is unmistakably a long nose, and it costs nothing it did not already. */
.d-nose {
  position: absolute;
  right: -36%; top: 47%;
  width: 44%; height: 31%;
  background: linear-gradient(120deg, var(--d-skin-hi) 4%, var(--d-skin) 44%, var(--d-skin-lo) 100%);
  /* narrow at the bridge, dropping to a soft bulb — the old telephone receiver
     the books describe, in outline rather than in detail */
  /* A wedge attached at the left and running out to the right. The tip has to
     finish OUTSIDE the skull's own outline: skin on skin has no edge, so a
     nose that stops within the silhouette is read as a cheek and nothing about
     its shape or shading rescues that. This one breaks the outline. */
  border-radius: 34% 72% 84% 18% / 74% 38% 62% 26%;
  /* and a shadow cast back onto the cheek, which is what tells you it stands
     off the face rather than being painted on it */
  filter: drop-shadow(-2px 3px 1.5px rgba(96, 60, 30, .42));
  z-index: 5;
}
.d-nose::after {            /* the bulb at the tip */
  content: ''; position: absolute; right: -6%; bottom: -22%;
  width: 48%; height: 78%;
  background: radial-gradient(circle at 34% 28%, var(--d-skin-hi), var(--d-skin) 54%, var(--d-skin-lo));
  border-radius: 50%;
}

/* --- mouth --------------------------------------------------------------- */
.d-mouth {
  position: absolute;
  right: 30%; bottom: 8%;
  width: 14%; height: 6%;
  background: none;
  border-bottom: 1.6px solid var(--d-ink);
  border-radius: 0 0 60% 60% / 0 0 100% 100%;
  opacity: .72;
  transform-origin: 50% 0;
  z-index: 5;
}
.d-mouth::after {           /* the open mouth, revealed only while talking */
  content: ''; position: absolute; left: 50%; top: 0;
  width: 82%; height: 200%;
  margin-left: -41%;
  background: var(--d-ink);
  border-radius: 46% 46% 52% 52% / 30% 30% 70% 70%;
  opacity: 0;
  transform-origin: 50% 0;
  transition: opacity .12s ease;
}

/* --- the hats ------------------------------------------------------------ */
/* He keeps every hat anyone has ever given him and wears all of them at once.
   Four, stacked and each turned a little off the last, so the pile leans —
   a plumb-straight stack reads as a chimney. */
.d-hats {
  position: absolute;
  left: 14%; top: -3%;
  width: 56%; height: 30%;
  z-index: 6;
  transform-origin: 50% 100%;
  animation: d-hats 4.6s ease-in-out infinite;
}
@keyframes d-hats {
  0%,100% { transform: rotate(-2deg) translateY(0); }
  50%     { transform: rotate(1.5deg) translateY(-1px); }
}
.d-hat {
  position: absolute;
  left: 50%;
  border-radius: 46% 46% 24% 24% / 62% 62% 38% 38%;
  box-shadow: inset 0 -3px 5px -3px rgba(0,0,0,.5);
}
.d-hat::after {              /* the turned-up brim */
  content: ''; position: absolute; left: -8%; bottom: -14%;
  width: 116%; height: 34%;
  background: inherit;
  filter: brightness(1.18);
  border-radius: 40%;
}
/* knitted ribbing, which is what keeps them from looking like plastic cups */
.d-hat::before {
  content: ''; position: absolute; inset: 6% 10% 12%;
  background: repeating-linear-gradient(90deg,
    rgba(255,255,255,.16) 0 1.5px, transparent 1.5px 4px);
  border-radius: inherit;
}
.d-hat.h1 { bottom: 0;   width: 96%;  height: 40%; margin-left: -48%; background: var(--d-hat1); transform: rotate(-3deg); }
.d-hat.h2 { bottom: 30%; width: 82%;  height: 34%; margin-left: -41%; background: var(--d-hat2); transform: rotate(4deg); }
.d-hat.h3 { bottom: 55%; width: 68%;  height: 32%; margin-left: -34%; background: var(--d-hat3); transform: rotate(-6deg); }
.d-hat.h4 { bottom: 78%; width: 54%;  height: 26%; margin-left: -27%; background: var(--d-hat4); transform: rotate(7deg);
            border-radius: 50% 50% 40% 40% / 70% 70% 30% 30%; }
/* the pompom on top */
.d-hat.h4::before {
  content: ''; position: absolute; left: 50%; top: -52%;
  width: 46%; height: 58%; margin-left: -23%;
  background: radial-gradient(circle at 36% 30%, #fff, #d9d3c6 60%, #b3ab9c);
  border-radius: 50%;
  inset: auto auto auto 50%;
}

/* --- states -------------------------------------------------------------- */

/* asleep: lids down and staying down, head bowed, three z's rising */
.d-elf[data-state='sleeping'] .d-lid  { transform: translateY(96%); }
.d-elf[data-state='sleeping'] .d-head { animation: d-doze 4.6s ease-in-out infinite; }
.d-elf[data-state='sleeping'] .d-mouth { width: 11%; height: 5%; opacity: .5; }
.d-elf[data-state='sleeping'] .d-blush.l { opacity: .66; }
.d-elf[data-state='sleeping'] .d-hats { animation-duration: 11s; }
@keyframes d-doze {
  0%,100% { transform: translateY(2px)   rotate(7deg); }
  50%     { transform: translateY(4.5px) rotate(9deg); }
}

.d-zzz { position: absolute; right: -2%; top: 4%; width: 34%; height: 34%; opacity: 0; z-index: 7; }
.d-elf[data-state='sleeping'] .d-zzz { opacity: 1; }
.d-zzz span {
  position: absolute;
  bottom: 0; left: 0;
  font: 700 13px/1 'IM Fell English', Georgia, serif;
  color: var(--d-accent);
  opacity: 0;
  animation: d-zzz 3.6s ease-out infinite;
}
.d-zzz span:nth-child(2) { left: 34%; font-size: 11px; animation-delay: 1.2s; }
.d-zzz span:nth-child(3) { left: 64%; font-size: 9px;  animation-delay: 2.4s; }
@keyframes d-zzz {
  0%   { opacity: 0; transform: translate(0, 6px) scale(.6) rotate(-8deg); }
  22%  { opacity: .95; }
  100% { opacity: 0; transform: translate(14px, -30px) scale(1.15) rotate(12deg); }
}

/* walking: legs swing, whole body bobs, arms counter-swing */
.d-elf[data-state='walking'] .d-leg.l { animation: d-step 0.62s ease-in-out infinite; }
.d-elf[data-state='walking'] .d-leg.r { animation: d-step 0.62s ease-in-out infinite reverse; }
.d-elf[data-state='walking'] .d-arm.l { animation: d-swing-l 0.62s ease-in-out infinite; }
.d-elf[data-state='walking'] .d-arm.r { animation: d-swing-r 0.62s ease-in-out infinite; }
.d-elf[data-state='walking'] .d-torso { animation: d-bob 0.31s ease-in-out infinite; }
@keyframes d-step    { 0%,100% { transform: rotate(-24deg); } 50% { transform: rotate(24deg); } }
@keyframes d-swing-l { 0%,100% { transform: rotate(24deg); }  50% { transform: rotate(-14deg); } }
@keyframes d-swing-r { 0%,100% { transform: rotate(-24deg); } 50% { transform: rotate(14deg); } }
@keyframes d-bob     { 0%,100% { transform: translateY(0); }  50% { transform: translateY(-1.6px); } }

/* talking: the mouth works and the near hand comes up to make a point */
.d-elf[data-state='talking'] .d-mouth::after { opacity: .8; animation: d-talk 0.34s ease-in-out infinite; }
.d-elf[data-state='talking'] .d-arm.l { animation: d-point 1.6s ease-in-out infinite; }
@keyframes d-talk  { 0%,100% { transform: scaleY(.28); } 50% { transform: scaleY(1); } }
@keyframes d-point { 0%,100% { transform: rotate(12deg); } 50% { transform: rotate(-42deg); } }

/* SHY — 👉👈. Both arms come round to the front and the fingertips meet, which
   is a pose rather than an emoji dropped into the text. He looks away and down
   at the same time, because that is the whole of the gesture. */
.d-elf[data-state='shy'] .d-arm.l { transform: rotate(-64deg) translate(18%, 16%); }
.d-elf[data-state='shy'] .d-arm.r { transform: rotate(58deg)  translate(-22%, 14%); z-index: 3; filter: none; }
.d-elf[data-state='shy'] .d-arm.l::after,
.d-elf[data-state='shy'] .d-arm.r::after { animation: d-poke 1.5s ease-in-out infinite; }
.d-elf[data-state='shy'] .d-head { animation: d-shy-head 3s ease-in-out infinite; }
.d-elf[data-state='shy'] .d-blush.l { opacity: .95; }
.d-elf[data-state='shy'] .d-blush.r { opacity: .6; }
.d-elf[data-state='shy'] .d-mouth { width: 11%; opacity: .55; }
@keyframes d-poke {
  0%,100% { transform: translateX(0); }
  50%     { transform: translateX(12%); }
}
@keyframes d-shy-head {
  0%,100% { transform: rotate(6deg)  translateY(1px); }
  50%     { transform: rotate(9deg)  translateY(2px); }
}

/* held: dangling, arms and legs loose, eyes wide, the hats in disarray */
.d-elf[data-state='held'] .d-body  { transform: rotate(4deg) scale(1.06); }
.d-elf[data-state='held'][data-face='left'] .d-body { transform: scaleX(-1) rotate(-4deg) scale(1.06); }
.d-elf[data-state='held'] .d-arm.l { transform: rotate(30deg); }
.d-elf[data-state='held'] .d-arm.r { transform: rotate(-26deg); }
.d-elf[data-state='held'] .d-leg.l { transform: rotate(-16deg); }
.d-elf[data-state='held'] .d-leg.r { transform: rotate(16deg); }
.d-elf[data-state='held'] .d-shadow { opacity: .18; }
.d-elf[data-state='held'] .d-pupil { transform: scale(1.12); }
.d-elf[data-state='held'] .d-eye   { transform: scale(1.1); }
.d-elf[data-state='held'] .d-mouth { width: 12%; height: 12%;
  background: var(--d-ink); border: 0; border-radius: 50%; opacity: .7; }
.d-elf[data-state='held'] .d-hats  { transform: rotate(-13deg) translate(-2px, 1px); animation: none; }

/* A squash on the way down and a stretch on the way up, on every tap. */
.d-elf.d-bounce .d-body { animation: d-bounce .52s cubic-bezier(.28,.9,.35,1); }
.d-elf.d-bounce[data-face='left'] .d-body { animation-name: d-bounce-l; }
@keyframes d-bounce {
  0%   { transform: translateY(0)     scale(1, 1); }
  22%  { transform: translateY(2px)   scale(1.12, .88); }
  52%  { transform: translateY(-13px) scale(.93, 1.09); }
  78%  { transform: translateY(1px)   scale(1.07, .94); }
  100% { transform: translateY(0)     scale(1, 1); }
}
@keyframes d-bounce-l {
  0%   { transform: scaleX(-1) translateY(0)     scale(1, 1); }
  22%  { transform: scaleX(-1) translateY(2px)   scale(1.12, .88); }
  52%  { transform: scaleX(-1) translateY(-13px) scale(.93, 1.09); }
  78%  { transform: scaleX(-1) translateY(1px)   scale(1.07, .94); }
  100% { transform: scaleX(-1) translateY(0)     scale(1, 1); }
}
.calm #dobby-layer .d-elf.d-bounce .d-body { animation-duration: .8s; }

/* --- the poof, when he is carried off the edge --------------------------- */

.d-elf.d-poof .d-body { animation: d-poof .42s cubic-bezier(.3,.9,.4,1) forwards; }
@keyframes d-poof {
  0%   { transform: scale(1)    rotate(0deg);   opacity: 1; }
  40%  { transform: scale(1.25) rotate(-14deg); opacity: 1; }
  100% { transform: scale(.1)   rotate(28deg);  opacity: 0; }
}
.d-puff {
  position: absolute; left: 50%; top: 46%;
  width: 8px; height: 8px; margin: -4px 0 0 -4px;
  border-radius: 50%;
  background: rgba(255,255,255,.85);
  pointer-events: none;
  animation: d-puff .62s ease-out forwards;
}
@keyframes d-puff {
  0%   { transform: translate(0,0) scale(.4); opacity: .95; }
  100% { transform: translate(var(--dx), var(--dy)) scale(1.9); opacity: 0; }
}
/* --------------------------------------------------------------- speech */

.d-bubble {
  position: absolute;
  left: 50%; bottom: calc(100% + 10px);
  width: max-content;
  max-width: min(300px, 74vw);
  padding: 11px 13px 12px;
  transform: translateX(-50%) translateY(6px) scale(.96);
  transform-origin: 50% 100%;
  background: var(--d-bubble);
  color: var(--d-bubble-ink);
  border: 1px solid var(--d-bubble-ed);
  border-radius: 12px;
  box-shadow: 0 10px 26px -12px rgba(0,0,0,.6);
  font-size: 14px;
  line-height: 1.42;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .22s ease, transform .22s cubic-bezier(.2,1.3,.5,1), visibility .22s;
}
.d-elf.d-speaking .d-bubble {
  opacity: 1; visibility: visible; pointer-events: auto;
  transform: translateX(-50%) translateY(0) scale(1);
}
/* near an edge the bubble hangs off one side instead of overflowing */
.d-elf[data-bubble='right'] .d-bubble { left: 0;    transform: translateX(-12px) translateY(6px) scale(.96); }
.d-elf[data-bubble='left']  .d-bubble { left: auto; right: 0; transform: translateX(12px) translateY(6px) scale(.96); }
.d-elf[data-bubble='right'].d-speaking .d-bubble { transform: translateX(-12px) translateY(0) scale(1); }
.d-elf[data-bubble='left'].d-speaking  .d-bubble { transform: translateX(12px)  translateY(0) scale(1); }

.d-bubble::after {              /* the tail, always over his head */
  content: '';
  position: absolute; bottom: -7px; left: 50%; margin-left: -6px;
  width: 12px; height: 12px;
  background: var(--d-bubble);
  border-right: 1px solid var(--d-bubble-ed);
  border-bottom: 1px solid var(--d-bubble-ed);
  transform: rotate(45deg);
}
.d-elf[data-bubble='right'] .d-bubble::after { left: 26px; }
.d-elf[data-bubble='left']  .d-bubble::after { left: auto; right: 20px; }

.d-say { margin: 0; }
.d-say b { color: var(--d-accent); font-weight: 700; }
.d-say code {
  font: 600 12.5px/1 ui-monospace, 'JetBrains Mono', Menlo, monospace;
  padding: 1px 5px;
  border-radius: 4px;
  background: rgba(128,128,128,.18);
}

.d-acts { display: flex; flex-wrap: wrap; gap: 6px; margin: 9px 0 0; }
.d-act {
  font: inherit;
  font-size: 12.5px;
  line-height: 1;
  padding: 6px 10px;
  color: var(--d-bubble-ink);
  background: transparent;
  border: 1px solid var(--d-bubble-ed);
  border-radius: 999px;
  cursor: pointer;
  transition: background .16s ease, color .16s ease, transform .16s ease;
}
.d-act:hover, .d-act:focus-visible { background: var(--d-accent); color: #fff; transform: translateY(-1px); }
.d-act[data-primary] { border-color: var(--d-accent); color: var(--d-accent); font-weight: 700; }
.d-act[data-primary]:hover, .d-act[data-primary]:focus-visible { color: #fff; }

.d-step {
  display: block;
  margin: 7px 0 0;
  font-size: 11px;
  letter-spacing: .08em;
  text-transform: uppercase;
  opacity: .55;
}

/* ------------------------------------------------------- the pointing ring */

.d-ring {
  position: absolute;
  border: 2px dashed var(--d-accent);
  border-radius: 10px;
  pointer-events: none;
  opacity: 0;
  transition: opacity .3s ease;
  box-shadow: 0 0 0 9999px rgba(0,0,0,.001);   /* forces its own layer, no repaint storms */
  animation: d-ring 2.2s ease-in-out infinite;
}
.d-ring.on { opacity: 1; }
@keyframes d-ring {
  0%,100% { transform: scale(1);    border-color: var(--d-accent); }
  50%     { transform: scale(1.03); border-color: color-mix(in srgb, var(--d-accent) 45%, transparent); }
}

/* ------------------------------------------------------------ the summons */

/* Once he is thrown away he stays away. This is the only way back: a stray
   sock on the floor of the page.

   It is built to read as a solid object rather than as a flat badge, but NOT
   as an assembled box. The first version extruded six planes with preserve-3d
   and at 46 pixels it rendered as a grey slab — the side walls are edge-on for
   most of a turn and the foot lands coplanar with the leg. What actually sells
   volume at this size is shading and one piece of occlusion: a directional
   gradient across the leg, a specular streak, and a dark ellipse for the cuff
   opening, which is the single cue that says "this thing is hollow and I am
   looking into it". The whole sock then sways on one rotateY, which is one
   compositor transform rather than a scene graph. */
.d-sock {
  position: absolute;
  left: 16px; bottom: 14px;
  width: 46px; height: 46px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  pointer-events: auto;
  perspective: 260px;
  opacity: .55;
  transition: opacity .22s ease, transform .22s ease;
}
.d-sock:hover, .d-sock:focus-visible { opacity: 1; transform: translateY(-3px) scale(1.08); }

.d-sock i {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  animation: d-sock-sway 6.5s ease-in-out infinite;
}
/* A sway rather than a full turn: past about 40 degrees the flat foot shows
   its zero thickness and the illusion goes with it. */
@keyframes d-sock-sway {
  0%,100% { transform: rotateY(-19deg) rotateZ(-3deg); }
  50%     { transform: rotateY( 19deg) rotateZ( 3deg); }
}
.d-sock:hover i, .d-sock:focus-visible i { animation-duration: 2.4s; }
.calm #dobby-layer .d-sock i { animation-duration: 20s; }

/* the leg */
.d-sock i::before {
  content: '';
  position: absolute; left: 15px; top: 4px;
  width: 17px; height: 25px;
  background:
    linear-gradient(180deg, var(--d-accent) 0 4px, transparent 4px),
    linear-gradient(100deg, #fff 0%, var(--d-cloth) 26%, var(--d-cloth-lo) 74%, #9c8f78 100%);
  border-radius: 4px 4px 3px 3px;
}
/* the foot, turned out to the left and tucked under the leg */
.d-sock i::after {
  content: '';
  position: absolute; left: 2px; top: 22px;
  width: 30px; height: 15px;
  background: linear-gradient(115deg, var(--d-cloth) 0%, var(--d-cloth-lo) 58%, #9c8f78 100%);
  border-radius: 10px 5px 6px 11px / 9px 5px 7px 10px;
  transform: translateZ(-1px);
}
/* the cuff opening. The one piece of real occlusion, and the reason the whole
   thing reads as hollow rather than as a printed shape. */
.d-sock b {
  position: absolute; left: 14px; top: 1px;
  width: 19px; height: 7px;
  background: radial-gradient(ellipse at 50% 120%, #6d6152, #3b342b 78%);
  border-radius: 50%;
  box-shadow: 0 1px 0 rgba(255,255,255,.5);
}
/* the specular streak down the leg */
.d-sock u {
  position: absolute; left: 18px; top: 9px;
  width: 4px; height: 15px;
  background: linear-gradient(180deg, rgba(255,255,255,.72), rgba(255,255,255,0));
  border-radius: 3px;
  filter: blur(.6px);
}
/* the heel crease, so the foot is not one flat lozenge */
.d-sock s {
  position: absolute; left: 12px; top: 25px;
  width: 9px; height: 9px;
  border-left: 1.4px solid rgba(0,0,0,.18);
  border-radius: 60% 0 0 0;
  text-decoration: none;
}
/* it lies in its own pool of shadow, on the floor of the page */
.d-sock::after {
  content: '';
  position: absolute; left: 3px; bottom: 3px;
  width: 40px; height: 8px;
  border-radius: 50%;
  background: radial-gradient(ellipse, rgba(0,0,0,.45), transparent 72%);
}

/* ------------------------------------------------------------ small screens */

@media (max-width: 720px) {
  .d-elf { --d-w: 64px; --d-h: 86px; }
  .d-sock { transform: scale(.82); transform-origin: 0 100%; }
  .d-bubble { font-size: 13px; max-width: min(250px, 78vw); padding: 9px 11px 10px; }
  .d-zzz span { font-size: 11px; }
}

/* ---------------------------------------------------------- reduced motion */

/* The house rule on this site: reduce-motion TONES DOWN, it does not switch
   off. He still walks — walking is how he gets anywhere — but he ambles, the
   idle loops crawl, and the two effects that cannot be made calm (the sleep
   z's and the pointing ring's pulse) stop rather than being slowed into a
   worse version of themselves. Everything the visitor triggers is untouched. */
.calm #dobby-layer .d-shadow,
.calm #dobby-layer .d-ear.l,
.calm #dobby-layer .d-ear.r,
.calm #dobby-layer .d-head { animation-duration: 11s; }
.calm #dobby-layer .d-elf[data-state='walking'] .d-leg.l,
.calm #dobby-layer .d-elf[data-state='walking'] .d-leg.r,
.calm #dobby-layer .d-elf[data-state='walking'] .d-arm.l,
.calm #dobby-layer .d-elf[data-state='walking'] .d-arm.r { animation-duration: 1.05s; }
.calm #dobby-layer .d-elf[data-state='walking'] .d-torso { animation-duration: .52s; }
.calm #dobby-layer .d-zzz span { animation: none; opacity: .8; }
.calm #dobby-layer .d-ring { animation: none; }
.calm #dobby-layer .d-elf[data-state='talking'] .d-arm.r { animation-duration: 3.4s; }



/* ===========================================================================
   SMALL MAGIC

   He is a house-elf with nothing to do, so between wanders he snaps his
   fingers and something small happens. Three tricks, chosen at random, each a
   handful of divs that animate once and delete themselves.

   What he deliberately does NOT do is move anything on the actual page. It
   would be the best trick in here and it is the one thing this file may not
   do: everything outside #dobby-layer belongs to the two sites, and a mascot
   that nudges real layout is a mascot that can break them. So the objects he
   conjures are his own, and they live and die inside the layer.
   ========================================================================= */

.d-fx {
  position: absolute;
  pointer-events: none;
  will-change: transform, opacity;
}

/* the snap: a bright flash and a ring of sparks off his fingers */
.d-spark {
  /* A white core inside an accent-coloured glow. A spark painted in the accent
     alone was a 3px brown dot on a dark Victorian plate — technically present
     and effectively invisible. The hot centre is what makes it read as a
     spark rather than as dust. */
  width: 9px; height: 9px;
  margin: -4.5px 0 0 -4.5px;
  border-radius: 50%;
  background: radial-gradient(circle, #fff 0 34%, var(--d-accent) 72%, transparent 100%);
  box-shadow: 0 0 14px 4px color-mix(in srgb, var(--d-accent) 75%, transparent);
  animation: d-spark var(--dur, .75s) cubic-bezier(.2,.7,.4,1) forwards;
}
@keyframes d-spark {
  0%   { transform: translate(0,0) scale(.5); opacity: 1; }
  70%  { opacity: .9; }
  100% { transform: translate(var(--dx), var(--dy)) scale(.2); opacity: 0; }
}

/* the pop: one expanding ring, like something has just arrived */
.d-pop {
  width: 12px; height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  border: 3px solid color-mix(in srgb, var(--d-accent) 80%, #fff);
  box-shadow: 0 0 18px color-mix(in srgb, var(--d-accent) 60%, transparent);
  animation: d-pop .7s cubic-bezier(.1,.8,.3,1) forwards;
}
@keyframes d-pop {
  0%   { transform: scale(.3); opacity: .95; }
  100% { transform: scale(9); opacity: 0; }
}

/* the motes: slow embers that drift up and wander sideways as they go */
.d-mote {
  width: 8px; height: 8px;
  margin: -4px 0 0 -4px;
  border-radius: 50%;
  background: radial-gradient(circle, #fff 0 30%, var(--d-accent) 70%, transparent);
  box-shadow: 0 0 10px color-mix(in srgb, var(--d-accent) 55%, transparent);
  animation: d-mote var(--dur, 2.4s) ease-out forwards;
}
@keyframes d-mote {
  0%   { transform: translate(0,0) scale(.4); opacity: 0; }
  18%  { opacity: .95; }
  100% { transform: translate(var(--dx), var(--dy)) scale(1.1); opacity: 0; }
}

/* the conjured thing: a small star that turns once and vanishes */
.d-star {
  width: 26px; height: 26px;
  margin: -13px 0 0 -13px;
  background: var(--d-accent);
  filter: drop-shadow(0 0 10px color-mix(in srgb, var(--d-accent) 80%, transparent));
  clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
  animation: d-star 1.5s cubic-bezier(.2,.8,.3,1) forwards;
}
@keyframes d-star {
  0%   { transform: rotate(0deg)   scale(0);   opacity: 0; }
  25%  { transform: rotate(90deg)  scale(1.15); opacity: 1; }
  70%  { transform: rotate(230deg) scale(1);   opacity: .9; }
  100% { transform: rotate(330deg) scale(.2) translateY(-16px); opacity: 0; }
}

/* the snap itself — the near hand throws all the way up above his head and
   flicks there, not a shrug near the hip. runTrick() in dobby.js fires the
   sparkle burst ~170ms in, once this has actually landed, so the magic reads
   as coming from the raised hand — not from him. */
.d-elf.d-snap .d-arm.l { animation: d-snap .62s ease-in-out; }
@keyframes d-snap {
  0%,100% { transform: rotate(13deg); }
  30%     { transform: rotate(-172deg) translate(-6%, -4%); }
  55%     { transform: rotate(-166deg) translate(-6%, -4%); }
}
.calm #dobby-layer .d-elf.d-snap .d-arm.l { animation-duration: 1s; }

/* ---------------------------------------------------------------------------
   MORE OF IT

   The conjuring was too small and too shy to be noticed at all — it fired
   once every other wander, in five-pixel dots, and a visitor could sit
   through several of them without ever registering that anything had
   happened. It is bigger above and there is more of it here.

   Everything is still transform and opacity on absolutely-positioned nodes
   inside the layer, so none of it can touch the two sites' own layout, and
   `calm` tones it down rather than switching it off.
   ------------------------------------------------------------------------- */

/* a short bloom of light around him, so the trick has a moment of weight
   rather than being only particles */
.d-flash {
  width: 20px; height: 20px;
  margin: -10px 0 0 -10px;
  border-radius: 50%;
  background: radial-gradient(circle, #fff 0 18%, var(--d-accent) 42%, transparent 72%);
  animation: d-flash .78s cubic-bezier(.1,.75,.3,1) forwards;
}
@keyframes d-flash {
  0%   { transform: scale(.2); opacity: .95; }
  45%  { opacity: .55; }
  100% { transform: scale(6.5); opacity: 0; }
}

/* a slow expanding halo, the tail end of a bigger trick */
.d-halo {
  width: 26px; height: 26px;
  margin: -13px 0 0 -13px;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--d-accent) 55%, transparent);
  animation: d-halo 1.5s cubic-bezier(.15,.7,.25,1) forwards;
}
@keyframes d-halo {
  0%   { transform: scale(.3) rotate(0deg); opacity: .9; }
  100% { transform: scale(7) rotate(80deg); opacity: 0; }
}

/* a ribbon of light that arcs away and fades — the showy one */
.d-arc {
  width: 3px; height: 3px;
  margin: -1.5px 0 0 -1.5px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 12px 4px var(--d-accent);
  animation: d-arc var(--dur, 1.1s) cubic-bezier(.25,.6,.35,1) forwards;
}
@keyframes d-arc {
  0%   { transform: translate(0,0) scale(1); opacity: 0; }
  15%  { opacity: 1; }
  100% { transform: translate(var(--dx), var(--dy)) scale(.35); opacity: 0; }
}

.calm #dobby-layer .d-flash { animation-duration: 1.5s; }
.calm #dobby-layer .d-halo  { animation-duration: 2.6s; }
.calm #dobby-layer .d-arc   { animation-duration: 2s; }

/* the hint ring he draws round whatever he is standing next to. Same idea as
   the tour ring, quieter, and dashed so the two never read as the same thing. */
.d-ring.d-ring-near.on { opacity: .5; }
.d-ring.d-ring-near { border-width: 1px; border-radius: 14px; }
