<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thoughtbot="https://thoughtbot.com/feeds/" xmlns:feedpress="https://feed.press/xmlns" xmlns:media="http://search.yahoo.com/mrss/" xmlns:podcast="https://podcastindex.org/namespace/1.0">
  <feedpress:locale>en</feedpress:locale>
  <link rel="hub" href="https://feedpress.superfeedr.com/"/>
  <title>Giant Robots Smashing Into Other Giant Robots</title>
  <subtitle>Written by thoughtbot, your expert partner for design and development.
</subtitle>
  <id>https://robots.thoughtbot.com/</id>
  <link href="https://thoughtbot.com/blog"/>
  <link href="https://feed.thoughtbot.com/" rel="self"/>
  <updated>2026-04-30T00:00:00+00:00</updated>
  <author>
    <name>thoughtbot</name>
  </author>
  <entry>
    <title>Reviewing Dependabot PRs is boring. Let Claude do it for you.</title>
    <link rel="alternate" href="https://feed.thoughtbot.com/link/24077/17328151/reviewing-dependabot-prs-is-boring-let-claude-do-it-for-you"/>
    <author>
      <name>Jose Blanco</name>
    </author>
    <id>https://thoughtbot.com/blog/reviewing-dependabot-prs-is-boring-let-claude-do-it-for-you</id>
    <published>2026-04-30T00:00:00+00:00</published>
    <updated>2026-04-29T15:13:56Z</updated>
    <content type="html"><![CDATA[<p>I’m not going to lie, when I start my day and I see 10 Dependabot PRs open
in the project, I just want to close the laptop and go for a walk. And I
have the feeling that, like me, many other developers feel the same way,
because I keep seeing Dependabot PRs sit open in projects for weeks. Nobody
wants to read the changelog, check the dependencies, look for breaking
changes, and still risk shipping a regression because they missed an
important line buried in the notes.</p>

<p>In the age of AI and automation, we can definitely get some help with this.
This is what my colleague <a href="https://thoughtbot.com/blog/authors/fritz-meissner">Fritz</a>
suggested. We were watching Dependabot PRs pile up and figured the real pain
point was that people were lacking the information they needed to merge with
confidence. So I used Claude’s <code>skill-creator</code> to build a
<a href="https://claude.com/skills">skill</a> that gives me exactly that:
a short summary of the changes, the risk, and a recommendation: can I merge,
or do I need to look more carefully myself?</p>
<h2 id="a-dependabot-pr-review-skill">
  
    A Dependabot PR review skill
  
</h2>

<p>You point the skill at a Dependabot PR or at the whole repo and it gives
you back the one thing the PR description never tells you: <em>should I merge
this, and if not, why not?</em></p>

<p>It works in two modes:</p>

<ul>
<li>
<strong>Single-PR mode</strong> — paste a Dependabot PR URL and you get a full review
for that one PR.</li>
<li>
<strong>Audit mode</strong> — ask it to “review all open dependabot PRs” and it
discovers every open Dependabot PR in the repo with <code>gh</code>, analyzes them
one by one, and produces a single triage report.</li>
</ul>

<p>For each PR, the skill does roughly what a careful human would do, just
faster and without getting bored:</p>

<ol>
<li>
<strong>Reads the PR diff</strong> to figure out the gem name, the old and new
version, and whether the bump is patch, minor, or major.</li>
<li>
<strong>Pulls the changelog</strong> between those two versions from GitHub
releases, <code>CHANGELOG.md</code>, or RubyGems and only keeps the parts that
matter: breaking changes, deprecations, security fixes, notable
behaviour changes. The release-notes-style noise gets stripped out.</li>
<li>
<strong>Greps the codebase</strong> to see where the gem is actually used. A bump
to a gem that lives in three test files is a very different story
from a bump to a gem that runs in your payment flow, and the skill
calls that out.</li>
<li>
<strong>Hands you a verdict</strong> in one of four buckets:

<ul>
<li>
<code>Merge</code> — safe, low risk</li>
<li>
<code>Verify</code> — looks safe but here are the specific things to check first</li>
<li>
<code>Investigate</code> — needs human judgment, here’s why</li>
<li>
<code>Hold</code> — there are breaking changes, you’ll need code work before
merging</li>
</ul>
</li>
</ol>

<p>In audit mode you get all of that as a summary table at the top: PR
number, gem, bump (<code>7.2.4 → 8.0.10</code>), type, age, verdict, and a “why”,
sorted worklist style: <code>Merge</code> first, then <code>Verify</code>, <code>Investigate</code>,
<code>Hold</code>.</p>
<h2 id="what-the-output-actually-looks-like">
  
    What the output actually looks like
  
</h2>

<p>Here’s a trimmed example of what audit mode prints back into the chat
for a repo with a handful of open Dependabot PRs:</p>
<div class="highlight"><pre class="highlight plaintext"><code>Found 5 open Dependabot PRs. Analyzing each now…

| #     | Gem            | Bump            | Type     | Age | Verdict     | Why                                  |
|-------|----------------|-----------------|----------|-----|-------------|--------------------------------------|
| #9170 | rubocop        | 1.65.0 → 1.68.0 | minor    | 12d | Merge       | dev-only, no breaking changes        |
| #9168 | sidekiq        | 7.2.4 → 7.3.1   | minor    | 6d  | Verify      | check Redis 6.2+ in production       |
| #9165 | aws-sdk-s3     | 1.143 → 1.150   | minor    | 21d | Verify      | new default checksum algorithm       |
| #9159 | devise         | 4.9.3 → 4.9.4   | patch    | 3d  | Merge       | patch, safe to merge                 |
| #9142 | rails          | 7.2.4 → 8.0.10  | major    | 30d | Hold        | breaking: deprecated Active Job APIs |
</code></pre></div>
<p>And then for each PR, a per-PR section with the changelog highlights,
the files in your codebase that touch that gem, and the reasoning behind
the verdict so you can scan the table for the easy wins.</p>
<h2 id="posting-the-review-back-to-the-pr">
  
    Posting the review back to the PR
  
</h2>

<p>The chat transcript is not where teams review code, so after the review
is done, the skill asks if you want to post it as a comment on the PR.
Nothing gets posted without an explicit yes.</p>

<p><img src="https://images.thoughtbot.com/xk062s3mpcx88di40bw992cvgs5b_image.png" alt="An example of the dependabot review comment in a PR"></p>

<p>The comment uses a collapsible <code>&lt;details&gt;</code> block, with the verdict and a
one-line reason above the fold so a teammate scrolling the timeline can
triage without expanding, and the full review tucked underneath. There’s
also an invisible marker in the comment, so if you re-run the audit a
week later, it can detect its own previous comments and skip PRs that
already have a review instead of spamming duplicates.</p>
<h2 id="give-it-a-try">
  
    Give it a try
  
</h2>

<p>Curious about the skill? Check it out <a href="https://github.com/thoughtbot/dependabot-review-thoughtbot">here</a>.
If you have any improvements or feedback, please open an issue or pull request. We love feedback!</p>

<aside class="related-articles"><h2>If you enjoyed this post, you might also like:</h2>
<ul>
<li><a href="https://thoughtbot.com/blog/internbot-chronicles-4-ci-test-metrics">Internbot Chronicles #4: CI &amp;amp; Test Metrics</a></li>
<li><a href="https://thoughtbot.com/blog/feature-branch-code-reviews">Feature branch code reviews</a></li>
<li><a href="https://thoughtbot.com/blog/introducing-copycopter">Introducing Copycopter: let your clients do the copy writing</a></li>
</ul></aside>
<img src="https://feed.thoughtbot.com/link/24077/17328151.gif" height="1" width="1"/>]]></content>
    <summary>A Claude skill to do the boring stuff for us.</summary>
    <thoughtbot:auto_social_share>true</thoughtbot:auto_social_share>
  </entry>
  <entry>
    <title>Retro-driven development</title>
    <link rel="alternate" href="https://feed.thoughtbot.com/link/24077/17326678/retro-driven-development"/>
    <author>
      <name>Rob Whittaker</name>
    </author>
    <id>https://thoughtbot.com/blog/retro-driven-development</id>
    <published>2026-04-28T00:00:00+00:00</published>
    <updated>2026-04-27T15:38:37Z</updated>
    <content type="html"><![CDATA[<p>Every session ends with a retro. This week, twenty-four
commits out of about a hundred and forty started with that
retro. Only a handful added anything new. I wasn’t building
the system anymore. It was refactoring itself.</p>

<p>It is Week Four.</p>
<h3 id="tuesday-four-commits-before-lunch">
  
    Tuesday: four commits before lunch
  
</h3>

<p>The 17th. Four refactor-from-retro commits before noon.
Reusing API connections across commands instead of
reconnecting each time. <code>/morning</code> filtering rules. Stale 1:1
prep dropped from the daily log. The system had been
running for three weeks, and friction points had
accumulated. I was working through them in fifteen-minute
bursts between meetings.</p>

<p>By the end of the day, I had added eight more commits. An
actionability check for <code>/context</code>. Top 7 priorities in
the daily log. A self-management outcome for my Fusion
goal. Retro after retro, feeding back into the commands.</p>
<h3 id="wednesday-picking-sides">
  
    Wednesday: picking sides
  
</h3>

<p>Wednesday ran hard. Nine refactor commits between meetings.</p>

<p>I read Sally Lait’s post on semantic calendar emoji and
colours. I copied her system straight into <code>/calendar</code>:</p>

<ul>
<li>🦚 Peacock (default): 1:1s, ad-hoc work</li>
<li>🫐 Blueberry: recurring group meetings</li>
<li>🌿 Sage: pairing, workshops, active work</li>
<li>🍌 Banana: internal socials, external community</li>
<li>✏️ Graphite: transit, food</li>
</ul>

<p>By evening, I’d refactored <code>/evening</code> to use Ruby instead
of Python. It was a small religious war. I picked the side
my team knows. The CLAUDE.md gained a preference note. This
sort of thing accumulates.</p>
<h3 id="thursday-the-anytime-problem">
  
    Thursday: the Anytime problem
  
</h3>

<p>The Anytime list from Things hit 75,000 characters. That’s
around 19,000 tokens. It triggered context compaction
mid-session. I noticed overdue items slipping through the
cracks.</p>

<p>I needed a filter. Not a prompt. A real script. I wrote
<code>bin/filter-anytime</code>.</p>
<div class="highlight"><pre class="highlight ruby"><code><span class="no">KEEP_FIELDS</span> <span class="o">=</span> <span class="sx">%w[Title UUID Tags Area Project Deadline Notes]</span>

<span class="n">items</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">raw</span><span class="o">|</span>
  <span class="n">status</span> <span class="o">=</span> <span class="n">raw</span><span class="p">[</span><span class="sr">/^Status:\s*(.+)/</span><span class="p">,</span> <span class="mi">1</span><span class="p">]</span>
  <span class="k">next</span> <span class="k">if</span> <span class="n">status</span> <span class="o">=~</span> <span class="sr">/completed|canceled/</span>

  <span class="k">if</span> <span class="n">tags</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="s2">"Waiting"</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">deadline_str</span>
    <span class="n">deadline</span> <span class="o">=</span> <span class="no">Date</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">deadline_str</span><span class="p">)</span> <span class="k">rescue</span> <span class="kp">nil</span>
    <span class="k">next</span> <span class="k">if</span> <span class="n">deadline</span> <span class="o">&amp;&amp;</span> <span class="n">deadline</span> <span class="o">&gt;=</span> <span class="n">today</span>
  <span class="k">end</span>

  <span class="nb">puts</span> <span class="n">raw</span>
<span class="k">end</span>
</code></pre></div>
<p>Fifty-five lines of Ruby. It runs before the agent sees the
list. The filter runs on rules. It doesn’t guess. Overdue
items stopped slipping.</p>

<p>Every session, the agent regenerates the filter. Not this
time. I wrote real code. Guessing has limits.</p>
<h3 id="monday-a-stretch-of-quiet-time">
  
    Monday: a stretch of quiet time
  
</h3>

<p>A quiet Monday morning. Six refactor commits in one
sitting. <code>/calendar</code>, <code>/inbox</code>, <code>/weekly</code>, <code>/context</code>. A
stretch of uninterrupted time before the week’s meetings
started.</p>

<p>That is when I realised the system had shifted. I wasn’t
grinding through tasks. I was editing the system that edits
my day. Maintenance, not task-grinding. The point of
building a system is to make it fade into the background.</p>
<h3 id="tuesday-the-cap">
  
    Tuesday: the cap
  
</h3>

<p>By the 24th, I noticed something else. My Anytime list
kept growing. Each session, I added new tasks from
retrospectives, meetings, and the inbox. The filter was
treating the symptom. The disease was that the input
exceeded the throughput.</p>

<p>I added a commitment cap.</p>
<div class="highlight"><pre class="highlight markdown"><code>Commitment cap: No more than 20 active next actions in
Things at any time across all areas (work and personal).
If /morning surfaces items that would exceed the cap,
flag it and ask what to defer before proceeding.
</code></pre></div>
<p><code>/morning</code> now blocks the Top 7 until I’ve deferred enough
items to sit under 20. The check is mechanical. I can’t
talk it into running anyway.</p>
<h3 id="what-i-learned">
  
    What I learned
  
</h3>

<p>The dominant mode this week wasn’t invention. It was
refactoring. Twenty-four commits out of about a hundred and
forty say “from retro” or “from feedback.” The system
improves by use, not by planning.</p>

<p>Retro-driven development. It works because the signal is
cheap and the fix is small. Notice a friction point. Name
it. In the next session, the command that caused the
friction receives a line of new guidance. No meetings. No
sprints. No planning.</p>

<p>The commitment cap came from one of those retros. So did
<code>bin/filter-anytime</code>. So did Sally Lait’s colour
conventions find their way into <code>/calendar</code>. Each started
as an irritation, ended as a line in a command file, and
changed how the next session ran.</p>
<h3 id="try-it">
  
    Try it
  
</h3>

<p>Retros don’t need to be long. End each session with one.
In the next session, fix what rubbed you the wrong way.
The system is yours.</p>

<aside class="related-articles"><h2>If you enjoyed this post, you might also like:</h2>
<ul>
<li><a href="https://thoughtbot.com/blog/theme-based-iterations">Theme-Based Iterations</a></li>
<li><a href="https://thoughtbot.com/blog/retrospective-fashionopoly">Retrospective: Fashionopoly</a></li>
<li><a href="https://thoughtbot.com/blog/this-week-in-open-source-11">This week in open source</a></li>
</ul></aside>
<img src="https://feed.thoughtbot.com/link/24077/17326678.gif" height="1" width="1"/>]]></content>
    <summary>Twenty-four refactor-from-retro commits in a week. How the management system started refactoring itself.</summary>
    <thoughtbot:auto_social_share>true</thoughtbot:auto_social_share>
  </entry>
  <entry>
    <title>Trimming our CSS with sibling-index() and sibling-count()</title>
    <link rel="alternate" href="https://feed.thoughtbot.com/link/24077/17324472/trimming-our-css-with-sibling-index-and-sibling-count"/>
    <author>
      <name>Elaina Natario</name>
    </author>
    <id>https://thoughtbot.com/blog/trimming-our-css-with-sibling-index-and-sibling-count</id>
    <published>2026-04-24T00:00:00+00:00</published>
    <updated>2026-04-23T15:59:52Z</updated>
    <content type="html"><![CDATA[<p>CSS got some handy new functions recently: <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/sibling-index">sibling-index()</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/sibling-count">sibling-count()</a>. <code>sibling-index()</code> gives us a number based on a child element’s position relative to its siblings, starting at 1. For example, the third child in a list of 10 would have an index of 3. <code>sibling-count()</code> gives us the total number of siblings within a parent element. We can leverage these functions for more brevity in our CSS.</p>

<aside class="info">
  <p><a href="https://caniuse.com/wf-sibling-count">Firefox has yet to ship sibling-index() and sibling-count()</a>, so this is purely experimental at this point. Though I’m hopeful it’ll be widely available soon and ready for production so we can push this update live!</p>
</aside>
<h2 id="the-original-code">
  
    The original code
  
</h2>

<p>Our <a href="https://thoughtbot.com/case-studies">case studies page</a> has an animated marquee of company logos — we derived that code from <a href="https://www.frontend.fyi/tutorials/css-only-logo-marquee">this tutorial on frontend.fyi</a>. The popular approach to this pattern is to repeat the HTML a few times to create a seamless loop, but we wanted to avoid bloating the content with a leaner CSS-only approach.</p>

<p>Our HTML is fairly straightforward with container that controls the overflow and then a list with list items and an image for each logo.</p>
<div class="highlight"><pre class="highlight html"><code><span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"horizontally-scrolling-logos"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;ul</span> <span class="na">class=</span><span class="s">"horizontally-scrolling-logos__list"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"horizontally-scrolling-logos__item"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;img</span>
        <span class="na">src=</span><span class="s">"images/vimeo.png"</span>
        <span class="na">alt=</span><span class="s">"vimeo logo"</span>
        <span class="na">class=</span><span class="s">"horizontally-scrolling-logos__image"</span>
        <span class="na">loading=</span><span class="s">"lazy"</span>
      <span class="nt">&gt;</span>
    <span class="nt">&lt;/li&gt;</span>
    <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"horizontally-scrolling-logos__item"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;img</span>
        <span class="na">src=</span><span class="s">"images/merck.png"</span>
        <span class="na">alt=</span><span class="s">"Merck logo"</span>
        <span class="na">class=</span><span class="s">"horizontally-scrolling-logos__image"</span>
        <span class="na">loading=</span><span class="s">"lazy"</span>
      <span class="nt">&gt;</span>
    <span class="nt">&lt;/li&gt;</span>
    <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"horizontally-scrolling-logos__item"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;img</span>
        <span class="na">src=</span><span class="s">"images/planned-parentood.png"</span>
        <span class="na">alt=</span><span class="s">"Planned Parenthood logo"</span>
        <span class="na">class=</span><span class="s">"horizontally-scrolling-logos__image"</span>
        <span class="na">loading=</span><span class="s">"lazy"</span>
      <span class="nt">&gt;</span>
    <span class="nt">&lt;/li&gt;</span>
    <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"horizontally-scrolling-logos__item"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;img</span>
        <span class="na">src=</span><span class="s">"images/hbr.png"</span>
        <span class="na">alt=</span><span class="s">"Harvard Business Review logo"</span>
        <span class="na">class=</span><span class="s">"horizontally-scrolling-logos__image"</span>
        <span class="na">loading=</span><span class="s">"lazy"</span>
      <span class="nt">&gt;</span>
    <span class="nt">&lt;/li&gt;</span>
    <span class="nt">&lt;li</span> <span class="na">class=</span><span class="s">"horizontally-scrolling-logos__item"</span><span class="nt">&gt;</span>
      <span class="nt">&lt;img</span>
        <span class="na">src=</span><span class="s">"images/moma.png"</span>
        <span class="na">alt=</span><span class="s">"MOMA logo"</span>
        <span class="na">class=</span><span class="s">"horizontally-scrolling-logos__image"</span>
        <span class="na">loading=</span><span class="s">"lazy"</span>
      <span class="nt">&gt;</span>
    <span class="nt">&lt;/li&gt;</span>
  <span class="nt">&lt;/ul&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div>
<p>The CSS leverages a lot of custom properties to note the animation speed, the number of logos in the list, etc. All of these go in to calculate the track width and position of each logo in the marquee.</p>
<div class="highlight"><pre class="highlight scss"><code><span class="nc">.horizontally-scrolling-logos</span> <span class="p">{</span>
  <span class="na">--spacing--medium</span><span class="p">:</span> <span class="m">1</span><span class="mi">.5rem</span><span class="p">;</span>
  <span class="na">--speed</span><span class="p">:</span> <span class="m">25s</span><span class="p">;</span>
  <span class="na">--numItems</span><span class="p">:</span> <span class="m">5</span><span class="p">;</span>
  <span class="na">--single-slide-speed</span><span class="p">:</span> <span class="nf">calc</span><span class="p">(</span><span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">speed</span><span class="p">)</span> <span class="o">/</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">numItems</span><span class="p">));</span>
  <span class="na">--item-width</span><span class="p">:</span> <span class="m">20rem</span><span class="p">;</span>
  <span class="na">--item-gap</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">spacing--medium</span><span class="p">);</span>
  <span class="na">--item-width-plus-gap</span><span class="p">:</span> <span class="nf">calc</span><span class="p">(</span><span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">item-width</span><span class="p">)</span> <span class="o">+</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">item-gap</span><span class="p">));</span>
  <span class="na">--track-width</span><span class="p">:</span> <span class="nf">calc</span><span class="p">(</span><span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">item-width-plus-gap</span><span class="p">)</span> <span class="o">*</span> <span class="nf">calc</span><span class="p">(</span><span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">numItems</span><span class="p">)));</span>

  <span class="nl">overflow</span><span class="p">:</span> <span class="nb">hidden</span><span class="p">;</span>

  <span class="k">&amp;</span><span class="nd">:hover</span> <span class="nc">.horizontally-scrolling-logos__item</span> <span class="p">{</span>
    <span class="nl">animation-play-state</span><span class="p">:</span> <span class="nb">paused</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nc">.horizontally-scrolling-logos__list</span> <span class="p">{</span>
  <span class="nl">align-items</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
  <span class="nl">container-type</span><span class="p">:</span> <span class="nb">inline-size</span><span class="p">;</span>
  <span class="nl">display</span><span class="p">:</span> <span class="nb">grid</span><span class="p">;</span>
  <span class="nl">gap</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">spacing--medium</span><span class="p">);</span>
  <span class="nl">grid-template-columns</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">track-width</span><span class="p">)</span> <span class="p">[</span><span class="n">track</span><span class="p">]</span> <span class="m">0</span> <span class="p">[</span><span class="n">resting</span><span class="p">];</span>
  <span class="nl">width</span><span class="p">:</span> <span class="n">max-content</span><span class="p">;</span>

  <span class="k">@media</span> <span class="nb">screen</span> <span class="nf">and</span> <span class="p">(</span><span class="n">prefers-reduced-motion</span><span class="o">:</span> <span class="n">reduce</span><span class="p">)</span> <span class="p">{</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">flex</span><span class="p">;</span>
    <span class="nl">flex-wrap</span><span class="p">:</span> <span class="nb">wrap</span><span class="p">;</span>
    <span class="nl">justify-content</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
    <span class="nl">width</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nc">.horizontally-scrolling-logos__item</span> <span class="p">{</span>
  <span class="nl">animation</span><span class="p">:</span> <span class="n">marquee</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">speed</span><span class="p">)</span> <span class="nb">linear</span> <span class="nb">infinite</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">direction</span><span class="o">,</span> <span class="nb">forwards</span><span class="p">);</span>
  <span class="nl">animation-delay</span><span class="p">:</span> <span class="nf">calc</span><span class="p">(</span>
    <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">single-slide-speed</span><span class="p">)</span> <span class="o">*</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">item-position</span><span class="p">)</span> <span class="o">*</span> <span class="m">-1</span>
  <span class="p">);</span>
  <span class="nl">grid-area</span><span class="p">:</span> <span class="n">resting</span><span class="p">;</span>
  <span class="nl">width</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">item-width</span><span class="p">);</span>

  <span class="k">&amp;</span><span class="nd">:nth-child</span><span class="o">(</span><span class="nt">1</span><span class="o">)</span> <span class="p">{</span>
    <span class="na">--item-position</span><span class="p">:</span> <span class="m">1</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">&amp;</span><span class="nd">:nth-child</span><span class="o">(</span><span class="nt">2</span><span class="o">)</span> <span class="p">{</span>
    <span class="na">--item-position</span><span class="p">:</span> <span class="m">2</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">&amp;</span><span class="nd">:nth-child</span><span class="o">(</span><span class="nt">3</span><span class="o">)</span> <span class="p">{</span>
    <span class="na">--item-position</span><span class="p">:</span> <span class="m">3</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">&amp;</span><span class="nd">:nth-child</span><span class="o">(</span><span class="nt">4</span><span class="o">)</span> <span class="p">{</span>
    <span class="na">--item-position</span><span class="p">:</span> <span class="m">4</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">&amp;</span><span class="nd">:nth-child</span><span class="o">(</span><span class="nt">5</span><span class="o">)</span> <span class="p">{</span>
    <span class="na">--item-position</span><span class="p">:</span> <span class="m">5</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">@media</span> <span class="nb">screen</span> <span class="nf">and</span> <span class="p">(</span><span class="n">prefers-reduced-motion</span><span class="o">:</span> <span class="n">reduce</span><span class="p">)</span> <span class="p">{</span>
    <span class="nl">animation</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">flex</span><span class="p">;</span>
    <span class="nl">justify-content</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">@keyframes</span> <span class="nt">marquee</span> <span class="p">{</span>
  <span class="nt">to</span> <span class="p">{</span>
    <span class="nl">transform</span><span class="p">:</span> <span class="nf">translateX</span><span class="p">(</span><span class="nf">calc</span><span class="p">(</span><span class="m">-100cqw</span> <span class="o">-</span> <span class="m">100%</span><span class="p">));</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<p>And what it looks like all together:</p>

<figure>
  <p class="codepen" data-height="300" data-pen-title="Marquee Old" data-default-tab="css,result" data-slug-hash="KwgLmEM" data-user="enatario" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/enatario/pen/KwgLmEM">
  Marquee Old</a> by Elaina Natario (<a href="https://codepen.io/enatario">@enatario</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
  </p>
  <script async src="https://public.codepenassets.com/embed/index.js"></script>
</figure>
<h2 id="reducing-our-code-with-functions">
  
    Reducing our code with functions
  
</h2>

<p>We can reduce the verbosity of this in one spot in particular: where we are defining the position of each child element.</p>

<p>Right now we’re using <code>--item-position</code> to relay the index of the child and set a staggered animation delay on each logo, which creates the marquee effect. We can remove all those <code>nth-child</code> declarations with a <code>sibling-index()</code> function in lieu of <code>--item-position</code>:</p>
<div class="highlight"><pre class="highlight diff"><code>.horizontally-scrolling-logos__item {
  animation: marquee var(--speed) linear infinite var(--direction, forwards);
  animation-delay: calc(
<span class="gd">-    var(--single-slide-speed) * var(--item-position) * -1
</span><span class="gi">+    var(--single-slide-speed) * sibling-index() * -1
</span>  );
  grid-area: resting;
  width: var(--item-width);
<span class="err">
</span><span class="gd">-  &amp;:nth-child(1) {
-    --item-position: 1;
-  }
-  &amp;:nth-child(2) {
-    --item-position: 2;
-  }
-  &amp;:nth-child(3) {
-    --item-position: 3;
-  }
-  &amp;:nth-child(4) {
-    --item-position: 4;
-  }
-  &amp;:nth-child(5) {
-    --item-position: 5;
-  }
</span><span class="err">
</span>  @media screen and (prefers-reduced-motion: reduce) {
    animation: none;
    display: flex;
    justify-content: center;
  }
}
</code></pre></div>
<p>The <code>--single-slide-speed</code> calculates the overall speed of the animation divided by the number of children (–numItems). We can use <code>sibling-count()</code> here to replace <code>--numItems</code>. And we’ll need to move that calculation to be within the <code>.horizontally-scrolling-logos__item</code> to be able to count the siblings.</p>
<div class="highlight"><pre class="highlight diff"><code>.horizontally-scrolling-logos__item {
<span class="gi">+  --single-slide-speed: calc(var(--speed) / sibling-count());
</span><span class="err">
</span>  animation: marquee var(--speed) linear infinite var(--direction, forwards);
  animation-delay: calc(
    var(--single-slide-speed) * sibling-index() * -1
  );
  grid-area: resting;
  width: var(--item-width);
<span class="err">
</span>  @media screen and (prefers-reduced-motion: reduce) {
    animation: none;
    display: flex;
    justify-content: center;
  }
}
</code></pre></div>
<p>And that’s it! A small change overall, but an impactful one!</p>

<figure>
  <p class="codepen" data-height="300" data-pen-title="Marquee New" data-default-tab="css,result" data-slug-hash="WbGBjmQ" data-user="enatario" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/enatario/pen/WbGBjmQ">
  Marquee New</a> by Elaina Natario (<a href="https://codepen.io/enatario">@enatario</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
  </p>
  <script async src="https://public.codepenassets.com/embed/index.js"></script>
</figure>
<h2 id="an-annoying-caveat">
  
    An annoying caveat
  
</h2>

<p>In a perfect world, we’d completely replace <code>--numItems</code> with <code>sibling-count()</code> so our CSS doesn’t have to manually track the number of logos in our marquee. But, in this current implementation, we need the parent element to use that to define the track width, not the children. Perhaps one day, <a href="https://github.com/w3c/csswg-drafts/issues/11068">we’ll have a function like <code>children-count()</code></a> to allow for more dynamic data in our CSS.</p>

<p>Another approach is to offload it onto Javascript by counting the siblings and setting the custom property inline. Our goal here, however, is to keep as much in the CSS as possible, and the tradeoff doesn’t seem worthwhile in this case.</p>

<p>And of course, there are a <a href="https://frontendmasters.com/blog/infinite-marquee-animation-using-modern-css/">handful of other approaches</a> to this web pattern that other people have solved in a variety of ways that would require us to rethink this architecture entirely. But we’re here for a quick and easy win.</p>
<h2 id="a-quick-word-on-motion">
  
    A quick word on motion
  
</h2>

<p>You may have also noticed a declaration block in the code defining a reduced motion layout. This has nothing to do with <code>sibling-index()</code> or <code>sibling-count()</code> but feels worth mentioning (and <a href="https://heyvaleria.github.io/accessibility/design/development/coding/inclusive-design/2026/03/24/designing-for-reduced-motion.html">has been a topic of discussion within our team</a>). While we can do very fun things with animation in CSS, it’s still important to <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/prefers-reduced-motion">respect user preferences</a>. Our scroll animation turns into static side-by-side images when that preference is reduced motion.</p>

<p>We could improve the code even more, by implementing an opt-in rather than an opt-out preference query.</p>
<div class="highlight"><pre class="highlight scss"><code><span class="nc">.horizontally-scrolling-logos__list</span> <span class="p">{</span>
  <span class="nl">align-items</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
  <span class="nl">display</span><span class="p">:</span> <span class="nb">flex</span><span class="p">;</span>
  <span class="nl">flex-wrap</span><span class="p">:</span> <span class="nb">wrap</span><span class="p">;</span>
  <span class="nl">justify-content</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
  <span class="nl">width</span><span class="p">:</span> <span class="nb">auto</span><span class="p">;</span>
  <span class="nl">container-type</span><span class="p">:</span> <span class="nb">inline-size</span><span class="p">;</span>
  <span class="nl">display</span><span class="p">:</span> <span class="nb">grid</span><span class="p">;</span>
  <span class="nl">gap</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">spacing--medium</span><span class="p">);</span>
  <span class="nl">grid-template-columns</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">track-width</span><span class="p">)</span> <span class="p">[</span><span class="n">track</span><span class="p">]</span> <span class="m">0</span> <span class="p">[</span><span class="n">resting</span><span class="p">];</span>
  <span class="nl">width</span><span class="p">:</span> <span class="n">max-content</span><span class="p">;</span>

  <span class="k">@media</span> <span class="nb">screen</span> <span class="nf">and</span> <span class="p">(</span><span class="n">prefers-reduced-motion</span><span class="o">:</span> <span class="n">no-preference</span><span class="p">)</span> <span class="p">{</span>
    <span class="nl">container-type</span><span class="p">:</span> <span class="nb">inline-size</span><span class="p">;</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">grid</span><span class="p">;</span>
    <span class="nl">gap</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">spacing--medium</span><span class="p">);</span>
    <span class="nl">grid-template-columns</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">track-width</span><span class="p">)</span> <span class="p">[</span><span class="n">track</span><span class="p">]</span> <span class="m">0</span> <span class="p">[</span><span class="n">resting</span><span class="p">];</span>
    <span class="nl">width</span><span class="p">:</span> <span class="n">max-content</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nc">.horizontally-scrolling-logos__item</span> <span class="p">{</span>
  <span class="na">--single-slide-speed</span><span class="p">:</span> <span class="nf">calc</span><span class="p">(</span><span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">speed</span><span class="p">)</span> <span class="o">/</span> <span class="nf">sibling-count</span><span class="p">());</span>

  <span class="nl">animation</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
  <span class="nl">display</span><span class="p">:</span> <span class="nb">flex</span><span class="p">;</span>
  <span class="nl">justify-content</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>

  <span class="k">@media</span> <span class="nb">screen</span> <span class="nf">and</span> <span class="p">(</span><span class="n">prefers-reduced-motion</span><span class="o">:</span> <span class="n">no-preference</span><span class="p">)</span> <span class="p">{</span>
    <span class="nl">animation</span><span class="p">:</span> <span class="n">marquee</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">speed</span><span class="p">)</span> <span class="nb">linear</span> <span class="nb">infinite</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">direction</span><span class="o">,</span> <span class="nb">forwards</span><span class="p">);</span>
    <span class="nl">animation-delay</span><span class="p">:</span> <span class="nf">calc</span><span class="p">(</span>
      <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">single-slide-speed</span><span class="p">)</span> <span class="o">*</span> <span class="nf">sibling-index</span><span class="p">()</span> <span class="o">*</span> <span class="m">-1</span>
    <span class="p">);</span>
    <span class="nl">grid-area</span><span class="p">:</span> <span class="n">resting</span><span class="p">;</span>
    <span class="nl">width</span><span class="p">:</span> <span class="nf">var</span><span class="p">(</span><span class="o">--</span><span class="n">item-width</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<hr>

<p>All of this is to say: small changes like this add up. Replacing repetitive selectors with functions like <code>sibling-index()</code> and <code>sibling-count()</code> makes the code easier to read and maintain, and a little more resilient to change.</p>

<p>It’s also a <a href="https://blog.logrocket.com/css-in-2026/">glimpse at where CSS is headed</a>. As more logic moves into the language itself, we can rely less on JavaScript for things like layout and interactivity. That shift doesn’t always come in big, flashy features, but in small utilities that quietly reduce verbosity.</p>

<p>This particular change won’t revolutionize your codebase. But it does make things a bit simpler, and that’s usually a good trade.</p>

<p>And if the pace of new CSS features is any indication, we’ll have plenty more opportunities like this soon.</p>

<aside class="related-articles"><h2>If you enjoyed this post, you might also like:</h2>
<ul>
<li><a href="https://thoughtbot.com/blog/debugging-why-your-specs-have-slowed-down">Debugging Why Your Specs Have Slowed Down</a></li>
<li><a href="https://thoughtbot.com/blog/theme-based-iterations">Theme-Based Iterations</a></li>
<li><a href="https://thoughtbot.com/blog/rust-doesn-t-have-named-arguments-so-what">Rust Doesn’t Have Named Arguments. So What?</a></li>
</ul></aside>
<img src="https://feed.thoughtbot.com/link/24077/17324472.gif" height="1" width="1"/>]]></content>
    <summary>We're experimenting with two new CSS functions to clean up our logo marquee code.</summary>
    <thoughtbot:auto_social_share>true</thoughtbot:auto_social_share>
  </entry>
  <entry>
    <title>Seven commands and the communication layer that emerged</title>
    <link rel="alternate" href="https://feed.thoughtbot.com/link/24077/17323874/seven-commands-and-the-communication-layer-that-emerged"/>
    <author>
      <name>Rob Whittaker</name>
    </author>
    <id>https://thoughtbot.com/blog/seven-commands-and-the-communication-layer-that-emerged</id>
    <published>2026-04-23T00:00:00+00:00</published>
    <updated>2026-04-22T10:22:30Z</updated>
    <content type="html"><![CDATA[<p>On Tuesday, 11 February, I made seventeen commits to my
management system. That is more than any other day in the
project so far. The previous two weeks had been about
structure. Daily routines. Meeting sync. Project tracking.
This week was about communication.</p>

<p>The trigger was simple. I ran <code>/inbox</code> and spotted the
pattern. Every time: fetch the item, decide what to do,
place it somewhere, move on. The first version of the
command automated that loop:</p>
<div class="highlight"><pre class="highlight markdown"><code><span class="gh"># Inbox Command</span>

Process the Things inbox one item at a time, newest first.

<span class="gu">## Instructions</span>

<span class="gu">### Step 1: Load Context</span>

Fetch in parallel:
<span class="p">
1.</span> Call <span class="sb">`mcp__things__get_inbox`</span> to get all inbox items
<span class="p">1.</span> Call <span class="sb">`mcp__things__get_projects`</span> to get project names

Sort inbox items newest first (by creation date).

If the inbox is empty, report "Inbox is empty" and stop.

<span class="gu">### Step 2: Present the Next Item</span>

For each inbox item, present:
<span class="p">
-</span> Title
<span class="p">-</span> Age
<span class="p">-</span> Tags (if any)
<span class="p">-</span> Notes (truncated if long)
<span class="p">-</span> Related project (fuzzy-match title against project names)

Then wait for the user to say what they want to do.
</code></pre></div>
<p>Within thirty minutes, that command went through three
revisions. The loop version advanced on its own. I changed
it to single-item mode because I wanted control. Then I
added reading detection: if the notes contain a URL, fetch
the page title and suggest a tag. I created three commits,
three lessons about how I process information.</p>
<h3 id="the-one-day-command">
  
    The one-day command
  
</h3>

<p>The same morning, I created <code>/reply</code> for Slack DMs. It
standardised the flow: find the user, open the DM, fetch
the history, draft the reply, and send.</p>

<p>It lasted twenty-four hours.</p>

<p>By Wednesday, I had split it into <code>/dm</code> for direct messages
and <code>/thread</code> for channel thread replies. Both shared a
patterns file that held the common steps:</p>
<div class="highlight"><pre class="highlight markdown"><code><span class="gh"># DM Command</span>

Send a direct message on Slack to $ARGUMENTS.

Follow shared patterns from
<span class="sb">`.claude/commands/slack-patterns.md`</span>.

<span class="gu">## Instructions</span>

<span class="gu">### Step 1: Setup and Find User</span>
<span class="p">
1.</span> <span class="gs">**Rube Session Setup**</span> (see <span class="sb">`slack-patterns.md`</span>)
<span class="p">1.</span> <span class="gs">**Find User**</span> (see <span class="sb">`slack-patterns.md`</span>)

<span class="gu">### Step 2: Open DM and Fetch History</span>
<span class="p">
1.</span> Use <span class="sb">`SLACK_OPEN_DM`</span> with the user's ID
<span class="p">1.</span> <span class="gs">**Fetch History**</span> on the DM channel
   (see <span class="sb">`slack-patterns.md`</span>)
</code></pre></div>
<p>The split happened because DMs and threads are different
conversations. A DM is private, one-to-one, with full
history. A thread is public, anchored to a specific message,
with context that the whole channel can see. The same “reply”
verb hid two different communication patterns.</p>
<h3 id="the-communication-stack">
  
    The communication stack
  
</h3>

<p>That refactoring revealed something. Each command I built
that week mapped to a communication channel:</p>

<ul>
<li>
<code>/dm</code> — Slack direct messages</li>
<li>
<code>/thread</code> — Slack channel threads</li>
<li>
<code>/slack</code> — new channel messages</li>
<li>
<code>/email</code> — Gmail replies and composition</li>
<li>
<code>/hub</code> — reading saved Hub pages</li>
<li>
<code>/draft</code> — anything else (LinkedIn, talking points,
Hub replies)</li>
</ul>

<p>Six commands. Six ways I talk to people at work. The <code>/draft</code>
command became the catch-all for channels without an API:</p>
<div class="highlight"><pre class="highlight markdown"><code><span class="gh"># Draft Command</span>

Draft a reply or message for any context. Does not send.

Use this for LinkedIn messages, in-person talking points,
Hub replies, or any situation where <span class="sb">`/dm`</span>, <span class="sb">`/thread`</span>, and
<span class="sb">`/email`</span> don't apply.

<span class="gu">## Pattern: Voice</span>

Blend the user's natural tone with DHH and Nicholas Lezard:
<span class="p">
-</span> Direct and opinionated, but not abrasive
<span class="p">-</span> Concise sentences that carry weight
<span class="p">-</span> Avoid corporate filler
<span class="p">-</span> Match the formality of the channel
</code></pre></div>
<p>The voice pattern is the part I did not expect to matter. I
had assumed Claude would write in a generic assistant tone.
Instead, by defining a voice, every draft came back in a
register I recognised as mine. Not perfect. Close enough to
edit rather than rewrite.</p>
<h3 id="plan-mode-and-command-boundaries">
  
    Plan mode and command boundaries
  
</h3>

<p>Not everything went well. On Tuesday, I hit a bug where
plan mode leaked between commands. When I ran <code>/waiting</code>,
it accumulated a state that bled into <code>/retro</code>. That broke
both commands.</p>

<p>The fix took two commits and a revert. The first attempt
added “Never use EnterPlanMode” to every command. That was
wrong. The real fix was removing the auto-advance loop from
<code>/waiting</code>. Each command invocation stayed self-contained.
Commands are not functions. They are conversations. And
conversations should end clean.</p>
<h3 id="what-i-learned">
  
    What I learned
  
</h3>

<p>Building these commands showed me something. My job as a
director is communication:</p>

<ul>
<li>Write messages</li>
<li>Respond to threads</li>
<li>Follow up on waiting items</li>
<li>Process my inbox</li>
<li>Draft replies</li>
<li>Read Hub posts</li>
</ul>

<p>The actual management decisions happen in the gaps between
those conversations.</p>

<p>The system I built in the first two weeks gave me structure:
routines, meeting sync, and project files. This week gave me
flow. The difference is that structure tells you what to do.
Flow tells you how to do it without thinking about the
mechanics.</p>

<p>I am not faster. I am less distracted. Each command removes
one decision about where to go and what to type. That
compounds over a day of fifty small conversations.</p>
<h3 id="try-it-yourself">
  
    Try it yourself
  
</h3>

<p>Pick one communication pattern you repeat daily. Write a
command for it. Not a script. A conversation. Define the
steps, the voice, the context. Then run it and see what
breaks. The breaking is where the learning is.</p>

<aside class="related-articles"><h2>If you enjoyed this post, you might also like:</h2>
<ul>
<li><a href="https://thoughtbot.com/blog/a-bullet-in-your-programs-head">A bullet in your programs head</a></li>
<li><a href="https://thoughtbot.com/blog/internbot-chronicles-2">Internbot Chronicles #2</a></li>
<li><a href="https://thoughtbot.com/blog/retrospective-fashionopoly">Retrospective: Fashionopoly</a></li>
</ul></aside>
<img src="https://feed.thoughtbot.com/link/24077/17323874.gif" height="1" width="1"/>]]></content>
    <summary>Week three of building a management system with Claude Code. Seventeen commits in one day, a command that lasted 24 hours, and the realisation that commands are conversations.</summary>
    <thoughtbot:auto_social_share>true</thoughtbot:auto_social_share>
  </entry>
</feed>
