/* ==========================================================================
   Odell — portfolio stylesheet · sections

   Load 5 of 8 — see tokens.css for the full map and for
   why the load order carries meaning.

   10 Work · 11 Capabilities · 12 Approach · 13 About ·
                        14 Contact

   The five content sections, in the order they appear on the page.
   This is the bulk of the stylesheet and the part that changes most
   often.

   One dashed sub-block follows Contact: the rating section. It is not
   a sixth section and is deliberately unnumbered, for the reason
   footer.css gives.
   ========================================================================== */

/* ==========================================================================
   10  WORK
   ========================================================================== */

.work-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1.15rem;
}

@media (min-width: 900px) {
  .work-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  /* Five cards in a two-column grid: the featured card spans both columns and
     the remaining four fill two further rows, leaving no hole. */
  .project-featured {
    grid-column: 1 / -1;
  }
}

.project {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  padding: 1.4rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease), transform 0.2s var(--ease);
}

.project:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}

.project-featured {
  padding: 1.6rem;
}

.project-private {
  background: var(--surface-2);
  border-style: dashed;
}

.project-top {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.project-title {
  font-size: 1.22rem;
}

.project-featured .project-title {
  font-size: 1.45rem;
}

.pill {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--muted);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.22rem 0.6rem;
}

.pill-flag {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent-line);
}

.project-desc {
  color: var(--muted);
  font-size: 0.95rem;
  max-width: 74ch;
}

.facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  margin-top: auto;
  /* pins the fact row to the bottom of the card */
  padding-top: 0.2rem;
}

.fact {
  font-size: 0.8rem;
  color: var(--muted);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 7px;
  padding: 0.3rem 0.6rem;
}

/* The verifiable number in each card. Monospaced so digits line up across
   cards, and coloured so the eye finds it first. */
.fact-strong {
  font-family: var(--mono);
  font-weight: 600;
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent-line);
}

/* ---- Live figures -------------------------------------------------------
   The shimmer is the real number with its colour turned off, not a
   placeholder block standing in for it. So it is exactly the width of the
   digits it replaces, and when the live value lands nothing reflows — the
   text changes colour and that is the whole animation. It also degrades on
   its own: with no JavaScript the class is never applied and the baked-in
   figure simply shows. */
.stat {
  border-radius: 4px;
}

.stat.is-loading {
  color: transparent;
  background-image: linear-gradient(100deg,
      var(--accent-soft) 25%,
      var(--accent-line) 50%,
      var(--accent-soft) 75%);
  background-size: 250% 100%;
  animation: stat-sweep 1.4s linear infinite;
}

@keyframes stat-sweep {
  from {
    background-position: 140% 0;
  }

  to {
    background-position: -140% 0;
  }
}

.stats-note {
  margin-top: 1.6rem;
  font-size: 0.82rem;
  color: var(--faint);
}

.stats-note time {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}

.project-note {
  display: flex;
  gap: 0.55rem;
  font-size: 0.87rem;
  color: var(--muted);
  background: var(--surface-2);
  border-left: 3px solid var(--accent-line);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  padding: 0.7rem 0.85rem;
}

.project-note .icon {
  margin-top: 0.22rem;
  color: var(--accent);
}

.project-note strong {
  color: var(--text);
}

.project-aside {
  font-size: 0.87rem;
  color: var(--faint);
  border-top: 1px dashed var(--border);
  padding-top: 0.75rem;
}

.project-links {
  display: flex;
  flex-wrap: wrap;
  gap: 1.1rem;
  padding-top: 0.2rem;
}

/* ==========================================================================
   11  CAPABILITIES
   ========================================================================== */

.cap-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 3.25rem);
}

@media (min-width: 900px) {
  .cap-grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }
}

.cap-title {
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--faint);
  padding-bottom: 0.7rem;
  margin-bottom: 1.2rem;
  border-bottom: 1px solid var(--border);
}

.cap-title-spaced {
  margin-top: 2.4rem;
}

/* ---- Skill bars ---------------------------------------------------------
   Kept, because they are wanted — but each bar is paired with the work it
   comes from. The bar shows relative strength; the caption under it is the
   part a reader can actually check. */
.skills {
  display: flex;
  flex-direction: column;
  gap: 1.15rem;
}

.skill {
  display: grid;
  gap: 0.38rem;
}

.skill-name {
  font-family: var(--mono);
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text);
}

.skill-track {
  height: 7px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  overflow: hidden;
}

.skill-bar {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--grad-a), var(--grad-b));
  /* The level lives on the element as --level (a percentage), so the correct
     width renders with no JavaScript at all. The script only animates it. */
  width: var(--level, 0%);
  transition: width 0.95s var(--ease);
}

/* Collapse only when scripting is available, then grow on reveal. Without
   JavaScript these rules never apply and the bars simply render at full
   width. */
.js .skill-bar {
  width: 0;
}

.js .skill.is-visible .skill-bar {
  width: var(--level, 0%);
}

.skill-src {
  font-size: 0.79rem;
  color: var(--faint);
}

/* Work that exists but can't be linked — client projects that aren't public.
   Set apart from the repo names so the two read as different kinds of claim.

   Italic only. The obvious extra move, an opacity below 1, dims this text to
   3.2:1 in the light theme and 4.0:1 in the dark one — under AA in both, and
   "private client work" is a claim about the work, not decoration. It is not
   a place to save contrast. */
.skill-src--private {
  font-style: italic;
}

.cap-foot {
  margin-top: 1.5rem;
  font-size: 0.85rem;
  color: var(--faint);
  max-width: 52ch;
}

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.42rem;
}

.tag {
  font-size: 0.83rem;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.34rem 0.66rem;
}

.tag-note {
  color: var(--faint);
  font-size: 0.92em;
}

/* The ellipsis that closes the tools list, and the only item up there that is
   a figure of speech rather than a name. It keeps a tag's box — a tag's type
   size, its radius, its border width — but the box draws nothing. A pill
   around three dots reads as a tag whose name is "...", which is worse than
   either.

   The padding is the one thing a tag has that the dots must not: a tag's
   0.66rem of it either side is what makes a pill read as a pill, and on a box
   that draws nothing it just holds the dots off x64dbg. With the padding gone
   the row's own gap is all that separates them, which is exactly what
   separates any two tags — so the dots sit beside the last tag the way the
   next tag would.

   The dots rest on the floor of the box rather than floating in the middle of
   it. Level with the tags' text they read as one more item; set down on the
   line the row stands on, they read as the list trailing off — which is what
   they are for. */
.tag-more {
  display: flex;
  align-items: flex-end;
  padding: 0;
  color: var(--faint);
  background: none;
  border-color: transparent;
}

/* Specialties sit below the two-column grid, not inside its right column:
   with a 50-tag list above them the left column ran out long before they
   started, leaving a dead gap the width of a column beside the heading.
   Cards fill whatever width is going — two across, four once there is room.
   Only 2 and 4 divide four cards evenly; a three-column middle state would
   strand the fourth card alone on a second row. */
.cap-specs {
  margin-top: 0.4rem;
}

.spec-list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1.5rem;
}

@media (min-width: 560px) {
  .spec-list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1.6rem 2.25rem;
  }
}

@media (min-width: 1024px) {
  .spec-list {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

.spec h4 {
  font-size: 0.97rem;
  margin-bottom: 0.25rem;
}

.spec p {
  font-size: 0.89rem;
  color: var(--muted);
}

/* ==========================================================================
   12  APPROACH
   ========================================================================== */

/* The statement is the thesis, so it leads at full width and the five
   principles sit under it as a card grid. The older split ran a short card
   down one column beside a tall numbered list in the other, which left a
   column of dead space under the statement and made the two halves read as
   unrelated. The content is unchanged — only the arrangement. */
.approach-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(1.8rem, 4vw, 2.6rem);
}

.statement {
  max-width: 74ch;
}

.statement-title {
  font-size: clamp(1.3rem, 2.6vw, 1.6rem);
  margin-bottom: 0.8rem;
  background: linear-gradient(115deg, var(--grad-a), var(--grad-b));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.statement p {
  color: var(--muted);
  font-size: 0.97rem;
}

/* Three across at desktop leaves the last row two-thirds full. A grid reads
   that as a normal ragged last row; a flex row would stretch the two cards to
   fill it and make them wider than the three above. */
.principles {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1rem;
  counter-reset: principle;
}

@media (min-width: 640px) {
  .principles {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 1000px) {
  .principles {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.principle {
  counter-increment: principle;
  padding: 1.4rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

/* The number sits at the top of the card now rather than hanging in a gutter:
   a grid has no gutter to hang it in. */
.principle::before {
  content: counter(principle, decimal-leading-zero);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.15rem;
  height: 2.15rem;
  margin-bottom: 0.85rem;
  font-family: var(--mono);
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--accent);
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: 8px;
}

.principle h3 {
  font-size: 1rem;
  margin-bottom: 0.25rem;
}

.principle p {
  font-size: 0.9rem;
  color: var(--muted);
}

/* ==========================================================================
   13  ABOUT
   ========================================================================== */

/* The bio, and only the bio. The four profile cards that used to sit beneath it
   are in Contact now, which is where someone looking for a way to make contact
   actually goes — the visitor who clicked "Get in Touch" used to land on a
   single email address. */
.about-body {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.about-body p {
  color: var(--muted);
  max-width: 68ch;
}

/* ==========================================================================
   14  CONTACT
   ========================================================================== */

.contact-card {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding: clamp(1.4rem, 4vw, 2.25rem);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}

.contact-mail {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  font-family: var(--mono);
  font-size: clamp(1rem, 2.6vw, 1.32rem);
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
  overflow-wrap: anywhere;
  border-bottom: 2px solid var(--accent-line);
  padding-bottom: 0.15rem;
  transition: color 0.18s var(--ease), border-color 0.18s var(--ease);
}

.contact-mail:hover {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

.contact-mail .icon {
  color: var(--accent);
  font-size: 1.15em;
}

.contact-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

/* Every way of reaching him, in one place. These four cards were in About,
   which meant the visitor who clicked "Get in Touch" — the visitor with the
   question this section exists to answer — found one email address and no
   other option. The section that says how to make contact is the section that
   should hold the ways of making it. */
.contact-social {
  flex-basis: 100%;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.85rem;
  padding-top: 1.15rem;
  border-top: 1px solid var(--border);
}

/* Two per row is the ceiling asked for. Below ~37rem a card stops being wider
   than its own URL, so they stack rather than squeeze. */
@media (max-width: 599px) {
  .contact-social {
    grid-template-columns: minmax(0, 1fr);
  }
}

.contact-label {
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--faint);
  margin-right: 0.35rem;
}

.contact-social .contact-label {
  grid-column: 1 / -1;
  margin-right: 0;
}

/* The card these destinations have always used. It keeps --surface and shows
   its edge with a border instead, which is what the ghost buttons below it do
   inside this same card — and the hover is theirs exactly: --surface-2 and a
   stronger border. The first attempt tinted the fill with --accent-soft on a
   --surface-2 base; measured, that put the subtitle at 4.43:1 in the light
   theme, under AA, where the fill it had in About had been 4.83. */
.profile-card {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  padding: 0.9rem 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  text-decoration: none;
  transition: border-color 0.18s var(--ease), background 0.18s var(--ease);
}

.profile-card:hover {
  border-color: var(--border-strong);
  background: var(--surface-2);
}

.profile-card .icon {
  font-size: 1.35rem;
  color: var(--muted);
}

.profile-card strong {
  display: block;
  font-size: 0.92rem;
}

.profile-card small {
  display: block;
  font-size: 0.79rem;
  color: var(--faint);
}

/* The CVs get a row of their own. They are documents to take away rather than
   another way to send a message, so they do not belong in the row of actions
   above. */
.contact-docs {
  flex-basis: 100%;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  padding-top: 1.15rem;
  border-top: 1px solid var(--border);
}

/* The two CV buttons, held in a box of their own so the row above wraps them
   as one thing. Left as two bare items, the row broke wherever it ran out of
   width — which put one button beside the label and dropped the other to the
   line below, the two halves of one choice landing in different places.

   As a single item the pair either fits beside the label or moves under it
   whole. The pair can still break between its own two buttons, but only once
   it is already on a line of its own and still too wide for it — a width no
   phone on the site reaches. */
.contact-doc-btns {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

/* On a phone the pair does fit side by side, and that is the problem: at this
   width it is the whole line, so two buttons sharing it read as one control
   with a seam down the middle rather than as two documents to choose between.

   width: 100% is what takes the line — it makes the block wrap under the label
   instead of sitting beside it — and the column then stretches both buttons to
   that full width. It is a width rather than a flex-basis because the block is
   a column by then, and in a column the basis is the height: flex-basis: 100%
   asked for a full page of it, left the width to the content, and the pair
   stayed beside the label at 265px instead of under it at 312. */
@media (max-width: 599px) {
  .contact-doc-btns {
    width: 100%;
    flex-direction: column;
  }

  /* The label sits over the pair at this width rather than beside it, and the
     buttons under it are centred by their own text, so a label still hanging
     left would be the one thing off the axis. */
  .contact-docs {
    justify-content: center;
  }
}

/* The copy button swaps icon and label together once the address is on the
   clipboard, so the change is never signalled by colour alone. */
#copyEmail .icon-done,
#copyEmail .copy-done {
  display: none;
}

#copyEmail.is-copied .icon-copy,
#copyEmail.is-copied .copy-label {
  display: none;
}

#copyEmail.is-copied .icon-done {
  display: block;
}

#copyEmail.is-copied .copy-done {
  display: inline;
}

/* ---- Rating -------------------------------------------------------------- */

/* The last section in <main>, and it takes no number: 15 is FOOTER, and
   footer.css records that renumbering is the hazard the dev entry was placed
   to avoid. So this is a sub-block at the end rather than a section of its
   own. */

/* A section whose whole content is a heading, one line of reassurance and five
   stars. Every other band on this page carries a grid of cards and earns the
   full clamp for it; this one took the same padding and then stood in it,
   which is what made the close of the page read as an empty room rather than
   as a section. At the desktop end this is 56px a side against the standard
   96px, which takes about 80px off a band holding roughly 150px of content.

   It is also the one section on the page with no band of its own — no
   .section-alt, so no --band wash and no border. The alternation earns its
   keep where an alternate band meets a plain one; this one met the footer,
   which is banded too, so the pattern bought nothing here and cost something:
   --band is translucent, and drawn over a .backdrop whose orbs never stop
   moving it read as a haze with two hard edges across it rather than as a
   band. Plain, the section reads as the page continuing, which is what it
   is. */
#rate {
  padding-block: clamp(2.25rem, 5vw, 3.5rem);
}

/* A browser that has already rated is not asked twice. Hidden as the whole
   section rather than as the stars alone, because a heading asking to be rated
   with nothing under it is a dead question — the same thing, for the same
   reason, as the print rule in motion.css.

   `html.is-rated` rather than a class on the section: the class is set by the
   inline script in the head, which has only the document element to work with
   before the body exists, and it has to be set there or the section paints and
   then disappears. Two selectors' worth of weight also puts it beyond anything
   in this file that might later give #rate a display of its own. */
html.is-rated #rate {
  display: none;
}

/* The heading-to-body gap, cut for the same reason. The standard margin
   separates a heading from a grid of cards; here the body is one row of
   stars, and at the standard size the gap is wider than the row it
   introduces. */
#rate .section-head {
  margin-bottom: clamp(1.1rem, 2.5vw, 1.5rem);
}

/* The five buttons and the gaps between them: 5 x 40px + 4 x 4px = 216px,
   which fits the narrowest viewport the site serves with room to spare —
   320px less the two 1.5rem gutters leaves 272px. So the row never has to
   wrap, shrink or scroll on a phone. */
.rate-stars {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

/* 40px square: above .icon-btn's 38px, and well above the 24px minimum
   target the dev switch names as the floor this site holds itself to
   everywhere else. The star inside is 28px of it, so most of the button is
   padding — which is the point, and is what makes five of them comfortable
   in a row. */
.rate-star {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--faint);
  cursor: pointer;
  transition: color 0.15s var(--ease), background 0.15s var(--ease);
}

/* The only icon in the sprite that takes a fill, which is why these two rules
   are here and not in base.css with the rest of .icon: every other symbol
   carries fill="none" of its own, and a presentation attribute on the symbol
   beats anything this stylesheet says, so a star so marked could never be
   filled at all.

   Measured against the page background, which is what this section stands on
   now that its band has gone: --faint is 5.28:1 in light and 6.26:1 in dark,
   and --accent is 6.01:1 and 9.62:1 once filled. WCAG 1.4.11 asks 3:1 of a
   graphical object and both clear it. The state is not signalled by colour
   alone either — the shape fills in, which is the half a colour-blind visitor
   reads. */
.rate-star .icon {
  width: 1.75rem;
  height: 1.75rem;
  fill: none;
  transition: fill 0.15s var(--ease);
}

.rate-star.is-on {
  color: var(--accent);
}

.rate-star.is-on .icon {
  fill: currentColor;
}

/* The hover preview has no rule here on purpose. stats.js fills the stars up
   to the one under the pointer itself, because the preview has to stop at the
   pointer — a fact about which star was entered, which no :hover can express
   for the stars before it. A rule here would be fighting it, not helping. */

/* The closing row, which held the answer on the left and the way into dev mode
   on the right. The link lives above the footer again, so what is left is a
   box with a gap above it. Kept as a row rather than folded into .rate-note
   because it is the line the section closes on, and whatever joins that line
   next should not have to move the margin to do it.

   No flex here any more. space-between across a single child says nothing,
   and the wrap existed for two things that no longer share the width. */
.rate-foot {
  margin-top: 0.9rem;
}

/* The answer, held open at one line so nothing below it moves when it
   arrives. All three sentences are in the markup and the classes pick one,
   the way the two languages do, so nothing here writes a word.

   No margin of its own: the gap above it belongs to .rate-foot, which is the
   only thing that row still does. */
.rate-note {
  min-height: 1.35rem;
  font-size: 0.9rem;
  color: var(--muted);
}

.rate-said {
  display: none;
}

/* :not(.is-again) rather than a fourth class, because "recorded" and "replaced
   the last one" are the same outcome with different history — stats.js sets
   is-again on a repeat, and exactly one of the two may show. The failed line
   ignores the distinction: a rating that did not arrive did not replace
   anything, whether or not an earlier one did. */
.rate-note.is-done:not(.is-again) .rate-thanks,
.rate-note.is-done.is-again .rate-again,
.rate-note.is-failed .rate-failed {
  display: inline;
}
