/* ==========================================================================
   tilmannb.de — a link page.
   Papercraft in reality: one folded-paper object, everything else plain and
   real. The paper model is generated by gen_papercraft.py.
   ========================================================================== */

@import url('https://fonts.googleapis.com/css2?family=Work+Sans:wght@400;500;600;700&display=swap');

:root {
  --bg: #fbfaf8;
  --surface: #ffffff;
  --line: #eae8e4;
  --line-strong: #dcd9d3;

  --ink: #26262a;
  --ink-soft: #6e6e75;
  --ink-faint: #9b9ba2;

  --accent: #e8722a;
  --accent-deep: #c15a1b;

  --font: 'Work Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }

.wrap {
  max-width: 700px;
  margin: 0 auto;
  padding: 0 24px;
}

/* ==========================================================================
   Hero
   ========================================================================== */
.hero {
  display: flex;
  align-items: center;
  gap: 20px;
  padding-top: 64px;
  padding-bottom: 28px;
}

.portrait {
  flex: 0 0 96px;
  width: 96px;
  aspect-ratio: 1;
  border-radius: 14px;
  overflow: hidden;
  background: var(--line);
}
.portrait.is-empty {
  background: linear-gradient(150deg, var(--line), var(--line-strong));
}
.portrait img { width: 100%; height: 100%; object-fit: cover; }

.identity { flex: 1 1 260px; min-width: 0; }

h1 {
  font-size: clamp(1.7rem, 5vw, 2.15rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  margin: 0 0 6px;
}

/* ==========================================================================
   The café. It is always attempted — it is the page — and it takes the whole
   window, with everything else floating over it. While it loads, a still of
   the opening view stands in for it. If it cannot start at all, what is left
   is the plain list of links.

   Everything here applies from the first paint whenever the café is being
   attempted — the head script sets data-city3d — and not only once it has
   booted: the still is on screen from the start, so the card has to sit where
   it will stay rather than jumping there when the café arrives.
   ========================================================================== */
.city3d {
  position: fixed;
  inset: 0;
  z-index: 0;
}
.city3d canvas { display: block; width: 100%; height: 100%; }

html[data-city3d="1"] body { overflow: hidden; }
html[data-city3d="1"] .links,
html[data-city3d="1"] .shot { display: none; }

/* The still: the opening view, rendered by tools/render_poster.py with the
   same camera as the live café. It is wide on purpose (2560x1080) and covers
   the window centred, which crops exactly as the camera does on any screen
   narrower than that — the camera keeps its vertical field and loses width,
   and so does `cover` — so the live café lands on top of it in register. */
.city-poster { display: none; }
html[data-city3d="1"] .city-poster {
  display: block;
  position: fixed;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: opacity 0.45s ease;
}
html[data-city3d="1"] .city-poster.is-gone { opacity: 0; }

/* A thin line along the top while the live café loads behind the still: the
   picture is there, but it does not move yet, and this says why. */
.city-loader { display: none; }
html.city-loading .city-loader {
  display: block;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  z-index: 3;
}
.city-loader-track { height: 100%; }
.city-loader-track span {
  display: block;
  width: 0;
  height: 100%;
  background: var(--accent);
  transition: width 0.25s ease;
}

/* Under the name when the café could not be put together and the still is all
   there is. */
.city-still-note {
  margin: 6px 0 0;
  font-size: 0.76rem;
  line-height: 1.4;
  color: var(--ink-faint);
}

/* The card fades in once it has a name and its topics, rather than appearing
   empty and filling up. */
html[data-city3d="1"] .hero { transition: opacity 0.3s ease; }
html[data-city3d="1"] body:not(.card-ready) .hero { opacity: 0; }

html[data-city3d="1"] .hero {
  position: fixed;
  top: 18px;
  left: 18px;
  z-index: 2;
  margin: 0;
  padding: 16px 20px;
  width: auto;
  max-width: min(420px, calc(100vw - 36px));
  background: rgba(255, 255, 255, 0.88);
  backdrop-filter: blur(10px);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: 0 6px 24px rgba(38, 38, 42, 0.12);
}

html[data-city3d="1"] footer {
  position: fixed;
  right: 18px;
  bottom: 16px;
  z-index: 2;
  padding: 0;
}
html[data-city3d="1"] footer .wrap {
  max-width: none;
  padding: 8px 14px;
  gap: 16px;
  background: rgba(255, 255, 255, 0.88);
  backdrop-filter: blur(10px);
  border: 1px solid var(--line);
  border-radius: 999px;
}

/* What the people talk about. Three controls on the hero card, folded away
   behind a summary that shows what they currently say. */
.city-talk {
  margin: 14px 0 0;
  padding-top: 13px;
  border-top: 1px solid var(--line);
}

/* The header is a control, so it looks like one: a bar you can obviously
   press, with the word for what pressing it does at the right-hand end. An
   unlabelled triangle was too easy to miss. */
.city-talk-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  cursor: pointer;
  list-style: none;                 /* the default marker, everywhere */
  padding: 7px 11px;
  border: 1px solid var(--line);
  border-radius: 9px;
  background: var(--bg);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  transition: border-color 0.15s ease, background 0.15s ease;
}
.city-talk-summary::-webkit-details-marker { display: none; }
.city-talk-summary:hover {
  border-color: var(--line-strong);
  background: #fff;
}
.city-talk-summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.city-talk-what {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.city-talk-toggle {
  display: flex;
  align-items: center;
  gap: 5px;
  flex: 0 0 auto;
  color: var(--accent-deep);
  text-transform: uppercase;
  letter-spacing: 0.09em;
}
.city-talk-toggle::after {
  content: '';
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid currentColor;
  transition: transform 0.15s ease;
}
.city-talk[open] .city-talk-toggle::after { transform: rotate(180deg); }

.city-talk-form {
  display: flex;
  flex-direction: column;
  gap: 11px;
  margin-top: 13px;
}

.city-talk-row {
  display: block;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.city-talk-row > span {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 5px;
}
.city-talk-row output {
  font-variant-numeric: tabular-nums;
  color: var(--accent-deep);
}
/* Something to click when the topic field is empty and nothing comes to mind. */
.city-topics {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: -3px;
}

.city-topic {
  font: inherit;
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: none;
  cursor: pointer;
  padding: 3px 9px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: #fff;
  color: var(--ink-soft);
  transition: border-color 0.12s ease, color 0.12s ease, background 0.12s ease;
}
.city-topic:hover {
  border-color: var(--accent);
  color: var(--accent-deep);
}
.city-topic.is-on {
  border-color: var(--accent);
  background: var(--accent);
  color: #fff;
}

/* Empty most of the time — it only speaks up while something is loading or
   when nothing is coming — so it takes no room then. */
.city-talk-note:empty { display: none; }

.city-talk-note:empty { display: none; }

/* The two ends of the scale belong at its two ends, under the track, not in a
   line of prose underneath it. */

.city-talk input[type="text"] {
  width: 100%;
  font: inherit;
  font-size: 0.9rem;
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  color: var(--ink);
  padding: 7px 10px;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: #fff;
}
.city-talk input[type="text"]:focus {
  outline: none;
  border-color: var(--accent);
}

.city-talk-note {
  margin: 0;
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  line-height: 1.35;
  color: var(--ink-faint);
}

.city-labels {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
}

/* What the people at a table say to each other.
   No transform here: CSS2DRenderer writes the element's transform itself, so
   any set in CSS is overwritten. Where the bubble sits relative to the head is
   CSS2DObject.center's business, in cafe3d.js. */
.city-bubble {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  /* A long line used to make one very wide bubble, because nothing wrapped.
     Two lines are better than a banner: it breaks at about thirty characters
     and never gets wider than three quarters of the window, whichever comes
     first. `balance` keeps the two lines roughly even instead of leaving one
     word stranded underneath. */
  white-space: normal;
  max-width: min(30ch, 75vw);
  text-align: center;
  text-wrap: balance;
  line-height: 1.3;
  padding: 4px 9px;
  border-radius: 11px;
  background: #fff;
  border: 1px solid var(--line);
  box-shadow: 0 2px 6px rgba(38, 38, 42, 0.16);
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--ink);
  transition: opacity 0.12s ease;
}
.city-bubble.is-on {
  opacity: 1;
}
/* names of Tilmann's things become links where they come up in a line */
.city-bubble a {
  pointer-events: auto;
  color: var(--accent-deep);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.city-bubble a:hover { color: var(--accent); }
/* the tail */
.city-bubble.is-on::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: -4px;
  width: 7px;
  height: 7px;
  margin-left: -3.5px;
  background: #fff;
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  transform: rotate(45deg);
}

/* the whole building lifts as one, sign included */
.bld {
  cursor: pointer;
  transition: transform 0.16s ease;
}
.bld:hover,
.bld:focus-visible { transform: translateY(-7px); }
.bld:focus { outline: none; }
.bld:focus-visible rect { stroke: var(--accent); stroke-width: 2; }

/* ==========================================================================
   Photographs — a paper model in a real place (see IMAGE_PROMPTS.md).
   The whole figure removes itself if the file isn't there yet.
   ========================================================================== */
.shot {
  margin: 0 0 32px;
  border-radius: 14px;
  overflow: hidden;
  background: var(--line);
  box-shadow: 0 1px 2px rgba(38, 38, 42, 0.05), 0 10px 26px rgba(38, 38, 42, 0.07);
}
.shot img { width: 100%; height: auto; }

/* ==========================================================================
   Links
   ========================================================================== */
/* What is left of the page when the city cannot start: hidden while it loads,
   hidden once it has, and on screen otherwise. */
.links { display: block; padding-bottom: 24px; }

.group {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 26px 0 10px;
}
.group:first-child { margin-top: 0; }

.row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 13px 16px;
  margin-bottom: 8px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 11px;
  transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}

.row:hover {
  transform: translateY(-2px);
  border-color: var(--line-strong);
  box-shadow: 0 6px 16px rgba(38, 38, 42, 0.09);
}

.ico {
  flex: 0 0 auto;
  display: flex;
  color: var(--accent);
}
.ico svg { width: 18px; height: 18px; }

.label { font-weight: 600; font-size: 0.96rem; }

.meta {
  margin-left: auto;
  padding-right: 4px;
  font-size: 0.85rem;
  color: var(--ink-faint);
  text-align: right;
}

.arrow { flex: 0 0 auto; display: flex; color: var(--line-strong); }
.arrow svg { width: 13px; height: 13px; transition: transform 0.15s ease; }
.row:hover .arrow { color: var(--accent); }
.row:hover .arrow svg { transform: translate(2px, -2px); }

/* ==========================================================================
   Footer
   ========================================================================== */
footer {
  padding: 24px 0 52px;
  font-size: 0.82rem;
  color: var(--ink-faint);
}
footer .wrap { display: flex; justify-content: space-between; gap: 12px; }
footer a { color: var(--ink-soft); font-weight: 600; }
footer a:hover { color: var(--accent-deep); }

/* ==========================================================================
   Responsive
   ========================================================================== */
@media (max-width: 620px) {
  .hero {
    flex-wrap: wrap;
    padding-top: 44px;
    gap: 16px;
  }
  .portrait { flex-basis: 76px; width: 76px; }

  .row { flex-wrap: wrap; row-gap: 2px; }
  .label { flex: 1; }
  .arrow { order: 2; }
  .meta { order: 3; margin-left: 0; flex-basis: 100%; text-align: left; }
}
