<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://levon003.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://levon003.github.io/" rel="alternate" type="text/html" /><updated>2026-08-15T06:30:00+00:00</updated><id>https://levon003.github.io/feed.xml</id><title type="html">Zachary Levonian’s blog</title><subtitle>Zachary Levonian&apos;s blog, for self-published writing. Zach is a researcher and data scientist with a PhD in Human-computer Interaction.</subtitle><entry><title type="html">Penn &amp;amp; Teller’s Love Ritual: a self-working card trick</title><link href="https://levon003.github.io/2026/07/04/card-trick.html" rel="alternate" type="text/html" title="Penn &amp;amp; Teller’s Love Ritual: a self-working card trick" /><published>2026-07-04T00:00:00+00:00</published><updated>2026-07-04T00:00:00+00:00</updated><id>https://levon003.github.io/2026/07/04/card-trick</id><content type="html" xml:base="https://levon003.github.io/2026/07/04/card-trick.html"><![CDATA[<p>Penn and Teller performed a “self-working” card trick on <a href="https://en.wikipedia.org/wiki/Penn_%26_Teller:_Fool_Us"><em>Penn &amp; Teller: Fool Us</em></a>. You can watch the trick <a href="https://youtu.be/K9vEYyBwgp4?t=2040">on YouTube</a>.</p>

<p>The trick is themed around finding love and the presentation has several good gags. But the basis of the trick is just executing a series of steps:</p>

<ol>
  <li>Shuffle 4 randomly-selected playing cards into a stack.</li>
  <li>Tear the stack in half, placing one half on top of the other, producing a new stack of 8 half-cards.</li>
  <li>Put the top card on the bottom of the stack.</li>
  <li>Put the top 2 cards on the bottom of the stack.</li>
  <li>Put the top 3 cards (together) somewhere in the middle.</li>
  <li>Pull the top card aside – this is the <strong>comparison card</strong>.</li>
  <li>Take 1, 2, or 3 cards from the top and put them somewhere in the middle.</li>
  <li>Put the top card somewhere in the middle.</li>
  <li>Throw away 1, 2, or 3 cards from the top.</li>
  <li>Seven times: put the top card on the bottom.</li>
  <li>Then, alternating until one card is left: top card to the bottom, then throw away the new top card.</li>
  <li>The surviving card matches the comparison card.</li>
</ol>

<p>There are choices in steps 2, 7, 8, and 9, but the trick works regardless. Why?</p>

<h2 id="try-it-yourself">Try it yourself</h2>

<p>First, try the trick yourself. Step through the moves and make the choices to get a feel for the trick.</p>

<!-- Performer's-eye view of the "Love Ritual" torn-card trick. Faithful to every move and
     spectator choice; identical face-down halves; history-backed so Back always works.
     The opening tear is fully illustrated: square up -> rip (animated clip-path) -> two
     half-stacks -> drop one on the other -> fan into a row. After the tear, every card is
     rendered as a half-height torn piece. Self-contained; include this file from a post. -->
<div class="ctp-widget" id="ctp-widget">
  <div class="ctp-stage-wrap">
    <div class="ctp-stage" id="ctp-stage">
      <div class="ctp-zone-label ctp-aside-label" id="ctp-aside-label">your&nbsp;card</div>
      <div class="ctp-zone-label ctp-discard-label" id="ctp-discard-label">discarded</div>
      <div class="ctp-pile-label" id="ctp-pl-top">top halves</div>
      <div class="ctp-pile-label" id="ctp-pl-bot">bottom halves</div>
      <div class="ctp-gaps" id="ctp-gaps"></div>
      <!-- card elements injected here -->
    </div>
  </div>

  <div class="ctp-prompt" id="ctp-prompt"></div>

  <div class="ctp-controls" id="ctp-controls">
    <button class="ctp-btn" id="ctp-back">◀ Back</button>
    <span class="ctp-advance" id="ctp-advance"></span>
    <button class="ctp-btn ctp-ghost" id="ctp-reset">↻ Restart</button>
  </div>
  <label class="ctp-toggle"><input type="checkbox" id="ctp-hl" /> Highlight the comparison card's two halves</label>
</div>

<style>
.ctp-widget{max-width:560px;margin:24px auto;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#2d2d2d}
.ctp-stage-wrap{width:100%;display:flex;justify-content:center;overflow:hidden}
.ctp-stage{position:relative;flex:none;width:440px;height:280px;transform-origin:top center}
.ctp-gaps{position:absolute;inset:0;z-index:40;pointer-events:none}
.ctp-gap{position:absolute;width:22px;height:44px;transform:translate(-50%,-50%);border-radius:5px;border:2px dashed #c7a14a;background:rgba(244,194,13,.12);cursor:pointer;pointer-events:auto;transition:background .12s ease,transform .12s ease}
.ctp-gap:hover{background:rgba(244,194,13,.4);transform:translate(-50%,-50%) scale(1.12)}
.ctp-gap::after{content:"▾";position:absolute;top:-16px;left:50%;transform:translateX(-50%);color:#c7a14a;font-size:13px}
.ctp-card{position:absolute;width:40px;height:29px;transition:transform .42s cubic-bezier(.22,.61,.36,1),opacity .4s ease,filter .4s ease;z-index:2;will-change:transform}
.ctp-noanim .ctp-card{transition:none!important}
.ctp-card .ctp-face{width:100%;height:100%;box-sizing:border-box;position:relative;overflow:hidden;
  background:#3a4a8f;background-image:repeating-linear-gradient(45deg,rgba(255,255,255,.10) 0 4px,transparent 4px 8px),repeating-linear-gradient(-45deg,rgba(255,255,255,.10) 0 4px,transparent 4px 8px);
  border:2px solid #2c3a73;transition:clip-path .5s ease}
/* each card is two half-elements: .th (top half) + .bh (bottom half), flush = one whole card */
.ctp-card.th .ctp-face{border-radius:6px 6px 0 0;border-bottom:none;
  clip-path:polygon(0 0,100% 0,100% 100%,87% 100%,75% 100%,62% 100%,50% 100%,37% 100%,25% 100%,12% 100%,0 100%)}
.ctp-card.bh .ctp-face{border-radius:0 0 6px 6px;border-top:none;background-position:0 -29px;
  clip-path:polygon(0 0,12% 0,25% 0,37% 0,50% 0,62% 0,75% 0,87% 0,100% 0,100% 100%,0 100%)}
/* torn: the flat edge becomes a jagged rip (same vertex count, so it animates) */
.ctp-card.torn.th .ctp-face{clip-path:polygon(0 0,100% 0,100% 74%,87% 100%,75% 76%,62% 98%,50% 74%,37% 100%,25% 78%,12% 96%,0 72%)}
.ctp-card.torn.bh .ctp-face{clip-path:polygon(0 28%,12% 4%,25% 26%,37% 0,50% 26%,62% 2%,75% 24%,87% 0,100% 26%,100% 100%,0 100%)}
/* clip-path clips box-shadow, so cast shadows via drop-shadow (follows the jagged shape) */
.ctp-card.bh,.ctp-card.torn,.ctp-card.rv{filter:drop-shadow(0 2px 2.5px rgba(0,0,0,.28))}
.ctp-card.lifted{filter:drop-shadow(0 7px 7px rgba(0,0,0,.28))}
.ctp-card.lifted::before{content:"";position:absolute;inset:-3px;border:2px solid #f4c20d;border-radius:8px;z-index:1}
.ctp-card.discarded{opacity:.4;filter:grayscale(.7) brightness(1.12)}
.ctp-card.hint::after{content:"";position:absolute;inset:-2px;border:2px dashed #8fa0e0;border-radius:8px;pointer-events:none}
.ctp-card.cmp::before{content:"";position:absolute;inset:-3px;border:3px solid #11a39b;border-radius:9px;box-shadow:0 0 7px rgba(17,163,155,.75);z-index:1}
@keyframes ctp-rip{0%{transform:rotate(0) translate(0,0)}22%{transform:rotate(-6deg) translate(-2px,-2px)}48%{transform:rotate(5deg) translate(2px,2px)}74%{transform:rotate(-3deg) translate(-1px,1px)}100%{transform:rotate(0) translate(0,0)}}
.ctp-card.tearing .ctp-face{animation:ctp-rip .42s ease both}
/* reveal styling: white face + heart, jagged edge from the torn clip */
.ctp-card.rv .ctp-face{background:#fff;border:1px solid #d8a7ad;border-radius:6px;color:#e2293f}
.ctp-card.rv.rv-top .ctp-face{clip-path:polygon(0 0,100% 0,100% 74%,87% 100%,75% 76%,62% 98%,50% 74%,37% 100%,25% 78%,12% 96%,0 72%)}
.ctp-card.rv.rv-bot .ctp-face{clip-path:polygon(0 28%,12% 4%,25% 26%,37% 0,50% 26%,62% 2%,75% 24%,87% 0,100% 26%,100% 100%,0 100%)}
.ctp-card.rv .ctp-heart{position:absolute;left:50%;transform:translateX(-50%);font-size:42px;line-height:1}
.ctp-card.rv.rv-top .ctp-heart{top:8px}
.ctp-card.rv.rv-bot .ctp-heart{top:-21px}
.ctp-card.rv .ctp-corner{position:absolute;font-size:9px;font-weight:800;line-height:.85;text-align:center}
.ctp-card.rv.rv-top .ctp-corner{top:2px;left:3px}
.ctp-card.rv.rv-bot .ctp-corner{bottom:2px;right:3px;transform:rotate(180deg)}
.ctp-card.match::after{content:"";position:absolute;inset:-4px;border:3px solid #2ecc71;border-radius:9px;animation:ctp-glow 1.1s ease infinite;z-index:3;pointer-events:none}
@keyframes ctp-glow{0%,100%{box-shadow:0 0 3px rgba(46,204,113,.5)}50%{box-shadow:0 0 13px rgba(46,204,113,1)}}
.ctp-zone-label{position:absolute;font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:#9aa0a6;font-weight:600;opacity:0;transition:opacity .3s}
.ctp-zone-label.show{opacity:1}
.ctp-aside-label{right:8px;top:0}
.ctp-discard-label{left:12px;top:192px}
.ctp-pile-label{position:absolute;font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:#9aa0a6;font-weight:600;opacity:0;transition:opacity .3s;transform:translateX(-50%);white-space:nowrap}
.ctp-pile-label.show{opacity:1}
.ctp-prompt{min-height:3.2em;background:#f6f7f9;border-left:4px solid #3a4a8f;border-radius:0 6px 6px 0;padding:12px 14px;margin:6px 0 12px;font-size:15.5px;line-height:1.5;transition:background .25s ease,border-color .25s ease}
.ctp-prompt.choose{background:#fff6e5;border-left-color:#e6932f}
.ctp-prompt.win{background:#eafaf0;border-left-color:#2ecc71;font-weight:500}
.ctp-controls{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-height:42px}
.ctp-advance{display:inline-flex;gap:8px;align-items:center;flex-wrap:wrap}
.ctp-btn{background:#fff;border:1.5px solid #d0d5dd;border-radius:6px;padding:9px 15px;font-size:14px;cursor:pointer;transition:all .15s ease;font-weight:500}
.ctp-btn:hover:not(:disabled){border-color:#3a4a8f;background:#eef1fb}
.ctp-btn:disabled{opacity:.4;cursor:not-allowed}
.ctp-btn.ctp-primary{background:#3a4a8f;color:#fff;border-color:#3a4a8f}
.ctp-btn.ctp-primary:hover:not(:disabled){background:#2f3d78}
.ctp-btn.ctp-ghost{margin-left:auto;color:#6b7280}
.ctp-choicetray{display:inline-flex;align-items:center;gap:8px;background:#fff6e5;border:1px solid #f0cf99;border-radius:8px;padding:5px 10px 5px 12px}
.ctp-choice-label{font-size:13px;color:#9a6a16;font-weight:600}
.ctp-btn.ctp-choice{min-width:42px;font-size:15px;font-weight:800;padding:8px 6px;background:#e6932f;border-color:#e6932f;color:#fff}
.ctp-btn.ctp-choice:hover{background:#d4811f;border-color:#d4811f}
.ctp-hintmsg{font-size:13px;color:#9a6a16;font-style:italic}
.ctp-toggle{display:inline-flex;align-items:center;gap:6px;margin-top:10px;font-size:13px;color:#555;cursor:pointer}
@media (max-width:480px){.ctp-prompt{font-size:14.5px}.ctp-btn{padding:8px 12px}}
</style>

<script>
(function(){
  function init(){
    var W=document.getElementById('ctp-widget');
    if(!W||W.dataset.ready)return; W.dataset.ready='1';

    var STAGE_W=440, STAGE_H=280, CARD_W=40, HALF=29;
    var DECK_S=46, DECK_Y=128, LIFT_Y=62, ASIDE_X=STAGE_W-CARD_W-6, ASIDE_Y=14, DISC_Y=204, DISC_S=22;
    var STEP_MS=240;            // per-card pace for cuts / dealing
    var WHOLE_Y=94;             // y of the 4 whole cards (each = th + bh flush, 58px tall)
    var PACK_X=(STAGE_W-CARD_W)/2, PACK_Y=94;          // squared packet
    var PILE_LX=142, PILE_RX=258, PILE_Y=118;          // the two half-stacks after tearing

    // 8 half-cards. card(uid)=uid%4; uid<4 = top half, uid+4 = its bottom half.
    var cards=[]; for(var u=0;u<8;u++) cards.push({uid:u});
    var BASE_TOPS=[0,1,2,3,4,5,6,7];   // top halves on top
    var BASE_BOTS=[4,5,6,7,0,1,2,3];   // bottom halves on top
    var CMP_PAIR=[2,6];                // the two halves that become comparison + survivor (either base)

    function rot(s,k){k=((k%s.length)+s.length)%s.length;return s.slice(k).concat(s.slice(0,k));}
    function toMiddle(s,m,p){var b=s.slice(0,m),r=s.slice(m);return r.slice(0,p).concat(b,r.slice(p));}

    // DOM
    var stage=document.getElementById('ctp-stage'), els={};
    stage.classList.add('ctp-noanim');   // no fly-in on first paint
    cards.forEach(function(c){
      var el=document.createElement('div'); el.className='ctp-card'; el.dataset.uid=c.uid;
      el.appendChild(Object.assign(document.createElement('div'),{className:'ctp-face'}));
      stage.appendChild(el); els[c.uid]=el;
    });
    var gapsLayer=document.getElementById('ctp-gaps'),
        asideLabel=document.getElementById('ctp-aside-label'), discardLabel=document.getElementById('ctp-discard-label'),
        plTop=document.getElementById('ctp-pl-top'), plBot=document.getElementById('ctp-pl-bot'),
        prompt=document.getElementById('ctp-prompt'), advance=document.getElementById('ctp-advance'),
        bBack=document.getElementById('ctp-back'), bReset=document.getElementById('ctp-reset'), cbHL=document.getElementById('ctp-hl');
    plTop.style.left=(PILE_LX+CARD_W/2+2)+'px'; plTop.style.top=(PILE_Y+HALF+13)+'px';
    plBot.style.left=(PILE_RX+CARD_W/2+2)+'px'; plBot.style.top=(PILE_Y+HALF+13)+'px';

    function deckX(i,n){return (STAGE_W-((n-1)*DECK_S+CARD_W))/2 + i*DECK_S;}

    // ---- phase metadata ----
    function phaseInfo(st){
      switch(st.phase){
        case 'start':  return {prompt:'Four cards, face down. First, <strong>square them up</strong> into a stack.',ctrl:'next',act:'gather',btn:'Begin ▶'};
        case 'packet': return {prompt:'Grip the whole stack and <strong>tear the cards in half</strong>.',ctrl:'next',act:'tear',btn:'Tear ✂'};
        case 'stack':  return {prompt:'You now have a stack of <strong>top halves</strong> and a stack of <strong>bottom halves</strong>. Drop one stack onto the other — <strong>which goes on top?</strong>',ctrl:'binary'};
        case 'cut1':   return {prompt:'Eight half-cards. Next: <strong>put the top card on the bottom</strong>.',ctrl:'next',hint:1,act:'rot1'};
        case 'cut2':   return {prompt:'Next: <strong>put the top two cards on the bottom</strong>, one at a time.',ctrl:'next',hint:2,act:'rot2'};
        case 'block5': return {prompt:'Take the <strong>top three cards together</strong> and slot them <strong>somewhere in the middle</strong>. Choose a gap.',ctrl:'pos',m:3,choose:true};
        case 'aside':  return {prompt:'Next: <strong>pull the top card aside</strong> — that\'s the comparison card.',ctrl:'next',hint:1,act:'setaside'};
        case 'take7c': return {prompt:'<strong>Your decision:</strong> take <strong>1, 2, or 3</strong> cards from the top…',ctrl:'count',which:7,choose:true};
        case 'take7p': return {prompt:'…and slot those '+st.k7+' card'+(st.k7>1?'s':'')+' <strong>somewhere in the middle</strong>. Choose a gap.',ctrl:'pos',m:st.k7,choose:true};
        case 'top8':   return {prompt:'Next: <strong>put the top card somewhere in the middle</strong>. Choose a gap.',ctrl:'pos',m:1,choose:true};
        case 'disc9':  return {prompt:'<strong>Your decision:</strong> <strong>throw away 1, 2, or 3</strong> cards from the top.',ctrl:'count',which:9,choose:true};
        case 'cut7':   return {prompt:'Next: <strong>put the top card on the bottom</strong> seven times.',ctrl:'next',hint:1,act:'rot7'};
        case 'under':  return {prompt:'Deal <strong>down-under</strong>: top card under (to the bottom), next card discarded. '+(st.deck.length)+' cards left.',ctrl:'deal',hint:1};
        case 'reveal': return {prompt:'One card remains. Match it with the card you set aside.',ctrl:'reveal'};
        case 'end':    return {prompt:'Your card and the lone survivor are the <strong>two halves of the same card</strong>. 🎉',ctrl:'end',reveal:true};
      }
    }

    // ---- rendering ----
    function reset(u){ var el=els[u]; el.className='ctp-card '+(u<4?'th':'bh');
      var f=el.querySelector('.ctp-face'); f.innerHTML=''; f.style.transitionDelay=''; }
    function place(el,x,y,rotDeg,z){
      el.style.transform='translate('+x+'px,'+y+'px)'+(rotDeg?' rotate('+rotDeg+'deg)':'');
      el.style.opacity='1'; el.style.zIndex=(z==null?2:z); }
    function dressReveal(el,top){ el.classList.add('rv',top?'rv-top':'rv-bot','match');
      el.querySelector('.ctp-face').innerHTML='<span class="ctp-heart">♥</span><span class="ctp-corner">A<br>♥</span>'; }
    function showPiles(b){ plTop.classList.toggle('show',b); plBot.classList.toggle('show',b); }
    function cmpRings(pred){ if(!cbHL.checked) return;
      CMP_PAIR.forEach(function(u){ if(!pred||pred(u)) els[u].classList.add('cmp'); }); }

    // 4 whole cards in a row (each = top half + bottom half, flush)
    function rowWhole(){
      cards.forEach(function(c){ reset(c.uid); });
      var sx=(STAGE_W-(3*66+CARD_W))/2;
      for(var id=0;id<4;id++){ var x=sx+id*66;
        place(els[id], x, WHOLE_Y, 0, 3); place(els[id+4], x, WHOLE_Y+HALF, 0, 3); }
      cmpRings();
    }
    // 4 whole cards squared into one packet (small offsets suggest thickness)
    function packetLayout(moveOnly){
      if(!moveOnly) cards.forEach(function(c){ reset(c.uid); });
      for(var id=0;id<4;id++){ var x=PACK_X+id*1.6, y=PACK_Y+id*1.6, z=30-id;
        place(els[id], x, y, 0, z); place(els[id+4], x, y+HALF, 0, z); }
      if(!moveOnly) cmpRings();
    }
    // two half-stacks side by side
    function pilesLayout(moveOnly){
      if(!moveOnly) cards.forEach(function(c){ reset(c.uid); els[c.uid].classList.add('torn'); });
      for(var i=0;i<4;i++){
        place(els[i],   PILE_LX+i*1.5, PILE_Y+i*1.5, 0, 30-i);
        place(els[4+i], PILE_RX+i*1.5, PILE_Y+i*1.5, 0, 30-i);
      }
      if(!moveOnly) cmpRings();
    }

    // position all cards for a (possibly transient) deck-phase display state
    function positionCards(st, info){
      if(info && info.reveal){
        cards.forEach(function(c){ reset(c.uid); });
        st.discard.forEach(function(u,di){ var el=els[u]; el.classList.add('torn','discarded');
          place(el,(STAGE_W-((st.discard.length-1)*DISC_S+CARD_W))/2+di*DISC_S, DISC_Y, 0, 2+di); });
        dressReveal(els[st.deck[0]], true); dressReveal(els[st.aside], false);
        place(els[st.deck[0]], STAGE_W/2-CARD_W/2, DECK_Y, 0, 5);
        place(els[st.aside],  STAGE_W/2-CARD_W/2, DECK_Y+HALF, 0, 5);
        asideLabel.classList.remove('show');
        discardLabel.classList.toggle('show', st.discard.length>0);
        return;
      }
      var isPos = info && info.ctrl==='pos';
      var m = isPos ? info.m : 0;
      var deck=st.deck, n=deck.length, rest=isPos?deck.slice(m):deck, L=rest.length;
      cards.forEach(function(c){ reset(c.uid); els[c.uid].classList.add('torn'); });
      // lifted block (only in position phases)
      if(isPos){ deck.slice(0,m).forEach(function(u,j){ var el=els[u]; el.classList.add('lifted');
        place(el, deckX(j,m), LIFT_Y, 0, 10+j); }); }
      // the deck row (rest, or whole deck)
      rest.forEach(function(u,i){ place(els[u], deckX(i,L), DECK_Y, 0, 2); });
      // hint outline on the top cards about to move
      if(info && info.hint && !isPos){ for(var i=0;i<info.hint && i<n;i++) els[deck[i]].classList.add('hint'); }
      if(st.aside!=null){ place(els[st.aside], ASIDE_X, ASIDE_Y, 0, 2); }
      st.discard.forEach(function(u,di){ var el=els[u]; el.classList.add('discarded');
        place(el,(STAGE_W-((st.discard.length-1)*DISC_S+CARD_W))/2+di*DISC_S, DISC_Y, 0, 2+di); });
      cmpRings(function(u){ return st.discard.indexOf(u)<0; });
      asideLabel.classList.toggle('show', st.aside!=null);
      discardLabel.classList.toggle('show', st.discard.length>0);
    }

    function drawGaps(st,info){
      gapsLayer.innerHTML='';
      if(!(info && info.ctrl==='pos')) return;
      var m=info.m, L=st.deck.length-m;
      for(var p=1;p<=L-1;p++){                 // interior gaps only = "in the middle"
        var cL=deckX(p-1,L)+CARD_W/2, cR=deckX(p,L)+CARD_W/2, gx=(cL+cR)/2;
        var g=document.createElement('div'); g.className='ctp-gap';
        g.style.left=gx+'px'; g.style.top=(DECK_Y+HALF/2)+'px';
        (function(pp){ g.addEventListener('click',function(){ if(!busy) choosePos(pp); }); })(p);
        gapsLayer.appendChild(g);
      }
    }

    // ---- history + controller ----
    var hist=[], ptr=0, busy=false;
    function curState(){ return hist[ptr]; }
    function push(st){ hist=hist.slice(0,ptr+1); hist.push(st); ptr++; }
    function clone(st){ return {deck:st.deck.slice(),aside:st.aside,discard:st.discard.slice(),phase:st.phase,k7:st.k7}; }

    function render(){
      var st=curState(), info=phaseInfo(st);
      gapsLayer.innerHTML='';
      if(st.phase==='start'||st.phase==='packet'||st.phase==='stack'){
        showPiles(st.phase==='stack');
        if(st.phase==='start') rowWhole(); else if(st.phase==='packet') packetLayout(); else pilesLayout();
        asideLabel.classList.remove('show'); discardLabel.classList.remove('show');
      } else {
        showPiles(false);
        positionCards(st,info); drawGaps(st,info);
      }
      prompt.innerHTML=info.prompt;
      prompt.classList.toggle('choose', !!info.choose);
      prompt.classList.toggle('win', !!info.reveal);
      buildControls(info);
    }
    // transient render during a multi-card animation (no control/gap changes)
    function renderT(st){ positionCards(st,{}); }

    function btn(label,cls,fn){ var b=document.createElement('button'); b.className='ctp-btn'+(cls?' '+cls:'');
      b.innerHTML=label; b.addEventListener('click',function(){ if(!busy) fn(); }); return b; }

    function buildControls(info){
      advance.innerHTML='';
      bBack.disabled = busy || ptr===0;
      bReset.disabled = busy;
      cbHL.disabled = busy;
      if(busy){ advance.appendChild(Object.assign(document.createElement('span'),{className:'ctp-hintmsg',textContent:'…'})); return; }
      if(info.ctrl==='binary'){
        var tray=document.createElement('span'); tray.className='ctp-choicetray';
        tray.appendChild(Object.assign(document.createElement('span'),{className:'ctp-choice-label',textContent:'On top:'}));
        tray.appendChild(btn('Top halves','ctp-choice',function(){ chooseBase(BASE_TOPS); }));
        tray.appendChild(btn('Bottom halves','ctp-choice',function(){ chooseBase(BASE_BOTS); }));
        advance.appendChild(tray);
      } else if(info.ctrl==='count'){
        var t=document.createElement('span'); t.className='ctp-choicetray';
        t.appendChild(Object.assign(document.createElement('span'),{className:'ctp-choice-label',textContent:'Pick a number:'}));
        [1,2,3].forEach(function(k){ t.appendChild(btn(String(k),'ctp-choice',function(){ chooseCount(info.which,k); })); });
        advance.appendChild(t);
      } else if(info.ctrl==='pos'){
        advance.appendChild(Object.assign(document.createElement('span'),{className:'ctp-hintmsg',textContent:'↑ click a gap to drop the lifted card'+(info.m>1?'s':'')}));
      } else if(info.ctrl==='deal'){
        advance.appendChild(btn('Deal one ▶','ctp-primary',dealOne));
      } else if(info.ctrl==='reveal'){
        advance.appendChild(btn('Reveal ✨','ctp-primary',doReveal));
      } else if(info.ctrl==='end'){
        advance.appendChild(btn('↻ Play again','ctp-primary',function(){ start(); }));
      } else { // next
        advance.appendChild(btn(info.btn||'Next ▶','ctp-primary',doNext));
      }
    }

    // ---- actions ----
    function start(){ hist=[{deck:BASE_TOPS.slice(),aside:null,discard:[],phase:'start',k7:null}]; ptr=0; busy=false; render(); }
    function back(){ if(!busy && ptr>0){ ptr--; render(); } }

    function doNext(){
      var info=phaseInfo(curState());
      if(info.act==='gather') return gatherAnim();
      if(info.act==='tear') return tearAnim();
      if(info.act==='rot1') return animateRot(1,'cut2');
      if(info.act==='rot2') return animateRot(2,'block5');
      if(info.act==='rot7') return animateRot(7,'under');
      if(info.act==='setaside') return setAside();
    }

    // start -> packet: the four cards slide together and square up
    function gatherAnim(){
      var st=clone(curState()); busy=true; buildControls(phaseInfo(st));
      packetLayout(true);
      setTimeout(function(){ var ns=clone(st); ns.phase='packet'; push(ns); busy=false; render(); },500);
    }

    // packet -> stack: strain, rip apart (jagged clip animates in), settle into two half-stacks
    function tearAnim(){
      var st=clone(curState()); busy=true; buildControls(phaseInfo(st));
      cards.forEach(function(c){ var el=els[c.uid], f=el.querySelector('.ctp-face');
        el.classList.remove('tearing'); void f.offsetWidth; el.classList.add('tearing'); });
      setTimeout(function(){
        cards.forEach(function(c){ var el=els[c.uid], f=el.querySelector('.ctp-face');
          el.classList.remove('tearing');
          f.style.transitionDelay=((c.uid%4)*55)+'ms';   // the rip propagates through the packet
          el.classList.add('torn'); });
        for(var i=0;i<4;i++){
          place(els[i],   138+i*1.5,  40+i*1.5, -8, 30-i);   // top halves pull up-left
          place(els[4+i], 260+i*1.5, 154+i*1.5,  6, 30-i);   // bottom halves pull down-right
        }
        setTimeout(function(){
          cards.forEach(function(c){ els[c.uid].querySelector('.ctp-face').style.transitionDelay=''; });
          pilesLayout(true); showPiles(true);                 // square up the two half-stacks
          setTimeout(function(){
            var ns=clone(st); ns.phase='stack'; push(ns); busy=false; render();
          },430);
        },600);
      },430);
    }

    // stack -> cut1: lift the chosen half-stack, drop it on the other, fan into a row
    function chooseBase(base){
      var st=clone(curState()); busy=true; buildControls(phaseInfo(st));
      var chosen=base.slice(0,4), topsChosen=(chosen[0]===0);
      var fromX = topsChosen?PILE_LX:PILE_RX, toX = topsChosen?PILE_RX:PILE_LX;
      chosen.forEach(function(u,i){ place(els[u], fromX+i*1.5, PILE_Y-32+i*1.5, 0, 40-i); });
      setTimeout(function(){
        showPiles(false);
        base.forEach(function(u,j){ place(els[u], toX+j*1.2, PILE_Y-4+j*1.2, 0, 40-j); });
        setTimeout(function(){
          base.forEach(function(u,j){ place(els[u], deckX(j,8), DECK_Y, 0, 2); });
          setTimeout(function(){
            var ns=clone(st); ns.deck=base.slice(); ns.phase='cut1'; push(ns); busy=false; render();
          },500);
        },480);
      },300);
    }

    function animateRot(k,nextPhase){
      var st=clone(curState()); busy=true; buildControls(phaseInfo(st));
      var deck=st.deck.slice(), i=0;
      (function one(){
        if(i>=k){ var ns=clone(st); ns.deck=deck; ns.phase=nextPhase; push(ns); busy=false; render(); return; }
        deck=rot(deck,1); i++; renderT({deck:deck,aside:st.aside,discard:st.discard,phase:st.phase,k7:st.k7});
        setTimeout(one,STEP_MS);
      })();
    }
    function setAside(){
      var st=clone(curState()); var c=st.deck[0];
      var ns=clone(st); ns.aside=c; ns.deck=st.deck.slice(1); ns.phase='take7c'; push(ns); render();
    }
    function chooseCount(which,k){
      var st=clone(curState());
      if(which===7){ var ns=clone(st); ns.k7=k; ns.phase='take7p'; push(ns); render(); }
      else { var d=st.deck.slice(0,k); var ns2=clone(st); ns2.deck=st.deck.slice(k); ns2.discard=st.discard.concat(d); ns2.phase='cut7'; push(ns2); render(); }
    }
    function choosePos(p){
      var st=clone(curState()), info=phaseInfo(st), m=info.m;
      var ns=clone(st); ns.deck=toMiddle(st.deck,m,p);
      ns.phase = st.phase==='block5' ? 'aside' : st.phase==='take7p' ? 'top8' : 'disc9';
      push(ns); render();
    }
    function dealOne(){
      var st=clone(curState()); if(st.deck.length<=1) return;
      busy=true; buildControls(phaseInfo(st));
      var under=st.deck.slice(1).concat(st.deck[0]);          // top under to bottom
      renderT({deck:under,aside:st.aside,discard:st.discard,phase:'under',k7:st.k7});
      setTimeout(function(){
        var off=under[0], d2=under.slice(1), disc=st.discard.concat([off]);
        var ns=clone(st); ns.deck=d2; ns.discard=disc; ns.phase = d2.length>1?'under':'reveal';
        push(ns); busy=false; render();
      }, STEP_MS+60);
    }
    function doReveal(){ var ns=clone(curState()); ns.phase='end'; push(ns); render(); }

    bBack.addEventListener('click',back);
    bReset.addEventListener('click',function(){ if(!busy) start(); });
    cbHL.addEventListener('change',function(){ if(!busy) render(); });

    start();
    setTimeout(function(){ stage.classList.remove('ctp-noanim'); },60);

    // responsive
    var wrap=stage.parentNode;
    function fit(){ var s=Math.min(1, wrap.clientWidth/STAGE_W); stage.style.transform='scale('+s+')'; wrap.style.height=(STAGE_H*s)+'px'; }
    window.addEventListener('resize',fit); fit();
  }
  if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',init); else init();
})();
</script>

<p>You can check “highlight comparison card” and run the trick again to visualize how both halves move through the stack.</p>

<h2 id="why-does-the-trick-work">Why does the trick work?</h2>

<p>The moves feel like they introduce randomness, but the trick works for three reasons:</p>

<ol>
  <li>The initial steps put the partner of the comparison card on the bottom</li>
  <li>None of the choices can disturb the bottom card</li>
  <li>The ending deal always picks the bottom card</li>
</ol>

<p>In other words, the trick is just obfuscation around a simple procedure: put the partner of the comparison card on the bottom, then run a deal that always returns the bottom card.</p>

<h3 id="the-initial-steps-put-the-partner-of-the-comparison-card-on-the-bottom">The initial steps put the partner of the comparison card on the bottom</h3>

<ul>
  <li>In the initial stack of half cards, the two halves of each full card – including the comparison card – are exactly 4 positions apart. (The “which half-stack goes on top” choice doesn’t matter at all; it’s a false choice.)</li>
  <li>Steps 3 and 4 (moving 1, then 2 cards from top to bottom) don’t change this structure, and now the two halves of the comparison card are fourth and eighth in the stack.</li>
  <li>Step 5 removes the top 3 cards as a block and buries them somewhere in the middle. Before you decide where to bury those 3 cards, notice that the top and bottom cards of the 5 remaining are <strong>two halves of the same card</strong>.</li>
  <li>Step 6 takes the top card as the comparison card – so its partner is sitting at the bottom of the stack before any of the subsequent choices are made.</li>
</ul>

<h3 id="none-of-the-choices-can-disturb-the-bottom-card">None of the choices can disturb the bottom card</h3>

<ul>
  <li>Steps 7 and 8 insert cards “somewhere in the middle”. Inserting in the middle never changes what’s on the bottom. Note that “somewhere in the middle” has to mean <em>strictly interior</em>: not on the very bottom of the stack, which would disrupt the location of the comparison card’s half.</li>
  <li>Step 9 discards from the top. Discarding from the top can’t affect the bottom.</li>
</ul>

<h3 id="the-ending-deal-always-picks-the-bottom-card">The ending deal always picks the bottom card</h3>

<ul>
  <li>After step 9, you have 4, 5, or 6 cards left (you discarded 1–3 from 7).</li>
  <li>Step 10 deals 7 cards to the bottom.
    <ul>
      <li>If you had 4 cards left, the comparison card is now in the first position.</li>
      <li>If you had 5 cards left, the comparison card is now in the third position.</li>
      <li>If you had 6 cards left, the comparison card is now in the fifth position.</li>
    </ul>
  </li>
  <li>Step 11 asks us to “down-under deal” (one card dealt to the bottom, the next discarded) until one survives.</li>
</ul>

<p>The 7 in Step 10 is not arbitrary: it’s the only choice that makes the bottom card survive for all three possible stack sizes at once. You can test this yourself below: pick any other number of times to cut the top card to the bottom and see which cards are selected at each possible stack size. The ♥ marks each stack’s bottom card.</p>

<!-- "Ending machine" for the torn-card trick: three piles (4, 5, 6 cards) run the ending
     simultaneously — k cuts, then the down-under deal. The ♥ half marks each pile's bottom
     card (the comparison card's partner). Click a cut count to run it; only 7 works for all
     three pile sizes. Self-contained; include this file from a post. -->
<div class="ctd-widget" id="ctd-widget">
  <div class="ctd-stage-wrap">
    <div class="ctd-stage" id="ctd-stage">
      <!-- column labels, verdicts, and card elements injected here -->
    </div>
  </div>

  <div class="ctd-prompt" id="ctd-prompt"></div>

  <div class="ctd-chiprow"><span class="ctd-chiplabel">Cuts:</span><span class="ctd-chips" id="ctd-chips"></span></div>
  <div class="ctd-hint">Choose a number to deal that many cards to the bottom, then run the down-under deal on all three piles at once. The trick uses <strong>7</strong>.</div>
</div>

<style>
.ctd-widget{max-width:560px;margin:24px auto;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#2d2d2d}
.ctd-stage-wrap{width:100%;display:flex;justify-content:center;overflow:hidden}
.ctd-stage{position:relative;flex:none;width:460px;height:272px;transform-origin:top center}
.ctd-card{position:absolute;width:40px;height:29px;transition:transform .38s cubic-bezier(.22,.61,.36,1),opacity .4s ease,filter .4s ease;z-index:2;will-change:transform;filter:drop-shadow(0 2px 2.5px rgba(0,0,0,.28))}
.ctd-noanim .ctd-card{transition:none!important}
.ctd-card .ctd-face{width:100%;height:100%;box-sizing:border-box;position:relative;overflow:hidden;
  background:#3a4a8f;background-image:repeating-linear-gradient(45deg,rgba(255,255,255,.10) 0 4px,transparent 4px 8px),repeating-linear-gradient(-45deg,rgba(255,255,255,.10) 0 4px,transparent 4px 8px);
  border:2px solid #2c3a73;border-bottom:none;border-radius:6px 6px 0 0;
  clip-path:polygon(0 0,100% 0,100% 74%,87% 100%,75% 76%,62% 98%,50% 74%,37% 100%,25% 78%,12% 96%,0 72%)}
.ctd-card.ctd-hrt .ctd-face{background:#fff;border:1px solid #d8a7ad;border-bottom:none;color:#e2293f}
.ctd-card .ctd-heart{position:absolute;left:50%;top:45%;transform:translate(-50%,-50%);font-size:16px;line-height:1}
.ctd-card.discarded{opacity:.4;filter:grayscale(.7) brightness(1.12)}
.ctd-card.lost{opacity:.8}
.ctd-card.lost::before{content:"";position:absolute;inset:-3px;border:2px solid #e2293f;border-radius:8px;z-index:1}
.ctd-card.win::after{content:"";position:absolute;inset:-4px;border:3px solid #2ecc71;border-radius:9px;animation:ctd-glow 1.1s ease infinite;z-index:3;pointer-events:none}
@keyframes ctd-glow{0%,100%{box-shadow:0 0 3px rgba(46,204,113,.5)}50%{box-shadow:0 0 13px rgba(46,204,113,1)}}
.ctd-collabel{position:absolute;transform:translateX(-50%);text-align:center;font-size:12px;line-height:1.3;font-weight:700;color:#2d2d2d;white-space:nowrap}
.ctd-collabel span{display:block;font-size:10px;font-weight:600;color:#9aa0a6;letter-spacing:.04em}
.ctd-verdict{position:absolute;transform:translateX(-50%);text-align:center;font-size:12px;font-weight:600;color:#555;white-space:nowrap}
.ctd-verdict b{font-size:15px;margin-right:3px}
.ctd-verdict b.ok{color:#2ecc71}
.ctd-verdict b.bad{color:#e2293f}
.ctd-prompt{min-height:2.6em;background:#f6f7f9;border-left:4px solid #3a4a8f;border-radius:0 6px 6px 0;padding:10px 14px;margin:6px 0 12px;font-size:15px;line-height:1.5;transition:background .25s ease,border-color .25s ease}
.ctd-prompt.win{background:#eafaf0;border-left-color:#2ecc71;font-weight:500}
.ctd-prompt.fail{background:#fdeef0;border-left-color:#e2293f}
.ctd-chiprow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.ctd-chiplabel{font-size:13px;font-weight:600;color:#555}
.ctd-chips{display:inline-flex;gap:5px;flex-wrap:wrap}
.ctd-chip{min-width:28px;padding:6px 0;border-radius:6px;border:1.5px solid #d0d5dd;background:#fff;font-size:13px;font-weight:600;cursor:pointer;transition:all .15s ease}
.ctd-chip:hover:not(:disabled){border-color:#e6932f}
.ctd-chip:disabled{opacity:.5;cursor:not-allowed}
.ctd-chip.ok{background:#eafaf0;border-color:#2ecc71;color:#1a7a43}
.ctd-chip.bad{background:#fdeef0;border-color:#e2a4ad;color:#a5192c}
.ctd-chip.sel{background:#e6932f;border-color:#e6932f;color:#fff}
.ctd-chip.sel.ok{background:#2ecc71;border-color:#2ecc71;color:#fff}
.ctd-chip.sel.bad{background:#e2293f;border-color:#e2293f;color:#fff}
.ctd-hint{font-size:12.5px;color:#6b7280;margin-top:8px;line-height:1.5}
</style>

<script>
(function(){
  function init(){
    var W=document.getElementById('ctd-widget');
    if(!W||W.dataset.ready)return; W.dataset.ready='1';

    var SIZES=[4,5,6], THREW=[3,2,1];
    var STAGE_W=460, STAGE_H=272, CARD_W=40;
    var COL_X=[64,204,344], BASE_Y=206, PITCH=33;   // stacks are bottom-aligned: the ♥ never moves until touched
    var DISC_DX=48, DISC_Y0=68, DISC_PITCH=13;
    var STEP=270;

    var stage=document.getElementById('ctd-stage'), prompt=document.getElementById('ctd-prompt'),
        chipsEl=document.getElementById('ctd-chips');
    stage.classList.add('ctd-noanim');

    var els={}, verd=[];
    SIZES.forEach(function(n,ci){
      var h=document.createElement('div'); h.className='ctd-collabel';
      h.style.left=(COL_X[ci]+CARD_W/2)+'px'; h.style.top='4px';
      h.innerHTML=n+' cards<span>threw '+THREW[ci]+' away</span>';
      stage.appendChild(h);
      var v=document.createElement('div'); v.className='ctd-verdict';
      v.style.left=(COL_X[ci]+CARD_W/2)+'px'; v.style.top=(BASE_Y+36)+'px';
      stage.appendChild(v); verd.push(v);
      for(var i=0;i<n;i++){
        var el=document.createElement('div'); el.className='ctd-card'+(i===n-1?' ctd-hrt':'');
        var f=document.createElement('div'); f.className='ctd-face';
        if(i===n-1) f.innerHTML='<span class="ctd-heart">♥</span>';
        el.appendChild(f); stage.appendChild(el); els[ci+':'+i]=el;
      }
    });

    var decks, discards, busy=false, runId=0, sel=null, results={};
    function rot(s){ return s.slice(1).concat([s[0]]); }
    function freshState(){
      decks=SIZES.map(function(n){ var a=[]; for(var i=0;i<n;i++)a.push(i); return a; });
      discards=SIZES.map(function(){ return []; });
    }
    function place(el,x,y,z){ el.style.transform='translate('+x+'px,'+y+'px)'; el.style.zIndex=z; }
    function render(movers){
      movers=movers||[];
      SIZES.forEach(function(n,ci){
        decks[ci].forEach(function(idx,i){
          var el=els[ci+':'+idx]; el.classList.remove('discarded');
          var y=BASE_Y-(decks[ci].length-1-i)*PITCH;
          place(el, COL_X[ci], y, movers.indexOf(ci+':'+idx)>-1?30:2);
        });
        discards[ci].forEach(function(idx,di){
          var el=els[ci+':'+idx]; el.classList.add('discarded');
          place(el, COL_X[ci]+DISC_DX, DISC_Y0+di*DISC_PITCH, 2+di);
        });
      });
    }
    function caption(html,cls){ prompt.innerHTML=html; prompt.className='ctd-prompt'+(cls?' '+cls:''); }

    function buildChips(){
      chipsEl.innerHTML='';
      for(var k=0;k<=12;k++){
        (function(k){
          var b=document.createElement('button'); b.className='ctd-chip'; b.textContent=k;
          if(k===sel) b.classList.add('sel');
          if(results[k]===true) b.classList.add('ok');
          if(results[k]===false) b.classList.add('bad');
          b.disabled=busy;
          b.addEventListener('click',function(){ if(!busy) run(k); });
          chipsEl.appendChild(b);
        })(k);
      }
    }

    function run(k){
      runId++; var id=runId; sel=k; busy=true;
      Object.keys(els).forEach(function(key){ els[key].classList.remove('win','lost'); });
      verd.forEach(function(v){ v.innerHTML=''; });
      freshState(); render(); buildChips();
      //caption(k===0?'No cuts — straight into the down-under deal.'
      //             :'Cutting: top card to the bottom, '+k+' time'+(k>1?'s':'')+', in all three piles at once.');
      var cutsDone=0, dealUnder=true;
      function step(){
        if(id!==runId) return;
        if(cutsDone<k){
          cutsDone++;
          var movers=decks.map(function(d,ci){ return ci+':'+d[0]; });
          decks=decks.map(rot); render(movers);
          caption('Cut '+cutsDone+' of '+k+': top card to the bottom.');
          setTimeout(step,STEP); return;
        }
        if(decks.some(function(d){ return d.length>1; })){
          if(dealUnder){
            var movers=[];
            decks=decks.map(function(d,ci){ if(d.length>1){ movers.push(ci+':'+d[0]); return rot(d); } return d; });
            render(movers);
            caption('Down-under deal: one card under, the next dealt off — until one is left in each pile.');
          } else {
            decks.forEach(function(d,ci){ if(d.length>1){ discards[ci].push(d.shift()); } });
            render();
          }
          dealUnder=!dealUnder;
          setTimeout(step,STEP+40); return;
        }
        finish(k);
      }
      setTimeout(step,420);
    }

    function finish(k){
      var fails=[];
      SIZES.forEach(function(n,ci){
        var s=decks[ci][0], ok=(s===n-1);
        if(ok){ els[ci+':'+s].classList.add('win'); }
        else { els[ci+':'+(n-1)].classList.add('lost'); fails.push(n); }
        verd[ci].innerHTML= ok ? '<b class="ok">✓</b>♥ survived' : '<b class="bad">✗</b>♥ dealt off';
      });
      results[k]=(fails.length===0); busy=false; buildChips();
      if(fails.length===0){
        caption('<strong>'+k+' cuts:</strong> the bottom card survives in all three piles. 🎉','win');
      } else {
        var where = fails.length===3 ? 'in all three piles' : 'in the '+fails.join('- and ')+'-card pile'+(fails.length>1?'s':'');
        caption('<strong>'+k+' cuts:</strong> the ♥ gets dealt off '+where+' — the ending breaks.','fail');
      }
    }

    freshState(); render(); buildChips();
    caption('Each stack\'s bottom card — the <strong style="color:#e2293f">♥</strong> — is the partner of the comparison card. How many cuts before the deal?');
    setTimeout(function(){ stage.classList.remove('ctd-noanim'); },60);

    // responsive
    var wrap=stage.parentNode;
    function fit(){ var s=Math.min(1, wrap.clientWidth/STAGE_W); stage.style.transform='scale('+s+')'; wrap.style.height=(STAGE_H*s)+'px'; }
    window.addEventListener('resize',fit); fit();
  }
  if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',init); else init();
})();
</script>

<p>Only 7 cuts works to choose the bottom card regardless of whether 4, 5, or 6 cards remained after step 9. That does explain <em>how</em> the ending works, but it doesn’t explain <em>why</em> the ending works. To understand that, we can notice that the ending of the trick is a known puzzle called the <em>Josephus problem</em>.</p>

<h3 id="the-josephus-problem">The Josephus problem</h3>

<p>Imagine that the stack of remaining cards after step 9 was laid out in a circle. Before worrying about the deal, let’s just think about the cuts in step 10: each individual cut keeps the same order but changes which card is the “top”.</p>

<!-- Bridge diagram between the stack view (card-trick-deal) and the ring view (card-trick-ring):
     just the 6-card pile, no down-under deal. Left = the stack; right = the same deck bent into
     a ring. Picking a number of cuts reorders the stack (top card to the bottom, one at a time)
     while on the ring nothing moves at all — only the starting pointer rotates one card clockwise
     per cut. The point is to make the equivalence obvious before introducing the Josephus problem.
     Self-contained; include this file from a post. -->
<div class="ctc-widget" id="ctc-widget">
  <div class="ctc-panels" id="ctc-panels">
    <div class="ctc-panel ctc-stackpanel">
      <div class="ctc-panellabel">6 cards<span>as a stack</span></div>
      <div class="ctc-stage" id="ctc-stack"></div>
    </div>
    <div class="ctc-panel ctc-ringpanel">
      <div class="ctc-panellabel">6 cards<span>as a ring</span></div>
      <div class="ctc-stage" id="ctc-ring"></div>
    </div>
  </div>

  <div class="ctc-prompt" id="ctc-prompt"></div>

  <div class="ctc-chiprow">
    <span class="ctc-chiplabel">Cuts:</span><span class="ctc-chips" id="ctc-chips"></span>
    <button class="ctc-btn" id="ctc-reset">↻ Reset</button>
  </div>
</div>

<style>
.ctc-widget{max-width:560px;margin:24px auto;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#2d2d2d}
.ctc-panels{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:6px;margin-bottom:6px}
.ctc-panel{flex:none;text-align:center}
.ctc-panellabel{text-align:center;font-size:12px;line-height:1.3;font-weight:700;color:#2d2d2d;white-space:nowrap;margin-bottom:4px}
.ctc-panellabel span{display:block;font-size:10px;font-weight:600;color:#9aa0a6;letter-spacing:.04em}
.ctc-arrow{flex:none;font-size:26px;color:#9aa0a6;font-weight:700;padding:0 2px;align-self:center}
.ctc-stage{position:relative;flex:none}
.ctc-stackpanel .ctc-stage{width:150px;height:210px}
.ctc-ringpanel .ctc-stage{width:178px;height:210px}
.ctc-card{position:absolute;width:40px;height:29px;z-index:2;filter:drop-shadow(0 2px 2.5px rgba(0,0,0,.28))}
.ctc-stack-card{transition:transform .38s cubic-bezier(.22,.61,.36,1);will-change:transform}
.ctc-noanim .ctc-stack-card{transition:none!important}
.ctc-card .ctc-face{width:100%;height:100%;box-sizing:border-box;position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center;
  background:#3a4a8f;background-image:repeating-linear-gradient(45deg,rgba(255,255,255,.10) 0 4px,transparent 4px 8px),repeating-linear-gradient(-45deg,rgba(255,255,255,.10) 0 4px,transparent 4px 8px);
  border:2px solid #2c3a73;border-bottom:none;border-radius:6px 6px 0 0;
  clip-path:polygon(0 0,100% 0,100% 74%,87% 100%,75% 76%,62% 98%,50% 74%,37% 100%,25% 78%,12% 96%,0 72%)}
.ctc-card.ctc-hrt .ctc-face{background:#fff;border:1px solid #d8a7ad;border-bottom:none;color:#e2293f}
.ctc-num{font-size:11px;font-weight:800;color:#fff;background:rgba(0,0,0,.28);border-radius:8px;padding:0 5px;line-height:1.4;margin-top:-4px}
.ctc-hrt .ctc-heart{font-size:15px;line-height:1;margin-top:-4px}
.ctc-card.hi::after{content:"";position:absolute;inset:-3px;border:3px solid #e6932f;border-radius:8px;z-index:4;pointer-events:none;box-shadow:0 0 8px rgba(230,147,47,.6)}
.ctc-marker{position:absolute;width:16px;height:16px;font-size:14px;line-height:16px;text-align:center;color:#e6932f;z-index:5;
  transition:transform .32s cubic-bezier(.22,.61,.36,1);will-change:transform;text-shadow:0 1px 2px rgba(0,0,0,.25)}
.ctc-marker.ctc-mnoanim{transition:none!important}
.ctc-slabel{position:absolute;left:100px;transform:translateY(-50%);font-size:10px;font-weight:700;color:#e6932f;white-space:nowrap}
.ctc-slabel small{display:block;font-size:8.5px;font-weight:600;color:#9aa0a6}
.ctc-prompt{min-height:2.6em;background:#f6f7f9;border-left:4px solid #3a4a8f;border-radius:0 6px 6px 0;padding:10px 14px;margin:6px 0 12px;font-size:15px;line-height:1.5;transition:background .25s ease,border-color .25s ease}
.ctc-chiprow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.ctc-chiplabel{font-size:13px;font-weight:600;color:#555}
.ctc-chips{display:inline-flex;gap:5px;flex-wrap:wrap}
.ctc-chip{min-width:28px;padding:6px 0;border-radius:6px;border:1.5px solid #d0d5dd;background:#fff;font-size:13px;font-weight:600;cursor:pointer;transition:all .15s ease}
.ctc-chip:hover:not(:disabled){border-color:#e6932f}
.ctc-chip:disabled{opacity:.5;cursor:not-allowed}
.ctc-chip.sel{background:#e6932f;border-color:#e6932f;color:#fff}
.ctc-btn{background:#fff;border:1.5px solid #d0d5dd;border-radius:6px;padding:6px 12px;font-size:13px;font-weight:500;color:#6b7280;cursor:pointer;transition:all .15s ease}
.ctc-btn:hover:not(:disabled){border-color:#3a4a8f;background:#eef1fb}
.ctc-btn:disabled{opacity:.4;cursor:not-allowed}
</style>

<script>
(function(){
  function init(){
    var W=document.getElementById('ctc-widget');
    if(!W||W.dataset.ready)return; W.dataset.ready='1';

    var N=6;
    // stack geometry
    var SX=55, SBASE_Y=176, SPITCH=26;
    // ring geometry
    var CX=89, CY=105, R=56, MR=82, CARD_W=40, CARD_H=29;

    var stackStage=document.getElementById('ctc-stack'),
        ringStage=document.getElementById('ctc-ring'),
        prompt=document.getElementById('ctc-prompt'),
        chipsEl=document.getElementById('ctc-chips'),
        resetBtn=document.getElementById('ctc-reset');
    stackStage.classList.add('ctc-noanim');

    function label(i){ return i===N-1 ? '<span class="ctc-heart">♥</span>' : '<span class="ctc-num">'+(i+1)+'</span>'; }
    function makeCard(i,cls){
      var el=document.createElement('div'); el.className='ctc-card '+cls+(i===N-1?' ctc-hrt':'');
      var f=document.createElement('div'); f.className='ctc-face'; f.innerHTML=label(i);
      el.appendChild(f); return el;
    }

    // build stack cards (index -> element)
    var stackEls=[];
    for(var i=0;i<N;i++){ var el=makeCard(i,'ctc-stack-card'); stackStage.appendChild(el); stackEls.push(el); }
    // "top" flag label on the stack
    var slabel=document.createElement('div'); slabel.className='ctc-slabel';
    slabel.innerHTML='▶ deals from<small>the top</small>'; stackStage.appendChild(slabel);

    // build ring cards + marker
    var ringEls=[];
    function angleOf(i){ return -90 + i*360/N; }
    function placeRingCard(el,i){
      var a=angleOf(i)*Math.PI/180;
      el.style.transform='translate('+(CX+R*Math.cos(a)-CARD_W/2)+'px,'+(CY+R*Math.sin(a)-CARD_H/2)+'px)';
    }
    for(var j=0;j<N;j++){ var re=makeCard(j,''); ringStage.appendChild(re); placeRingCard(re,j); ringEls.push(re); }
    var marker=document.createElement('div'); marker.className='ctc-marker'; marker.textContent='▶'; ringStage.appendChild(marker);

    var cumAng=-90, markerIdx=0;
    function placeMarker(){
      var a=cumAng*Math.PI/180;
      marker.style.transform='translate('+(CX+MR*Math.cos(a)-8)+'px,'+(CY+MR*Math.sin(a)-8)+'px) rotate('+(cumAng+180)+'deg)';
    }

    var deck, busy=false, runId=0, sel=0;
    function rot(a){ return a.slice(1).concat([a[0]]); }
    function freshDeck(){ deck=[]; for(var i=0;i<N;i++) deck.push(i); }

    function renderStack(movers){
      movers=movers||[];
      deck.forEach(function(idx,i){
        var el=stackEls[idx];
        var y=SBASE_Y-(deck.length-1-i)*SPITCH;
        el.style.transform='translate('+SX+'px,'+y+'px)';
        el.style.zIndex=movers.indexOf(idx)>-1?30:2;
        el.classList.toggle('hi', i===0);
      });
      // pin the "deals from the top" label beside the current top card
      slabel.style.top=(SBASE_Y-(deck.length-1)*SPITCH+CARD_H/2)+'px';
    }
    function highlightRing(){
      ringEls.forEach(function(el,i){ el.classList.toggle('hi', i===markerIdx); });
    }
    function cardName(i){ return i===N-1 ? 'the ♥' : 'card '+(i+1); }
    function capsCardName(i){ return i===N-1 ? 'The ♥' : 'Card '+(i+1); }

    function caption(html){ prompt.innerHTML=html; }
    function idleCaption(){
      caption('Cutting a card from the top to the bottom changes the top card but doesn\'t reorder the stack. Choose a number of cuts.');
    }

    function buildChips(){
      resetBtn.disabled=busy;
      chipsEl.innerHTML='';
      for(var k=0;k<=N;k++){
        (function(k){
          var b=document.createElement('button'); b.className='ctc-chip'+(k===sel?' sel':''); b.textContent=k;
          b.disabled=busy;
          b.addEventListener('click',function(){ if(!busy) run(k); });
          chipsEl.appendChild(b);
        })(k);
      }
    }

    function reset(){
      runId++; sel=0; busy=false;
      freshDeck();
      cumAng=-90; markerIdx=0;
      marker.classList.add('ctc-mnoanim'); placeMarker(); void marker.offsetWidth; marker.classList.remove('ctc-mnoanim');
      renderStack(); highlightRing(); buildChips(); idleCaption();
    }

    function run(k){
      runId++; var id=runId; sel=k; busy=true;
      // start from the un-cut state so every run is comparable
      freshDeck();
      markerIdx=0; cumAng=-90;
      marker.classList.add('ctc-mnoanim'); placeMarker(); void marker.offsetWidth; marker.classList.remove('ctc-mnoanim');
      renderStack(); highlightRing(); buildChips();
      if(k===0){ busy=false; buildChips();
        caption('<strong>0 cuts.</strong> The pointer sits on card 1, the top of the stack.'); return; }
      var done=0;
      function step(){
        if(id!==runId) return;
        if(done>=k){
          busy=false; buildChips();
          var top=deck[0];
          caption('<strong>'+k+' cut'+(k>1?'s':'')+'.</strong> '+capsCardName(top)+' is now the top of the stack — and the ring pointer landed on '
            +cardName(top)+'. '+(k===N?'Right back where we started.':''));
          return;
        }
        done++;
        // stack: top card slides to the bottom
        var mover=deck[0]; deck=rot(deck); renderStack([mover]);
        // ring: marker hops one card clockwise; cards stay put
        markerIdx=(markerIdx+1)%N; cumAng+=360/N; placeMarker(); highlightRing();
        caption('Cut '+done+' of '+k+': top card to the bottom = one step around the ring.');
        setTimeout(step,440);
      }
      setTimeout(step,360);
    }

    resetBtn.addEventListener('click',function(){ if(!busy) reset(); });

    reset();
    setTimeout(function(){ stackStage.classList.remove('ctc-noanim'); },60);
  }
  if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',init); else init();
})();
</script>

<p>The number of cuts only changes which card is the top card, which we keep track of in the ring using an orange ▶ pointer. 
In the 6-card stack, 6 cuts moves the pointer marking the top of the stack all the way around the ring and back to your starting point, leaving the stack completely unchanged.</p>

<p>In the actual trick, we do 7 cuts, which for the 6-card stack is equivalent to 6 cuts followed by 1 cut. We already know 6 cuts leaves the stack unchanged, so 7 cuts must be equivalent to just doing 1 cut.
We can figure out the “actual” number of cuts by dividing the total number of cuts by the number of cards in the stack and looking only at the <em>remainder</em>.</p>

<p>If I tell you to cut the 6-card stack 36 times, you could recognize that (# of cuts / # of cards) = 36 / 6 = 6 with a remainder of 0, so you don’t need to do any cuts! 36 cuts is equivalent to doing 0 cuts, 6 cuts, 12 cuts, etc. (The mathematical way to describe this equivalence is to use <a href="https://en.wikipedia.org/wiki/Modular_arithmetic">modular arithmetic</a>, writing 36 ≡ 0 (mod 6).)</p>

<p>Ultimately, that means the number of cuts is just a way of choosing a new top card: the selected card in the ring.</p>

<p>Why is it useful to think about the problem this way? Because the repeated down-under deal in step 11 is the <a href="https://en.wikipedia.org/wiki/Josephus_problem">Josephus problem</a>.
Given a ring of a particular size and starting from a particular point in that ring, the Josephus problem involves discarding and sparing every other card until we have one card remaining.</p>

<p>Here’s the Josephus procedure applied to a 6-card stack.</p>

<!-- The Josephus procedure on a single fixed six-card ring. Cards 1..6 run clockwise from a
     fixed starting pointer (▶). One button runs the down-under deal: spare a card (it goes
     "under", staying on the ring), then deal off the next, repeating until one card survives —
     here always card 5. No cuts, no heart, no prose: this exists purely to show the Josephus
     elimination for a fixed ring size and starting point. Self-contained; include from a post. -->
<div class="ctj-widget" id="ctj-widget">
  <div class="ctj-ring" id="ctj-ring"></div>
  <div class="ctj-controls">
    <button class="ctj-btn ctj-primary" id="ctj-play">Run the deal ▶</button>
  </div>
</div>

<style>
.ctj-widget{max-width:560px;margin:24px auto;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#2d2d2d}
.ctj-ring{position:relative;width:200px;height:210px;margin:0 auto 10px}
.ctj-card{position:absolute;width:40px;height:29px;transition:opacity .4s ease,filter .4s ease;z-index:2;filter:drop-shadow(0 2px 2.5px rgba(0,0,0,.28))}
.ctj-card .ctj-face{width:100%;height:100%;box-sizing:border-box;position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center;
  background:#3a4a8f;background-image:repeating-linear-gradient(45deg,rgba(255,255,255,.10) 0 4px,transparent 4px 8px),repeating-linear-gradient(-45deg,rgba(255,255,255,.10) 0 4px,transparent 4px 8px);
  border:2px solid #2c3a73;border-bottom:none;border-radius:6px 6px 0 0;
  clip-path:polygon(0 0,100% 0,100% 74%,87% 100%,75% 76%,62% 98%,50% 74%,37% 100%,25% 78%,12% 96%,0 72%)}
.ctj-num{font-size:11px;font-weight:800;color:#fff;background:rgba(0,0,0,.28);border-radius:8px;padding:0 5px;line-height:1.4;margin-top:-4px}
.ctj-card.dead{opacity:.45;filter:grayscale(.7) brightness(1.1)}
.ctj-kb{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;color:#a5192c;z-index:3;text-shadow:0 0 3px #fff,0 0 3px #fff;margin-top:-3px}
.ctj-card.skip::before{content:"";position:absolute;inset:-3px;border:2px solid #3a4a8f;border-radius:8px;z-index:1;animation:ctj-blip .5s ease}
@keyframes ctj-blip{0%{box-shadow:0 0 0 3px rgba(58,74,143,.4)}100%{box-shadow:0 0 0 0 rgba(58,74,143,0)}}
.ctj-card.win::after{content:"";position:absolute;inset:-4px;border:3px solid #2ecc71;border-radius:9px;animation:ctj-glow 1.1s ease infinite;z-index:3;pointer-events:none}
@keyframes ctj-glow{0%,100%{box-shadow:0 0 3px rgba(46,204,113,.5)}50%{box-shadow:0 0 13px rgba(46,204,113,1)}}
.ctj-marker{position:absolute;width:16px;height:16px;font-size:14px;line-height:16px;text-align:center;color:#e6932f;z-index:5;
  transition:transform .34s cubic-bezier(.22,.61,.36,1);will-change:transform;text-shadow:0 1px 2px rgba(0,0,0,.25)}
.ctj-marker.ctj-mnoanim{transition:none!important}
.ctj-controls{display:flex;align-items:center;justify-content:center;gap:10px}
.ctj-btn{background:#fff;border:1.5px solid #d0d5dd;border-radius:6px;padding:8px 14px;font-size:14px;cursor:pointer;transition:all .15s ease;font-weight:500}
.ctj-btn:hover:not(:disabled){border-color:#3a4a8f;background:#eef1fb}
.ctj-btn:disabled{opacity:.4;cursor:not-allowed}
.ctj-btn.ctj-primary{background:#3a4a8f;color:#fff;border-color:#3a4a8f}
.ctj-btn.ctj-primary:hover:not(:disabled){background:#2f3d78}
</style>

<script>
(function(){
  function init(){
    var W=document.getElementById('ctj-widget');
    if(!W||W.dataset.ready)return; W.dataset.ready='1';

    var N=6, START=0;
    var CX=100, CY=105, R=64, MR=90, CARD_W=40, CARD_H=29;

    var ringEl=document.getElementById('ctj-ring'), bPlay=document.getElementById('ctj-play');

    function angleOf(i){ return -90 + i*360/N; }
    function normAng(a){ return ((a%360)+360)%360; }
    function placeCard(el,i){
      var a=angleOf(i)*Math.PI/180;
      el.style.transform='translate('+(CX+R*Math.cos(a)-CARD_W/2)+'px,'+(CY+R*Math.sin(a)-CARD_H/2)+'px)';
    }

    var els=[], alive=[], markerIdx=0, cumAng=-90, killed=0, busy=false, runId=0;
    for(var i=0;i<N;i++){
      var el=document.createElement('div'); el.className='ctj-card';
      var f=document.createElement('div'); f.className='ctj-face';
      f.innerHTML='<span class="ctj-num">'+(i+1)+'</span>';
      el.appendChild(f); ringEl.appendChild(el); placeCard(el,i); els.push(el);
    }
    var marker=document.createElement('div'); marker.className='ctj-marker'; marker.textContent='▶'; ringEl.appendChild(marker);

    function placeMarker(){
      var a=cumAng*Math.PI/180;
      marker.style.transform='translate('+(CX+MR*Math.cos(a)-8)+'px,'+(CY+MR*Math.sin(a)-8)+'px) rotate('+(cumAng+180)+'deg)';
    }
    function setMarkerInstant(idx){
      markerIdx=idx; cumAng=angleOf(idx);
      marker.classList.add('ctj-mnoanim'); placeMarker();
      void marker.offsetWidth; marker.classList.remove('ctj-mnoanim');
    }
    function nextAlive(i){ do{ i=(i+1)%N; }while(!alive[i]); return i; }
    function advanceMarker(){
      var delta=normAng(normAng(angleOf(markerIdx))-normAng(cumAng));
      cumAng+=delta; placeMarker();
    }
    function aliveCount(){ return alive.filter(Boolean).length; }

    function reset(){
      alive=[]; for(var i=0;i<N;i++) alive.push(true);
      killed=0;
      els.forEach(function(el){ el.classList.remove('dead','skip','win');
        var kb=el.querySelector('.ctj-kb'); if(kb) kb.remove(); });
      setMarkerInstant(START);
    }

    function play(){
      if(busy) return;
      runId++; var id=runId; busy=true; bPlay.disabled=true;
      reset();
      var skip=true;
      (function tick(){
        if(id!==runId) return;
        if(aliveCount()<=1){
          var s=alive.indexOf(true);
          els[s].classList.add('win');
          busy=false; bPlay.disabled=false; return;
        }
        if(skip){
          var el=els[markerIdx];
          el.classList.remove('skip'); void el.offsetWidth; el.classList.add('skip');
          markerIdx=nextAlive(markerIdx); advanceMarker();
        } else {
          alive[markerIdx]=false; killed++;
          var dead=els[markerIdx]; dead.classList.add('dead');
          var kb=document.createElement('div'); kb.className='ctj-kb'; kb.textContent='✗'+killed; dead.appendChild(kb);
          if(aliveCount()>1){ markerIdx=nextAlive(markerIdx); advanceMarker(); }
        }
        skip=!skip; setTimeout(tick,560);
      })();
    }

    bPlay.addEventListener('click',play);
    reset();
  }
  if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',init); else init();
})();
</script>

<p>Notice that the 5th card is always the remaining card. In fact, the Josephus problem is solved, and it has a nice formula (see the <a href="https://en.wikipedia.org/wiki/Josephus_problem">Wikipedia page</a>). We can look up which card survives relative to the starting card:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Ring size</th>
      <th style="text-align: center">Surviving card</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">4</td>
      <td style="text-align: center">1st</td>
    </tr>
    <tr>
      <td style="text-align: center">5</td>
      <td style="text-align: center">3rd</td>
    </tr>
    <tr>
      <td style="text-align: center">6</td>
      <td style="text-align: center">5th</td>
    </tr>
  </tbody>
</table>

<p>Because the Josephus procedure always spares the same card position relative to the top of the stack, we just need to first do a number of cuts such that the comparison card ends up as the 1st card in the 4-card stack, the 3rd card in the 5-card stack, or the 5th card in the 6-card stack.
As we’ve already seen, this is exactly what 7 total cuts accomplishes.</p>

<ul>
  <li>In the 6-card stack, 7 cuts is equivalent to 1 cut, moving the comparison card from the bottom to the 5th position.</li>
  <li>In the 5-card stack, 7 cuts is equivalent to 2 cuts, moving the comparison card from the bottom to the 3rd position.</li>
  <li>In the 4-card stack, 7 cuts is equivalent to 3 cuts, moving the comparison card from the bottom to the 1st position.</li>
</ul>

<p>We can now run the ending from differing numbers of cuts, confirming that 7 cuts is the only option to move the comparison card to the appropriate position in the stack at all three possible stack sizes.</p>

<!-- Josephus-circle view of the torn-card trick's ending: all three pile sizes as rings,
     side by side (flex-wrap on mobile). Cards 1..n clockwise = the deck from top to bottom,
     the ♥ (card n) is the bottom card. Cuts visibly change nothing but where each dealer
     starts — the modular arithmetic sits in the center of each ring. The deal walks each
     circle eliminating every second card. Self-contained; include this file from a post. -->
<div class="ctr-widget" id="ctr-widget">
  <div class="ctr-rings" id="ctr-rings"></div>

  <div class="ctr-prompt" id="ctr-prompt"></div>

  <div class="ctr-controls">
    <span class="ctr-grp"><span class="ctr-grplabel">Cuts:</span><span class="ctr-chips" id="ctr-chips"></span></span>
    <button class="ctr-btn ctr-primary" id="ctr-play">Run the deal ▶</button>
    <button class="ctr-btn ctr-ghost" id="ctr-reset">↻ Reset</button>
  </div>
</div>

<style>
.ctr-widget{max-width:560px;margin:24px auto;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#2d2d2d}
.ctr-rings{display:flex;flex-wrap:wrap;justify-content:center;gap:4px;margin-bottom:4px}
.ctr-panel{position:relative;width:178px;height:192px;flex:none}
.ctr-center{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);text-align:center;font-size:11px;color:#555;line-height:1.4;pointer-events:none}
.ctr-center b{display:block;font-size:12px;color:#2d2d2d}
.ctr-center i{display:block;font-style:normal;font-weight:700;font-size:13px;margin-top:2px}
.ctr-center i.ok{color:#2ecc71}
.ctr-center i.bad{color:#e2293f}
.ctr-card{position:absolute;width:40px;height:29px;transition:opacity .4s ease,filter .4s ease;z-index:2;filter:drop-shadow(0 2px 2.5px rgba(0,0,0,.28))}
.ctr-card .ctr-face{width:100%;height:100%;box-sizing:border-box;position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center;
  background:#3a4a8f;background-image:repeating-linear-gradient(45deg,rgba(255,255,255,.10) 0 4px,transparent 4px 8px),repeating-linear-gradient(-45deg,rgba(255,255,255,.10) 0 4px,transparent 4px 8px);
  border:2px solid #2c3a73;border-bottom:none;border-radius:6px 6px 0 0;
  clip-path:polygon(0 0,100% 0,100% 74%,87% 100%,75% 76%,62% 98%,50% 74%,37% 100%,25% 78%,12% 96%,0 72%)}
.ctr-card.ctr-hrt .ctr-face{background:#fff;border:1px solid #d8a7ad;border-bottom:none;color:#e2293f}
.ctr-num{font-size:11px;font-weight:800;color:#fff;background:rgba(0,0,0,.28);border-radius:8px;padding:0 5px;line-height:1.4;margin-top:-4px}
.ctr-hrt .ctr-heart{font-size:15px;line-height:1;margin-top:-4px}
.ctr-card.dead{opacity:.45;filter:grayscale(.7) brightness(1.1)}
.ctr-kb{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;color:#a5192c;z-index:3;text-shadow:0 0 3px #fff,0 0 3px #fff;margin-top:-3px}
.ctr-card.skip::before{content:"";position:absolute;inset:-3px;border:2px solid #3a4a8f;border-radius:8px;z-index:1;animation:ctr-blip .5s ease}
@keyframes ctr-blip{0%{box-shadow:0 0 0 3px rgba(58,74,143,.4)}100%{box-shadow:0 0 0 0 rgba(58,74,143,0)}}
.ctr-card.win::after{content:"";position:absolute;inset:-4px;border:3px solid #2ecc71;border-radius:9px;animation:ctr-glow 1.1s ease infinite;z-index:3;pointer-events:none}
.ctr-card.srv::after{content:"";position:absolute;inset:-4px;border:3px solid #9aa0a6;border-radius:9px;z-index:3;pointer-events:none}
@keyframes ctr-glow{0%,100%{box-shadow:0 0 3px rgba(46,204,113,.5)}50%{box-shadow:0 0 13px rgba(46,204,113,1)}}
.ctr-marker{position:absolute;width:16px;height:16px;font-size:14px;line-height:16px;text-align:center;color:#e6932f;z-index:5;
  transition:transform .34s cubic-bezier(.22,.61,.36,1);will-change:transform;text-shadow:0 1px 2px rgba(0,0,0,.25)}
.ctr-marker.ctr-mnoanim{transition:none!important}
.ctr-prompt{min-height:2.6em;background:#f6f7f9;border-left:4px solid #3a4a8f;border-radius:0 6px 6px 0;padding:10px 14px;margin:6px 0 12px;font-size:15px;line-height:1.5;transition:background .25s ease,border-color .25s ease}
.ctr-prompt.win{background:#eafaf0;border-left-color:#2ecc71;font-weight:500}
.ctr-prompt.fail{background:#fdeef0;border-left-color:#e2293f}
.ctr-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px}
.ctr-grp{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}
.ctr-grplabel{font-size:13px;font-weight:600;color:#555}
.ctr-chips{display:inline-flex;gap:5px;flex-wrap:wrap}
.ctr-chip{min-width:26px;padding:5px 0;border-radius:6px;border:1.5px solid #d0d5dd;background:#fff;font-size:12.5px;font-weight:600;cursor:pointer;transition:all .15s ease}
.ctr-chip:hover:not(:disabled){border-color:#e6932f}
.ctr-chip:disabled{opacity:.5;cursor:not-allowed}
.ctr-chip.sel{background:#e6932f;border-color:#e6932f;color:#fff}
.ctr-btn{background:#fff;border:1.5px solid #d0d5dd;border-radius:6px;padding:8px 14px;font-size:14px;cursor:pointer;transition:all .15s ease;font-weight:500}
.ctr-btn:hover:not(:disabled){border-color:#3a4a8f;background:#eef1fb}
.ctr-btn:disabled{opacity:.4;cursor:not-allowed}
.ctr-btn.ctr-primary{background:#3a4a8f;color:#fff;border-color:#3a4a8f}
.ctr-btn.ctr-primary:hover:not(:disabled){background:#2f3d78}
.ctr-btn.ctr-ghost{color:#6b7280}
</style>

<script>
(function(){
  function init(){
    var W=document.getElementById('ctr-widget');
    if(!W||W.dataset.ready)return; W.dataset.ready='1';

    var SIZES=[4,5,6];
    var PW=178, PH=192, CX=89, CY=96, R=54, MR=79;
    var CARD_W=40, CARD_H=29;

    var ringsEl=document.getElementById('ctr-rings'), prompt=document.getElementById('ctr-prompt'),
        chipsEl=document.getElementById('ctr-chips'),
        bPlay=document.getElementById('ctr-play'), bReset=document.getElementById('ctr-reset');

    var k=7, busy=false, runId=0, rings=[];

    function angleOf(rg,i){ return -90 + i*360/rg.n; }
    function normAng(a){ return ((a%360)+360)%360; }
    function placeCard(rg,el,i){
      var a=angleOf(rg,i)*Math.PI/180;
      el.style.transform='translate('+(CX+R*Math.cos(a)-CARD_W/2)+'px,'+(CY+R*Math.sin(a)-CARD_H/2)+'px)';
    }
    function placeMarker(rg){
      var a=rg.cumAng*Math.PI/180;
      rg.marker.style.transform='translate('+(CX+MR*Math.cos(a)-8)+'px,'+(CY+MR*Math.sin(a)-8)+'px) rotate('+(rg.cumAng+180)+'deg)';
    }
    function setMarkerInstant(rg,idx){
      rg.markerIdx=idx; rg.cumAng=angleOf(rg,idx);
      rg.marker.classList.add('ctr-mnoanim'); placeMarker(rg);
      void rg.marker.offsetWidth; rg.marker.classList.remove('ctr-mnoanim');
    }
    function caption(html,cls){ prompt.innerHTML=html; prompt.className='ctr-prompt'+(cls?' '+cls:''); }

    // build the three ring panels
    SIZES.forEach(function(n){
      var panel=document.createElement('div'); panel.className='ctr-panel';
      var center=document.createElement('div'); center.className='ctr-center'; panel.appendChild(center);
      var marker=document.createElement('div'); marker.className='ctr-marker'; marker.textContent='▶'; panel.appendChild(marker);
      var rg={n:n, marker:marker, center:center, els:[], alive:[], markerIdx:0, cumAng:-90, killedCount:0, verdict:null};
      for(var i=0;i<n;i++){
        var el=document.createElement('div'); el.className='ctr-card'+(i===n-1?' ctr-hrt':'');
        var f=document.createElement('div'); f.className='ctr-face';
        f.innerHTML = i===n-1 ? '<span class="ctr-heart">♥</span>' : '<span class="ctr-num">'+(i+1)+'</span>';
        el.appendChild(f); panel.appendChild(el); rg.els.push(el);
        placeCard(rg,el,i);
      }
      ringsEl.appendChild(panel); rings.push(rg);
    });

    function centerText(rg){
      var v = rg.verdict===null ? '' : (rg.verdict ? '<i class="ok">✓</i>' : '<i class="bad">✗</i>');
      rg.center.innerHTML='<b>'+rg.n+' cards</b>'+v;
    }
    function clearRun(rg){
      rg.alive=[]; for(var i=0;i<rg.n;i++) rg.alive.push(true);
      rg.killedCount=0; rg.verdict=null;
      rg.els.forEach(function(el){ el.classList.remove('dead','skip','win','srv');
        var kb=el.querySelector('.ctr-kb'); if(kb) kb.remove(); });
      centerText(rg);
    }
    function nextAlive(rg,i){ do{ i=(i+1)%rg.n; }while(!rg.alive[i]); return i; }
    function advanceMarker(rg){
      var delta=normAng(normAng(angleOf(rg,rg.markerIdx))-normAng(rg.cumAng));
      rg.cumAng+=delta; placeMarker(rg);
    }

    function updateControls(){
      bPlay.disabled=busy; bReset.disabled=busy;
      chipsEl.querySelectorAll('button').forEach(function(b){ b.disabled=busy; });
    }
    function buildChips(){
      chipsEl.innerHTML='';
      for(var kk=0;kk<=12;kk++){
        (function(kk){
          var b=document.createElement('button'); b.className='ctr-chip'+(kk===k?' sel':''); b.textContent=kk;
          b.disabled=busy;
          b.addEventListener('click',function(){ if(!busy) walkCuts(kk); });
          chipsEl.appendChild(b);
        })(kk);
      }
    }
    function modSummary(){
      return SIZES.map(function(n){ return k+' mod '+n+' = '+(k%n); }).join(', &nbsp;');
    }
    function idleCaption(){
      caption('Each possible stack size as a ring. The <strong style="color:#e2293f">♥</strong> always starts at the bottom. '
        +'After <strong>'+k+' cuts</strong> each dealer starts where its marker points.');
    }
    function resetAll(){
      runId++;
      rings.forEach(function(rg){ clearRun(rg); setMarkerInstant(rg, k%rg.n); });
      idleCaption(); updateControls();
    }

    // choosing a cut count: every marker walks in sync — the circles themselves never change
    function walkCuts(kk){
      runId++; var id=runId; k=kk; busy=true;
      rings.forEach(function(rg){ clearRun(rg); setMarkerInstant(rg,0); });
      buildChips(); updateControls();
      var j=0;
      (function hop(){
        if(id!==runId) return;
        if(j>=k){
          busy=false;
          caption('<strong>'+k+' cuts.</strong> The top cards have changed. Now run the deal.');
          updateControls(); return;
        }
        j++;
        rings.forEach(function(rg){ rg.markerIdx=(rg.markerIdx+1)%rg.n; rg.cumAng+=360/rg.n; placeMarker(rg); });
        caption('Cut '+j+' of '+k+': top card to the bottom = one step around each ring.');
        setTimeout(hop,240);
      })();
    }

    // the down-under deal on all three rings at once: skip the current card, deal off the next
    function play(){
      if(busy) return;
      runId++; var id=runId; busy=true; updateControls();
      rings.forEach(function(rg){ clearRun(rg); setMarkerInstant(rg, k%rg.n); });
      var skip=true;
      function aliveCount(rg){ return rg.alive.filter(Boolean).length; }
      function finishRing(rg){
        var s=rg.alive.indexOf(true);
        rg.els[s].classList.add(s===rg.n-1?'win':'srv');
        rg.verdict=(s===rg.n-1); centerText(rg);
      }
      (function tick(){
        if(id!==runId) return;
        var active=rings.filter(function(rg){ return aliveCount(rg)>1; });
        if(!active.length){
          busy=false; updateControls();
          var fails=rings.filter(function(rg){ return !rg.verdict; }).map(function(rg){ return rg.n; });
          if(!fails.length){
            caption('<strong>'+k+' cuts:</strong> every walk lands on the ♥ — the original bottom card survives in all three circles. 🎉','win');
          } else {
            var where = fails.length===3 ? 'all three circles' : 'the '+fails.join('- and ')+'-card circle'+(fails.length>1?'s':'');
            caption('<strong>'+k+' cuts:</strong> the ♥ gets eliminated in '+where+' — the deal selects the wrong card.','fail');
          }
          return;
        }
        active.forEach(function(rg){
          if(skip){
            var el=rg.els[rg.markerIdx];
            el.classList.remove('skip'); void el.offsetWidth; el.classList.add('skip');
            rg.markerIdx=nextAlive(rg,rg.markerIdx); advanceMarker(rg);
          } else {
            rg.alive[rg.markerIdx]=false; rg.killedCount++;
            var dead=rg.els[rg.markerIdx]; dead.classList.add('dead');
            var kb=document.createElement('div'); kb.className='ctr-kb'; kb.textContent='✗'+rg.killedCount; dead.appendChild(kb);
            if(aliveCount(rg)>1){ rg.markerIdx=nextAlive(rg,rg.markerIdx); advanceMarker(rg); }
            else finishRing(rg);
          }
        });
        caption(skip?'<strong>Under:</strong> skip a card. <strong>Off:</strong> discard a card. (Discarding)'
                    :'<strong>Under:</strong> skip a card. <strong>Off:</strong> discard a card. (Skipping)');
        skip=!skip; setTimeout(tick,600);
      })();
    }

    bPlay.addEventListener('click',play);
    bReset.addEventListener('click',function(){ if(!busy) resetAll(); });

    buildChips(); resetAll();
  }
  if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',init); else init();
})();
</script>

<p>(If you’re familiar with modular arithmetic, you’ll note that 7 ≡ 3 (mod 4), 2 (mod 5), and 1 (mod 6) – the right top card for all three stack sizes simultaneously.)</p>

<p>I thought this was a neat trick. I had Claude Opus create the visualizations for me so I could understand the trick better (although I wrote the text in the animations and in the rest of the post). I guess that’s another use of large language models… removing a little magic from the world!</p>

<h2 id="further-reading">Further reading</h2>

<ul>
  <li><a href="https://www.youtube.com/watch?v=uCsD3ZGzMgE">The Josephus Problem</a> by Numberphile (YouTube)</li>
</ul>]]></content><author><name>Zachary Levonian</name></author><category term="math" /><category term="simulation" /><category term="games" /><category term="magic" /><category term="algorithms" /><summary type="html"><![CDATA[Animating a self-working card trick.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://levon003.github.io/images/josephus_card_trick.png" /><media:content medium="image" url="https://levon003.github.io/images/josephus_card_trick.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">My auto-updating Bluesky reading list</title><link href="https://levon003.github.io/2026/06/28/bsky-link-history.html" rel="alternate" type="text/html" title="My auto-updating Bluesky reading list" /><published>2026-06-28T00:00:00+00:00</published><updated>2026-06-28T00:00:00+00:00</updated><id>https://levon003.github.io/2026/06/28/bsky-link-history</id><content type="html" xml:base="https://levon003.github.io/2026/06/28/bsky-link-history.html"><![CDATA[<p>I had Claude Opus 4.8 vibe-code a nightly CI job to maintain a <a href="/bsky-links/">list of every link I’ve liked on Bluesky</a>.
My goal here was to make a reading list I can consult and search even if people later delete their posts, and I also wanted to capture additional metadata for articles and papers by using <a href="https://www.mediawiki.org/wiki/Citoid">Citoid</a>.</p>

<p><img src="/images/bsky_reading_list_summary.png" alt="Most-liked domains and Bluesky accounts" style="display:block; height: auto; max-width: 50%; margin-left: auto; margin-right: auto;" />
<em>My most-liked domains and Bluesky accounts, as of June 2026. Updated nightly on <a href="/bsky-links/">the list</a>.</em></p>

<p>Claude’s solution was a few Python scripts:</p>

<ol>
  <li>Scrape liked posts using <a href="https://atproto.blue/en/latest/atproto/atproto_client.models.app.bsky.feed.get_actor_likes.html"><code class="language-plaintext highlighter-rouge">getActorLikes</code></a>. To capture “new paper” threads better, I capture any links in the whole thread, not just the specific post I liked. I also capture links in quote posts.</li>
  <li>Use Wikimedia’s <a href="https://www.mediawiki.org/wiki/Citoid">Citoid</a> service to attempt to get better title and author metadata for each link. Citoid fails quite regularly, so we “fall back” to the title and description in Bluesky’s preview card, or just display the actual URL if no preview card was created. For example, we fail to get the title and author from links like <a href="https://users.eecs.northwestern.edu/~jhullman/AI_metascience_position.pdf">https://users.eecs.northwestern.edu/~jhullman/AI_metascience_position.pdf</a>, which is <a href="https://github.com/zotero/translation-server/issues/70">not supported</a> by the Zotero translation server that Citoid uses. A PDF metadata-parsing library could be used to try to infer title, author, and date from PDFs like that.</li>
  <li>Check for dead links, attaching a <a href="https://web.archive.org/">Wayback Machine</a> link if the link seems dead (404, 410, SSL error, connection refused, or DNS failure).</li>
</ol>

<p>The output is a <a href="https://github.com/levon003/levon003.github.io/blob/main/_data/bsky_likes.yml">YAML file</a> versioned in this blog’s public GitHub repository, which a Jekyll template consumes as site data. GitHub Actions runs nightly to refresh the YAML file.</p>

<p>Claude was great at iteratively identifying issues and fixing them, and there are a number of special cases. The most prominent of these issues is something that’s been annoying me on Wikipedia lately too: a lot of the academic publishers have started loading an “are you human” page before directing you on to the article page, which means Citoid can’t resolve metadata correctly. The most prominent examples are papers hosted on the ACM Digital Library or in the Open Science Framework’s PsyArXiv pre-print repository. Fortunately, the URLs for those two sites contain enough information to extract a DOI from the URL, which enables us to recover in most cases. But it requires a special case, so there’s a good chance that Citoid will just stop working in the future for more and more publisher pages.</p>

<p>If you want to do something similar, it’s very easy to set up: you’ll just need to generate an <a href="https://bsky.app/settings/app-passwords">App Password</a>. Find the code <a href="https://github.com/levon003/levon003.github.io/tree/main/src/bsky_link_history">on GitHub</a>, but again, this was vibe-coded: I literally didn’t read the code at all.</p>

<p>(I’ll note that I still don’t use LLMs for writing… although Claude did generate the URL slug for this post and produced a summary text that I used to write up the post myself. I’m not quite willing to give up my writing yet!)</p>]]></content><author><name>Zachary Levonian</name></author><category term="bluesky" /><category term="data" /><category term="llm" /><category term="short" /><summary type="html"><![CDATA[A vibe-coded pipeline to extract a reading list from my like history on Bluesky.]]></summary></entry><entry><title type="html">Tacoma’s pre-approved street trees</title><link href="https://levon003.github.io/2026/06/14/tacoma-street-trees.html" rel="alternate" type="text/html" title="Tacoma’s pre-approved street trees" /><published>2026-06-14T00:00:00+00:00</published><updated>2026-06-14T00:00:00+00:00</updated><id>https://levon003.github.io/2026/06/14/tacoma-street-trees</id><content type="html" xml:base="https://levon003.github.io/2026/06/14/tacoma-street-trees.html"><![CDATA[<p>If you want to plant a tree in the Right-of-Way <a href="https://en.wikipedia.org/wiki/Road_verge">planting strip</a> between
the sidewalk and the street in Tacoma, Washington, you need to get a planting permit. See <a href="https://tacoma.gov/government/departments/environmental-services/urban-forestry/trees-in-the-right-of-way/">“Trees in the Right-of-Way”</a> on the City of Tacoma website.</p>

<p>The city maintains lists of <em>pre-approved</em> species you can choose from that serve as recommendations and that will make review easier.
Unfortunately, those lists live in a few random PDFs.
I wanted to get an easier sense of what these options are, so I had Claude Opus 4.8 build the table below, which is searchable and sortable.
This table is based on the 2025 revision of the pre-approved tree lists, so it may be out-of-date if you’re looking at this in 2027 or later.</p>

<p>You can also download the extracted and structured data as JSON <a href="/files/tacoma_trees/trees.json">here</a>.</p>

<h2 id="pre-approved-trees">Pre-approved trees</h2>

<!-- Sortable / filterable table of Tacoma's preapproved street trees.
     Data is extracted from the city PDFs by files/tacoma_trees/extract_trees.py
     into files/tacoma_trees/trees.json, which this widget fetches at runtime.
     Self-contained vanilla JS; include from a post with the Jekyll include tag. -->
<div class="tt-widget" id="tt-widget">
  <div class="tt-controls">
    <input type="search" id="tt-search" class="tt-search" placeholder="Search name or genus… (e.g. maple, Acer, dogwood)" aria-label="Search trees" />

    <div class="tt-filtergroup" id="tt-size" data-key="size_class">
      <span class="tt-filterlabel">Size</span>
      <button class="tt-pill" data-val="small">Small</button>
      <button class="tt-pill" data-val="medium">Medium</button>
      <button class="tt-pill" data-val="large">Large</button>
      <button class="tt-pill" data-val="fruit">Fruit</button>
    </div>

    <div class="tt-filtergroup" id="tt-traits">
      <span class="tt-filterlabel">Traits</span>
      <button class="tt-pill" data-trait="evergreen">🌲 Evergreen</button>
      <button class="tt-pill" data-trait="deciduous">🍂 Deciduous</button>
      <button class="tt-pill" data-trait="ornamental">✿ Ornamental</button>
      <button class="tt-pill" data-trait="utility_friendly">⚡ Okay under power lines</button>
      <button class="tt-pill" data-trait="native">🏔 Native</button>
      <button class="tt-pill" data-trait="food_producing">🍎 Fruit</button>
    </div>

    <button class="tt-clear" id="tt-clear">Clear filters</button>
  </div>

  <div class="tt-count" id="tt-count"></div>

  <div class="tt-scroll">
    <table class="tt-table" id="tt-table">
      <thead>
        <tr id="tt-head"></tr>
      </thead>
      <tbody id="tt-body"></tbody>
    </table>
  </div>
  <p class="tt-note" id="tt-status">Loading tree data…</p>

  <details class="tt-credits" id="tt-credits" hidden="">
    <summary>Image credits</summary>
    <p class="tt-note">Thumbnails are from Wikimedia Commons, each linked to its source. Credited below by photographer and license.</p>
    <ul id="tt-credits-body"></ul>
  </details>
</div>

<style>
.tt-widget{margin:24px 0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#2d2d2d;font-size:14px}
.tt-controls{display:flex;flex-wrap:wrap;gap:10px 16px;align-items:center;margin-bottom:10px}
.tt-search{flex:1 1 240px;min-width:200px;padding:8px 12px;border:1.5px solid #d0d5dd;border-radius:8px;font-size:14px}
.tt-search:focus{outline:none;border-color:#5e7ce0;box-shadow:0 0 0 3px rgba(94,124,224,.15)}
.tt-filtergroup{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.tt-filterlabel{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:#9aa0a6;font-weight:600;margin-right:2px}
.tt-pill{background:#fff;border:1.5px solid #d0d5dd;border-radius:999px;padding:5px 11px;font-size:13px;cursor:pointer;transition:all .12s ease;white-space:nowrap;color:#444}
.tt-pill:hover{border-color:#5e7ce0;background:#f3f6ff}
.tt-pill.on{background:#5e7ce0;border-color:#5e7ce0;color:#fff}
.tt-clear{background:none;border:none;color:#5e7ce0;cursor:pointer;font-size:13px;text-decoration:underline;padding:4px}
.tt-clear:hover{color:#4a6ad6}
.tt-count{font-size:12px;color:#9aa0a6;margin-bottom:6px}
.tt-scroll{overflow-x:auto;border:1px solid #ececf0;border-radius:8px}
.tt-table{border-collapse:collapse;width:100%;min-width:680px}
.tt-table th,.tt-table td{padding:8px 10px;text-align:left;border-bottom:1px solid #f0f0f3;vertical-align:top}
.tt-table thead th{position:sticky;top:0;background:#fbfbfc;border-bottom:2px solid #e4e4ea;font-size:12px;text-transform:uppercase;letter-spacing:.03em;color:#6b7280;cursor:pointer;user-select:none;white-space:nowrap}
.tt-table thead th:hover{color:#5e7ce0}
.tt-table thead th .tt-arrow{opacity:.35;font-size:10px}
.tt-table thead th.tt-sorted .tt-arrow{opacity:1;color:#5e7ce0}
.tt-table tbody tr:hover{background:#f7f9ff}
.tt-num{text-align:right;font-variant-numeric:tabular-nums}
.tt-namecell{display:flex;gap:9px;align-items:flex-start}
.tt-thumb{flex:0 0 auto;display:block;width:48px;height:60px}
.tt-thumb img{width:48px;height:60px;object-fit:cover;border-radius:4px;background:#f0f0f3;display:block}
.tt-thumb-empty{background:#f3f4f6;border-radius:4px}
.tt-nametext{min-width:0}
.tt-name{font-weight:600;color:#2d2d2d}
.tt-name a{color:inherit;text-decoration:none}
.tt-name a:hover{text-decoration:underline}
.tt-credits{margin-top:16px;font-size:13px}
.tt-credits summary{cursor:pointer;color:#5e7ce0;font-weight:500}
.tt-credits ul{margin:8px 0 0;padding-left:20px;color:#6b7280;font-size:11.5px;line-height:1.7}
.tt-credits a{color:#5e7ce0;text-decoration:none}
.tt-credits a:hover{text-decoration:underline}
.tt-sci{font-style:italic;color:#6b7280;font-size:12.5px}
.tt-cv{color:#9aa0a6;font-size:11.5px;margin-top:2px}
.tt-cv-rest{display:none}
.tt-cv.open .tt-cv-rest{display:inline}
.tt-cv-toggle{color:#5e7ce0;text-decoration:none;white-space:nowrap}
.tt-cv-toggle:hover{text-decoration:underline}
.tt-cv.open .tt-cv-toggle{margin-left:3px}
.tt-badge{display:inline-block;padding:2px 7px;border-radius:5px;font-size:11px;font-weight:600;text-transform:capitalize}
.tt-sz-small{background:#e7f5ec;color:#2f8a4e}
.tt-sz-medium{background:#fff4e0;color:#b7791f}
.tt-sz-large{background:#fde8e8;color:#c0392b}
.tt-sz-fruit{background:#f0e9fb;color:#7c4dca}
.tt-traits{font-size:15px;white-space:nowrap;line-height:1.3}
.tt-traits span{cursor:default}
.tt-links a{display:block;color:#5e7ce0;text-decoration:none;font-size:12px;white-space:nowrap;line-height:1.7}
.tt-links a:hover{text-decoration:underline}
.tt-note{font-size:12px;color:#9aa0a6;margin-top:8px}
</style>

<script>
(function(){
  var widget=document.getElementById('tt-widget');
  if(!widget||widget.dataset.ready)return; widget.dataset.ready='1';

  var DATA_URL="/files/tacoma_trees/trees.json";
  var THUMBS_URL="/files/tacoma_trees/thumbnails.json";
  var THUMB_BASE="/files/tacoma_trees/thumbs/";
  var THUMBS={};
  var GROWTH={1:'Slow',2:'Medium',3:'Fast'};

  // column definitions: key, label, optional class, value accessor, sort accessor
  var COLS=[
    {key:'name',  label:'Tree',   cls:'',            sort:function(r){return r.common_name.toLowerCase();}},
    {key:'size',  label:'Size',   cls:'',            sort:function(r){return ['small','medium','large','fruit'].indexOf(r.size_class);}},
    {key:'height',label:'Ht (ft)',cls:'tt-num',                sort:function(r){return r.height_ft;}},
    {key:'width', label:'W (ft)', cls:'tt-num tt-col-width',   sort:function(r){return r.width_ft;}},
    {key:'growth',label:'Growth', cls:'',            sort:function(r){return r.growth_rate;}},
    {key:'canopy',label:'Canopy', cls:'tt-num tt-col-canopy',  sort:function(r){return r.canopy_factor==null?-1:r.canopy_factor;}},
    {key:'strip', label:'Min strip (ft)',cls:'tt-num tt-col-strip',sort:function(r){return r.min_strip_ft;}},
    {key:'traits',label:'Traits', cls:'',            sort:function(r){return -((r.evergreen?8:0)+(r.ornamental?4:0)+(r.utility_friendly?2:0)+(r.native?1:0));}},
    {key:'links', label:'Look up',cls:'',            sort:null}
  ];

  var rows=[], sortKey='name', sortDir=1;
  var state={text:'', sizes:new Set(), traits:new Set()};

  var elBody=document.getElementById('tt-body'),
      elHead=document.getElementById('tt-head'),
      elCount=document.getElementById('tt-count'),
      elStatus=document.getElementById('tt-status'),
      elSearch=document.getElementById('tt-search');

  function esc(s){return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
  function cap(s){return s?s.charAt(0).toUpperCase()+s.slice(1):s;}
  function q(r){return encodeURIComponent((r.genus+' '+r.species).replace(/[^\w .×-]/g,'').trim());}

  function linksHtml(r){
    var term=q(r);
    return '<span class="tt-links">'+
      '<a href="https://en.wikipedia.org/w/index.php?search='+term+'" target="_blank" rel="noopener">Wikipedia</a>'+
      '<a href="https://commons.wikimedia.org/w/index.php?search='+term+'&title=Special:MediaSearch&type=image" target="_blank" rel="noopener">Commons Photos</a>'+
      '<a href="https://www.google.com/search?tbm=isch&q='+term+'" target="_blank" rel="noopener">Google Images</a>'+
    '</span>';
  }

  function thumbHtml(r){
    var t=THUMBS[r.genus+' '+r.species];
    if(!t)return '<span class="tt-thumb tt-thumb-empty"></span>';
    return '<a class="tt-thumb" href="'+t.source+'" target="_blank" rel="noopener" title="'+esc(t.artist+' · '+t.license)+'">'+
      '<img src="'+THUMB_BASE+t.file+'" loading="lazy" decoding="async" width="48" height="60" alt="'+esc(cap(r.common_name))+'"></a>';
  }

  function renderCredits(){
    var keys=Object.keys(THUMBS); if(!keys.length)return;
    var nameFor={};
    rows.forEach(function(r){var k=r.genus+' '+r.species; if(!nameFor[k])nameFor[k]=cap(r.common_name);});
    keys.sort(function(a,b){return (nameFor[a]||a).localeCompare(nameFor[b]||b);});
    document.getElementById('tt-credits-body').innerHTML=keys.map(function(k){
      var t=THUMBS[k];
      return '<li><a href="'+t.source+'" target="_blank" rel="noopener">'+esc(nameFor[k]||k)+'</a> '+
        '<em>('+esc(k)+')</em> — '+esc(t.artist)+', '+esc(t.license)+'</li>';
    }).join('');
    document.getElementById('tt-credits').hidden=false;
  }

  function cvHtml(cv){
    if(!cv)return '';
    var parts=cv.split(/,\s*/);
    if(parts.length<=3)return '<div class="tt-cv">cv. '+esc(cv)+'</div>';
    return '<div class="tt-cv">cv. '+esc(parts.slice(0,3).join(', '))+
      '<span class="tt-cv-rest">, '+esc(parts.slice(3).join(', '))+'</span>'+
      ' <a href="#" class="tt-cv-toggle" data-n="'+(parts.length-3)+'">+'+(parts.length-3)+' more</a>'+
    '</div>';
  }

  function traitsHtml(r){
    var t=[];
    if(r.evergreen)t.push('<span title="Evergreen">🌲</span>');
    else if(!r.food_producing)t.push('<span title="Deciduous">🍂</span>');
    if(r.ornamental)t.push('<span title="Ornamental">✿</span>');
    if(r.utility_friendly)t.push('<span title="Overhead-utility friendly">⚡</span>');
    if(r.native)t.push('<span title="West Coast native">🏔</span>');
    if(r.food_producing)t.push('<span title="Food-producing">🍎</span>');
    return '<div class="tt-traits">'+(t.join(' ')||'—')+'</div>';
  }

  function cell(col,r){
    switch(col.key){
      case 'name': return '<div class="tt-namecell">'+thumbHtml(r)+
                          '<div class="tt-nametext">'+
                          '<div class="tt-name"><a href="https://en.wikipedia.org/w/index.php?search='+q(r)+'" target="_blank" rel="noopener">'+esc(cap(r.common_name))+'</a></div>'+
                          '<div class="tt-sci">'+esc((r.genus+' '+r.species).trim())+'</div>'+
                          cvHtml(r.cultivars)+'</div></div>';
      case 'size': return '<span class="tt-badge tt-sz-'+r.size_class+'">'+r.size_class+'</span>';
      case 'height':return r.height_label||r.height_ft;
      case 'width': return r.width_label||r.width_ft;
      case 'growth':return GROWTH[r.growth_rate]||'—';
      case 'canopy':return r.canopy_factor==null?'—':r.canopy_factor;
      case 'strip': return r.min_strip_ft==null?'—':r.min_strip_ft;
      case 'traits':return traitsHtml(r);
      case 'links': return linksHtml(r);
    }
  }

  function passes(r){
    if(state.sizes.size && !state.sizes.has(r.size_class))return false;
    for(var t of state.traits){
      if(t==='deciduous'){ if(r.evergreen||r.food_producing)return false; }
      else if(!r[t])return false;
    }
    if(state.text){
      var hay=(r.common_name+' '+r.genus+' '+r.species+' '+r.cultivars).toLowerCase();
      if(hay.indexOf(state.text)===-1)return false;
    }
    return true;
  }

  function render(){
    var col=COLS.find(function(c){return c.key===sortKey;});
    var view=rows.filter(passes);
    if(col&&col.sort){
      view.sort(function(a,b){
        var x=col.sort(a),y=col.sort(b);
        if(x==null)x=-Infinity; if(y==null)y=-Infinity;
        if(x<y)return -1*sortDir; if(x>y)return 1*sortDir;
        return a.common_name.localeCompare(b.common_name);
      });
    }
    elBody.innerHTML=view.map(function(r){
      return '<tr>'+COLS.map(function(c){
        return '<td class="'+c.cls+'">'+cell(c,r)+'</td>';
      }).join('')+'</tr>';
    }).join('');
    elCount.textContent=view.length+' of '+rows.length+' trees';
  }

  function buildHead(){
    elHead.innerHTML=COLS.map(function(c){
      var arrow=c.sort?'<span class="tt-arrow"> ↕</span>':'';
      var sorted=c.key===sortKey?' tt-sorted':'';
      if(c.key===sortKey)arrow='<span class="tt-arrow"> '+(sortDir>0?'▲':'▼')+'</span>';
      return '<th class="'+c.cls+sorted+'" data-key="'+c.key+'"'+(c.sort?'':' style="cursor:default"')+'>'+c.label+arrow+'</th>';
    }).join('');
    elHead.querySelectorAll('th').forEach(function(th){
      var key=th.dataset.key, col=COLS.find(function(c){return c.key===key;});
      if(!col||!col.sort)return;
      th.onclick=function(){
        if(sortKey===key)sortDir*=-1; else {sortKey=key;sortDir=1;}
        buildHead(); render();
      };
    });
  }

  // wire filters
  elSearch.oninput=function(){state.text=elSearch.value.trim().toLowerCase();render();};
  document.querySelectorAll('#tt-size .tt-pill').forEach(function(b){
    b.onclick=function(){ b.classList.toggle('on');
      b.classList.contains('on')?state.sizes.add(b.dataset.val):state.sizes.delete(b.dataset.val); render(); };
  });
  document.querySelectorAll('#tt-traits .tt-pill').forEach(function(b){
    b.onclick=function(){ b.classList.toggle('on');
      b.classList.contains('on')?state.traits.add(b.dataset.trait):state.traits.delete(b.dataset.trait); render(); };
  });
  document.getElementById('tt-clear').onclick=function(){
    state={text:'',sizes:new Set(),traits:new Set()}; elSearch.value='';
    widget.querySelectorAll('.tt-pill.on').forEach(function(b){b.classList.remove('on');}); render();
  };
  // expand/collapse long cultivar lists (delegated, survives re-render)
  elBody.addEventListener('click',function(e){
    var a=e.target.closest('.tt-cv-toggle'); if(!a)return;
    e.preventDefault();
    var box=a.parentNode, open=box.classList.toggle('open');
    a.textContent=open?'less':'+'+a.dataset.n+' more';
  });

  Promise.all([
    fetch(DATA_URL).then(function(r){return r.json();}),
    fetch(THUMBS_URL).then(function(r){return r.json();}).catch(function(){return {};})
  ]).then(function(res){
    rows=res[0]; THUMBS=res[1];
    elStatus.style.display='none'; buildHead(); render(); renderCredits();
  }).catch(function(e){
    elStatus.textContent='Could not load tree data ('+e+').';
  });
})();
</script>

<h2 id="notes">Notes</h2>

<ul>
  <li><strong>Data is from January 2025.</strong> The data were extracted from the PDFs on the City of Tacoma’s website, last revised January 30, 2025.</li>
  <li><strong>Data tweaks.</strong> The data has a few quirks — a misspelled species, an evergreen icon on a tree you’d call deciduous, etc. I worked with Claude to fix these; see the full list of corrections below.</li>
  <li><strong>Definitions.</strong> <em>Growth rate</em> and <em>Canopy factor</em> are defined as follows (see <a href="https://ecode360.com/48470096">13.01.060.C</a>):
    <blockquote>
      <p>A method of calculating tree size by taking into account the tree’s mature height, mature crown spread and growth rate. The Canopy Factor is calculated using the following formula: (mature height in feet) x (mature crown spread in feet) x (growth rate number) x 0.01 = Canopy Factor. The growth rate number is 1 for slow growing trees, 2 for moderately growing trees, and 3 for fast growing trees.</p>

      <p>•Large Trees = Canopy Factor greater than 70,</p>

      <p>•Medium Trees = Canopy Factor from 40 to 70,</p>

      <p>•Small Trees = Canopy Factor less than 40.</p>
    </blockquote>
  </li>
  <li><strong>Prohibited trees.</strong> There’s also a separate list of not recommended and prohibited trees, which is not included here. At the time of writing, you can find that list <a href="https://tacoma.gov/wp-content/uploads/2025/09/Appendix-7D-Prohibited-Tree-List_2025_Update.pdf">here</a>.</li>
</ul>

<details>
<summary>Full list of tweaks Claude and I made to the city's data</summary>
<p>Data corrections:</p>
<ul>
<li><strong>Misspelled scientific names:</strong> <em>Chamaecyparis obstusa</em> → <em>obtusa</em>; <em>Gingko biloba</em> → <em>Ginkgo biloba</em>; <em>Eucalyptus gunni</em> → <em>gunnii</em>; <em>Cladastris kentukea</em> → <em>Cladrastis kentukea</em>; <em>Tillia tomentosa</em> → <em>Tilia tomentosa</em>; <em>Ulmus parviflora</em> → <em>parvifolia</em>; <em>Ulmus caprinifolia × parvifloia</em> → <em>carpinifolia × parvifolia</em>; <em>Cornus kousa × nuttalii</em> → <em>nuttallii</em>.</li>
<li><strong>Naming and taxonomy:</strong> <em>Crataegus crus-galli var. inermis</em> is corrected to <em>Crataegus crus-galli</em> 'Inermis' — the thornless cockspur hawthorn, where "inermis" is a cultivar rather than a botanical variety; and <em>Cupressus bakeri</em> is updated to its currently accepted name, <em>Hesperocyparis bakeri</em>.</li>
<li><strong>Reclassified as evergreen:</strong> Four broadleaf evergreens were marked with a deciduous icon in the source: <em>Eucalyptus neglecta</em> (omeo gum), <em>Eucalyptus parvula</em> (small-leaved gum), <em>Quercus ilex</em> (holm oak), and <em>Quercus myrsinifolia</em> (bamboo-leaf oak).</li>
<li><strong>Untangled columns:</strong> The two <em>Syringa pekinensis</em> rows had the cultivar code (<em>'Morton'</em>, <em>DTR 124'</em>) sitting in the common-name slot and the trade name in the cultivars slot; I swapped them back. A footnote asterisk was stripped from three large trees (Douglas-fir, coastal redwood, ponderosa pine).</li>
<li><strong>Fruit tree canopy factor:</strong> The fruit list gives sizes as ranges (e.g. "6–12"); the canopy factor for these is computed from the formula above using the upper end of the range. In addition, the fruit sheet doesn't carry the utility-friendly flag, but I believe all of the dwarf fruit trees are allowed under power lines — so I've marked them as such, with the exception of the fig (<em>Ficus carica</em>) which I'm not sure about.</li>
</ul>
</details>

<p><br />
If you spot any additional errors, please do let me know!</p>]]></content><author><name>Zachary Levonian</name></author><category term="tacoma" /><category term="trees" /><category term="data" /><category term="short" /><summary type="html"><![CDATA[The City of Tacoma, Washington's preapproved street-tree list in one table.]]></summary></entry><entry><title type="html">Research paper: Undergraduate usage of generative AI in spring 2026</title><link href="https://levon003.github.io/2026/06/13/henricksen-gallagher-genai-survey.html" rel="alternate" type="text/html" title="Research paper: Undergraduate usage of generative AI in spring 2026" /><published>2026-06-13T00:00:00+00:00</published><updated>2026-06-13T00:00:00+00:00</updated><id>https://levon003.github.io/2026/06/13/henricksen-gallagher-genai-survey</id><content type="html" xml:base="https://levon003.github.io/2026/06/13/henricksen-gallagher-genai-survey.html"><![CDATA[<p>Eleanor Henricksen and John Gallagher have just released a pre-print on SSRN: <a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=6869641">“Students still think they write better than GenAI, but not equally: Disciplinary differences in usage, trust, and writing practices with artificial intelligence”</a></p>

<p>The paper summarizes responses to a spring 2026 survey on student generative AI use. The specific university where the survey was administered was omitted from the pre-print during review, but I presume it was administered at UIUC or a similar institution.</p>

<p>I was most interested by their question about generative AI usage frequency: 84% of students report using generative AI at least once per week. The survey results also show that self-reported usage frequency differs substantially depending on a student’s academic discipline. (A “discipline” is just a grouping of related majors; the paper contains full lists of the associated majors.)</p>

<p><img src="/images/henricksen_genai_usage_by_discipline.svg" alt="Frequency of GenAI usage by discipline, from Henricksen &amp; Gallagher (2026)" style="display:block; margin-left: auto; margin-right: auto;" /></p>

<p>The usual caveats about small-<em>n</em> subgroup comparison apply here, but the most interesting standout is how different Humanities &amp; Arts students’ self-reported usage is compared to all other students. 
I wonder how much of this is explained by <a href="https://en.wikipedia.org/wiki/Expressive_responding">expressive responding</a> or by greater normative expectations in the humanities around avoiding LLM use.</p>

<p>Here’s the raw data:</p>

<table>
  <thead>
    <tr>
      <th>Discipline</th>
      <th><em>n</em></th>
      <th>Never</th>
      <th>Once a week</th>
      <th>2–3×/week</th>
      <th>4–6×/week</th>
      <th>Daily</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Business &amp; Finance</td>
      <td>38</td>
      <td>0% (0)</td>
      <td>5% (2)</td>
      <td>18% (7)</td>
      <td>32% (12)</td>
      <td>45% (17)</td>
    </tr>
    <tr>
      <td>Engineering</td>
      <td>84</td>
      <td>6% (5)</td>
      <td>10% (8)</td>
      <td>24% (20)</td>
      <td>24% (20)</td>
      <td>37% (31)</td>
    </tr>
    <tr>
      <td>CS / Data / Info Science</td>
      <td>20</td>
      <td>10% (2)</td>
      <td>10% (2)</td>
      <td>20% (4)</td>
      <td>40% (8)</td>
      <td>20% (4)</td>
    </tr>
    <tr>
      <td>Education</td>
      <td>17</td>
      <td>12% (2)</td>
      <td>41% (7)</td>
      <td>18% (3)</td>
      <td>29% (5)</td>
      <td>0% (0)</td>
    </tr>
    <tr>
      <td>Physical &amp; Env. Sciences</td>
      <td>10</td>
      <td>10% (1)</td>
      <td>10% (1)</td>
      <td>40% (4)</td>
      <td>20% (2)</td>
      <td>20% (2)</td>
    </tr>
    <tr>
      <td>Life Sciences &amp; Health</td>
      <td>28</td>
      <td>4% (1)</td>
      <td>7% (2)</td>
      <td>46% (13)</td>
      <td>25% (7)</td>
      <td>18% (5)</td>
    </tr>
    <tr>
      <td>Undeclared / Exploratory</td>
      <td>25</td>
      <td>12% (3)</td>
      <td>24% (6)</td>
      <td>24% (6)</td>
      <td>20% (5)</td>
      <td>20% (5)</td>
    </tr>
    <tr>
      <td>Social Sciences</td>
      <td>25</td>
      <td>16% (4)</td>
      <td>20% (5)</td>
      <td>36% (9)</td>
      <td>16% (4)</td>
      <td>12% (3)</td>
    </tr>
    <tr>
      <td>Agriculture &amp; ACES</td>
      <td>23</td>
      <td>17% (4)</td>
      <td>17% (4)</td>
      <td>30% (7)</td>
      <td>9% (2)</td>
      <td>26% (6)</td>
    </tr>
    <tr>
      <td>Humanities &amp; Arts</td>
      <td>59</td>
      <td>49% (29)</td>
      <td>17% (10)</td>
      <td>19% (11)</td>
      <td>8% (5)</td>
      <td>7% (4)</td>
    </tr>
    <tr>
      <td><strong>All</strong></td>
      <td><strong>329</strong></td>
      <td><strong>16% (51)</strong></td>
      <td><strong>14% (47)</strong></td>
      <td><strong>26% (84)</strong></td>
      <td><strong>21% (70)</strong></td>
      <td><strong>23% (77)</strong></td>
    </tr>
  </tbody>
</table>

<p>There are several other interesting questions in the survey, so feel free to check out the paper for more:</p>

<blockquote>
  <p>Henricksen, Eleanor and Gallagher, John, <em>Students still think they write better than GenAI, but not equally: Disciplinary differences in usage, trust, and writing practices with artificial intelligence</em>. Available at SSRN: <a href="https://ssrn.com/abstract=6869641">https://ssrn.com/abstract=6869641</a> or <a href="http://dx.doi.org/10.2139/ssrn.6869641">http://dx.doi.org/10.2139/ssrn.6869641</a>.</p>
</blockquote>]]></content><author><name>Zachary Levonian</name></author><category term="research" /><category term="education" /><category term="llm" /><category term="short" /><summary type="html"><![CDATA[A survey of how undergrad students use, trust, and write with GenAI.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://levon003.github.io/images/henricksen_genai_usage_by_discipline.png" /><media:content medium="image" url="https://levon003.github.io/images/henricksen_genai_usage_by_discipline.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Research paper: Efficacy of Social Belonging and Growth Mindset interventions among college students</title><link href="https://levon003.github.io/2026/06/02/nber-growth-mindset.html" rel="alternate" type="text/html" title="Research paper: Efficacy of Social Belonging and Growth Mindset interventions among college students" /><published>2026-06-02T00:00:00+00:00</published><updated>2026-06-02T00:00:00+00:00</updated><id>https://levon003.github.io/2026/06/02/nber-growth-mindset</id><content type="html" xml:base="https://levon003.github.io/2026/06/02/nber-growth-mindset.html"><![CDATA[<p>A new NBER working paper finds null results for <a href="https://en.wikipedia.org/wiki/Belongingness">social belonging</a> and <a href="https://en.wikipedia.org/wiki/Mindset#Fixed_and_growth_mindsets">growth mindset</a> interventions for first-year college students.
The paper is <a href="https://www.nber.org/papers/w35230">“The (Lack of) Efficacy of Social Belonging and Growth Mindset Interventions Among College Students”</a> by Abid Alam, Philip Oreopoulos, and Uros Petronijevic. Here’s the abstract:</p>

<blockquote>
  <p>Using four large-scale experiments across two major Canadian universities, we experimentally evaluate the effects of growth mindset and social belonging interventions on student outcomes. In a sample of nearly 12,000 students, we find no immediate or dynamic effects on student grades and no effect on persistence through university. We further combine survey and administrative data with machine learning methods to explore treatment effect heterogeneity, finding no evidence of meaningful variation in treatment effects across student subgroups. Despite the recent promise of these light-touch interventions, our findings indicate further research is required to identify the contexts in which their benefits generalize.</p>
</blockquote>

<p>Both types of interventions involved an invitation to complete an online survey and an online module. Students were incentivized to participate “by assigning a small participation grade of 1-2% of the total course grade for completion.” The interventions were delivered from 2015 to 2019 at two Canadian universities: the University of Toronto and York University. All together, ~12,000 students enrolled in first-year economics classes were invited to complete the survey.</p>

<p>To my mind, this is a classic <a href="https://en.wikipedia.org/wiki/Nudge_theory">nudge</a> intervention; a small lift for participants, so the best we can hope for is small effect sizes.</p>

<h3 id="what-was-in-the-online-modules">What was in the online modules?</h3>

<p>The social belonging intervention:</p>

<blockquote>
  <p>Students randomly assigned to the belonging intervention read short descriptive stories from a diverse body of upper-year students. These stories were designed to convey the message that worries about belonging are common during academic transition and, with time, they should expect these feelings to dissipate as they meet others like themselves, become more familiar with their surroundings, and settle into a routine. The stories also illustrated that there are others like them who faced and overcame similar struggles. The students were then asked to write a short script on how this message resonated with their own experiences to date, and were told that these responses might be used to help future cohorts.</p>
</blockquote>

<p>The growth mindset intervention:</p>

<blockquote>
  <p>Students who were randomly assigned to the intervention first read information about the brain and how it learns new information. The content emphasized that by exerting effort through practicing and learning, the brain can be developed and abilities can be improved. In addition, the messages emphasized that working on strategies along with exerting effort can be beneficial. This was followed by scripts that conveyed personal experiences and responses from upper-year students who received the same information. After going through the articles and responses, students were asked to reflect on the information presented to them and were again informed that these responses may be used to help future incoming students.</p>
</blockquote>

<p>Again, this is a small nudge: 30-60 minutes of eductional readings and written reflection.</p>

<h3 id="what-effect-did-this-have-on-students">What effect did this have on students?</h3>

<p>The authors used information about students’ grades and enrollment status to track students during their subsequent years in college.</p>

<p>First of all, the big outcome: did the interventions affect 5-year student graduation rates? No, the pooled treatment effect across both interventions was very close to zero and swamped by a large standard error.</p>

<p>Well, maybe that’s not so surprising. What about for shorter-term academic outcomes? Did the interventions improve student grades in the semester they received the intervention? Also no. The intervention <em>slightly</em> improved final course grade (by 0.1 percentage points… out of 100!), but the effect is again indistinguishable from zero.</p>

<p>They do a lot of additional sub-group analyses, but the overall punchline is clear: this intervention did not have a detectable effect on students’ academic performance.</p>

<h3 id="what-can-we-learn-from-this-study">What can we learn from this study?</h3>

<p>I’ve previously helped <a href="https://arxiv.org/abs/2407.04915">build growth mindset interventions</a> for students onboarding to a math practice tool, so I was really interested by this result. “Mindset” interventions failing to replicate is nothing new, but this is a particularly large and high-quality study.</p>

<p>One point the authors make: a follow-up survey for students who received the growth mindset intervention did indicate increased growth mindset attitudes. So taking the module does change students minds… but those changes are either too small or not relevant to academic performance in the university context where the intervention was delivered.</p>

<p>If I wanted to keep investigating mindset interventions, I would probably attempt to measure to what degree the beliefs these interventions are designed to change are present in my target population. The authors write:</p>

<blockquote>
  <p>Another possible explanation for our results is that the targeted psychological constraints are not sufficiently present or binding in our setting. These interventions are designed to address specific barriers—such as fixed beliefs about intelligence or uncertainty about belonging—and may not be effective unless such barriers exist and constrain performance. When these constraints are weak or absent the scope for improvement is limited (<a href="https://eric.ed.gov/?id=EJ1280132">McPartlan et al., “Testing Basic Assumptions Reveals When (Not) to Expect Mindset and Belonging Interventions to Succeed”</a>).</p>
</blockquote>

<p>I wish that the initial survey had asked students about their existing mindset. That would let you explicitly measure the impact of the intervention in the most favorable possible case: does the intervention help students who <em>start</em> with a fixed mindset? In other words, do students <em>with fixed mindsets</em> who <em>subsequently increase their growth mindset attitudes</em> do better academically? If so, there might be something to salvage here. If not… then we may be mistaken about the relevance of a fixed mindset for many academic outcomes we care about.</p>]]></content><author><name>Zachary Levonian</name></author><category term="research" /><category term="education" /><category term="psychology" /><category term="growth mindset" /><category term="short" /><summary type="html"><![CDATA[May 2026 NBER working paper finds null results for nudge-style mindset interventions.]]></summary></entry><entry><title type="html">MIT course (2023): Generative Artificial Intelligence in K12 Education</title><link href="https://levon003.github.io/2026/04/24/mit-genai-in-education.html" rel="alternate" type="text/html" title="MIT course (2023): Generative Artificial Intelligence in K12 Education" /><published>2026-04-24T00:00:00+00:00</published><updated>2026-04-24T00:00:00+00:00</updated><id>https://levon003.github.io/2026/04/24/mit-genai-in-education</id><content type="html" xml:base="https://levon003.github.io/2026/04/24/mit-genai-in-education.html"><![CDATA[<p>A colleague pointed me to MIT CSAIL and the MIT Media Lab’s Fall 2023 course <a href="https://mit-cml.github.io/gen-ai-fall-2023.github.io/">“Generative Artificial Intelligence in K12 Education”</a>.</p>

<p>Here’s a reformatted list of the readings from that course.</p>

<h2 id="topic-1-collaborative-learning">Topic 1: Collaborative Learning</h2>

<p><strong>Main reading:</strong>
Dillenbourg, P. (1999). What do you mean by collaborative learning? In <em>Collaborative-learning: Cognitive and Computational Approaches</em>. Elsevier. <a href="https://tecfa.unige.ch/tecfa/teaching/aei/papiers/Dillenbourg.pdf">PDF</a></p>

<p><strong>Optional additional reading:</strong></p>

<ul>
  <li><em>In-Person Approaches</em> — Education Endowment Foundation. <a href="https://educationendowmentfoundation.org.uk/education-evidence/teaching-learning-toolkit/collaborative-learning-approaches">Collaborative learning approaches</a>. EEF Teaching and Learning Toolkit.</li>
  <li><em>Technology Support Approaches</em> — Collaborative Learning Models and Support Technologies in the Future Classroom. (2011). <em>International Journal for Educational Media and Technology</em>, <em>5</em>(1). <a href="https://ijemt.org/index.php/journal/article/view/133">ijemt.org</a></li>
  <li><em>Collaborative or Cooperative Learning?</em> — Brandon, D. P., &amp; Hollingshead, A. B. (2004). Collaborative learning and computer-supported groups. In T. S. Roberts (Ed.), <em>Online collaborative learning: Theory and practice</em>. Information Science Publishing. <a href="https://doi.org/10.4018/978-1-59140-174-2.ch009">DOI:10.4018/978-1-59140-174-2.ch009</a></li>
</ul>

<h2 id="topic-2-reciprocal-peer-tutoring-rpt">Topic 2: Reciprocal Peer Tutoring (RPT)</h2>

<p><strong>Main reading:</strong>
Allen, A. R., &amp; Boraks, N. (1978). Peer tutoring: Putting it to the test. <em>The Reading Teacher</em>, <em>32</em>(3), 274–278. <a href="https://www.jstor.org/stable/20194752">JSTOR</a></p>

<p><strong>Optional additional reading:</strong></p>

<ul>
  <li><em>Research Paper #1</em> — Cheng, Y.-C., &amp; Ku, H.-Y. (2009). An investigation of the effects of reciprocal peer tutoring. <em>Computers in Human Behavior</em>, <em>25</em>(1), 40–49. <a href="https://doi.org/10.1016/j.chb.2008.06.001">DOI:10.1016/j.chb.2008.06.001</a></li>
  <li><em>Research Paper #2</em> — Walker, E., Rummel, N., &amp; Koedinger, K. R. (2014). Adaptive intelligent support to improve peer tutoring in algebra. <em>International Journal of Artificial Intelligence in Education</em>, <em>24</em>(1), 33–61. <a href="https://doi.org/10.1007/s40593-013-0001-9">DOI:10.1007/s40593-013-0001-9</a></li>
  <li><em>Preliminary Research in Technology-enhanced RPT</em> — Oikarinen, R. M., Oikarinen, J. K., Havu-Nuutinen, S., &amp; Pöntinen, S. (2022). Students’ collaboration in technology-enhanced reciprocal peer tutoring as an approach towards learning mathematics. <em>Education and Information Technologies</em>, <em>27</em>(6), 7519–7548. <a href="https://doi.org/10.1007/s10639-021-10799-3">DOI:10.1007/s10639-021-10799-3</a></li>
</ul>

<h2 id="topic-3-heterogeneous-grouping">Topic 3: Heterogeneous Grouping</h2>

<p><strong>Main reading:</strong>
Gillies, R. M. (2007). Group composition (Chapter 5). In <em>Cooperative learning: Integrating theory and practice</em>. Sage. <a href="https://sk.sagepub.com/book/mono/cooperative-learning/toc">SAGE Knowledge</a></p>

<p><strong>Optional additional reading:</strong></p>

<ul>
  <li><em>Review of the literature</em> — Education Endowment Foundation. (2018). <a href="https://educationendowmentfoundation.org.uk/projects-and-evaluation/projects/best-practice-in-mixed-attainment-grouping">Best practice in mixed attainment grouping (pilot)</a>.</li>
</ul>

<h2 id="topic-4-ubiquitous-feedback">Topic 4: Ubiquitous Feedback</h2>

<p><strong>Main reading:</strong>
Monteiro, V., Mata, L., Santos, N., Sanches, C., &amp; Gomes, M. (2019). Classroom talk: The ubiquity of feedback. <em>Frontiers in Education</em>, <em>4</em>, 140. <a href="https://www.frontiersin.org/journals/education/articles/10.3389/feduc.2019.00140/full">https://www.frontiersin.org/journals/education/articles/10.3389/feduc.2019.00140/full</a></p>

<p><strong>Optional additional reading:</strong></p>

<ul>
  <li><em>Longitudinal Study</em> — Drayton, B., Falk, J. K., Stroud, R., Hobbs, K., &amp; Hammerman, J. (2010). After installation: Ubiquitous computing and high school science in three experienced, high-technology schools. <em>The Journal of Technology, Learning and Assessment</em>, <em>9</em>(3). <a href="https://ejournals.bc.edu/index.php/jtla/article/view/1608">https://ejournals.bc.edu/index.php/jtla/article/view/1608</a></li>
  <li><em>Literature Review of Studies on Ubiquitous Learning Environments</em> — Virtanen, M. A., Haavisto, E., Liikanen, E., &amp; Kääriäinen, M. (2018). Ubiquitous learning environments in higher education: A scoping literature review. <em>Education and Information Technologies</em>, <em>23</em>(2), 985–998. <a href="https://doi.org/10.1007/s10639-017-9646-6">DOI:10.1007/s10639-017-9646-6</a></li>
</ul>]]></content><author><name>Zachary Levonian</name></author><category term="education" /><category term="llm" /><category term="course" /><category term="roundup" /><category term="short" /><summary type="html"><![CDATA[List of readings from an AI in education undergraduate course.]]></summary></entry><entry><title type="html">Structuring research narratives</title><link href="https://levon003.github.io/2026/04/03/structuring-research-narratives.html" rel="alternate" type="text/html" title="Structuring research narratives" /><published>2026-04-03T00:00:00+00:00</published><updated>2026-04-03T00:00:00+00:00</updated><id>https://levon003.github.io/2026/04/03/structuring-research-narratives</id><content type="html" xml:base="https://levon003.github.io/2026/04/03/structuring-research-narratives.html"><![CDATA[<style>
  /* ---------- annotated-abstract ---------- */
  .annotated-abstract {
    font-family: Georgia, "Times New Roman", serif;
    line-height: 1.7;
    background: transparent;
  }

  .annotated-abstract .label {
    display: inline-block;
    font-family: -apple-system, system-ui, "Segoe UI", Helvetica, Arial, sans-serif;
    font-size: 0.65em;
    font-weight: 600;
    letter-spacing: 0.02em;
    padding: 0.05em 0.45em;
    border-radius: 999px;
    vertical-align: middle;
    margin-right: 0.25em;
    line-height: 1.5;
    white-space: nowrap;
  }

  .annotated-abstract .sentence {
    border-radius: 3px;
    padding: 0.1em 0.25em;
    margin-right: 0.15em;
  }

  /*
    Color palette — each category gets:
      --pill-bg / --pill-fg  → label pill
      --sent-bg              → sentence highlight

    These are intentionally muted so they read well on both white
    and dark backgrounds. In dark mode we just nudge opacity/lightness.
  */

  /* 1 · Background / Setting — slate blue */
  .cat-background .label { background: #4a6fa5; color: #fff; }
  .cat-background .sentence { background: rgba(74, 111, 165, 0.12); }

  /* 2 · Societal Problem — warm rose */
  .cat-society .label { background: #b5485a; color: #fff; }
  .cat-society .sentence { background: rgba(181, 72, 90, 0.12); }

  /* 3 · Technical Problem — amber */
  .cat-techproblem .label { background: #b8860b; color: #fff; }
  .cat-techproblem .sentence { background: rgba(184, 134, 11, 0.12); }

  /* 4 · Technical Solution — teal */
  .cat-techsolution .label { background: #2a9d8f; color: #fff; }
  .cat-techsolution .sentence { background: rgba(42, 157, 143, 0.12); }

  /* 5 · Societal Benefit — green */
  .cat-benefit .label { background: #548c3a; color: #fff; }
  .cat-benefit .sentence { background: rgba(84, 140, 58, 0.12); }

  /* 6 · Roadmapping — purple */
  .cat-roadmap .label { background: #7b5ea7; color: #fff; }
  .cat-roadmap .sentence { background: rgba(123, 94, 167, 0.12); }

  /* 7 · Details — steel */
  .cat-details .label { background: #6b7d8a; color: #fff; }
  .cat-details .sentence { background: rgba(107, 125, 138, 0.12); }

  /* ---- Dark-mode overrides ---- */
  @media (prefers-color-scheme: dark) {
    .annotated-abstract {
      border-left-color: #666;
    }
    .cat-background .label  { background: #5b83be; }
    .cat-background .sentence { background: rgba(91, 131, 190, 0.18); }

    .cat-society .label { background: #cf6275; }
    .cat-society .sentence { background: rgba(207, 98, 117, 0.18); }

    .cat-techproblem .label { background: #d4a017; }
    .cat-techproblem .sentence { background: rgba(212, 160, 23, 0.18); }

    .cat-techsolution .label { background: #3bc0ad; }
    .cat-techsolution .sentence { background: rgba(59, 192, 173, 0.18); }

    .cat-benefit .label { background: #6aad4e; }
    .cat-benefit .sentence { background: rgba(106, 173, 78, 0.18); }

    .cat-roadmap .label { background: #9678c2; }
    .cat-roadmap .sentence { background: rgba(150, 120, 194, 0.18); }

    .cat-details .label { background: #839bab; }
    .cat-details .sentence { background: rgba(131, 155, 171, 0.18); }
  }
</style>

<p>The story you tell about your research <em>is</em> the research.
The narrative you share with the research community is how your findings are understood by others; without a story, there is no research.</p>

<p>Research papers are the most common medium for presenting research stories, but talks, posters, and hallway conversations all involve making choices about how to structure your research narrative.</p>

<p>Learning to structure research narratives doesn’t come naturally, and it’s one of the things you’ll learn during a PhD.
Many researchers learn by imitating the structure of existing abstracts or Introduction sections, but I find it’s really useful to have a rough framework in mind.
What follows is the framework I use, as taught to me by <a href="https://mwittie.io/">Mike Wittie</a> during a 2012 <a href="https://en.wikipedia.org/wiki/Research_Experiences_for_Undergraduates">REU program</a>.
I would think about this framework like the <a href="https://en.wikipedia.org/wiki/Five-paragraph_essay">five-paragraph essay</a>: a structure that helps you produce a reasonable argument, but overly restrictive once you get more experience structuring research narratives.</p>

<h2 id="a-framework-for-structuring-research-narratives">A framework for structuring research narratives</h2>

<p>The framework has four components:</p>

<ol>
  <li>Societal Problem</li>
  <li>Technical Problem</li>
  <li>Technical Solution</li>
  <li>How the Solution Helps Society</li>
</ol>

<h3 id="societal-problem">Societal problem</h3>

<p>The <strong>societal problem</strong> tells us how the world is lacking.</p>

<p>The society in question can be as broad as you choose, from all of humanity to a small group of researchers, engineers, or practitioners trying to accomplish something.</p>

<p>A problem must – in principle – be solvable.</p>

<p>The problem should be as specific as you can make it for the audience you have in mind. “Food insecurity exists” <em>is</em> a societal problem, but “food distribution networks cannot meet existing needs” is more appropriate if the research you conducted is specific to food distribution.</p>

<h3 id="technical-problem">Technical problem</h3>

<p>The <strong>technical problem</strong> is why we can’t just <em>fix</em> the societal problem.</p>

<p>The technical problem is the domain-specific problem contained within the broader problem that is amenable to attack by our discipline’s methods.</p>

<p>Resolving the technical problem should recognizably move us closer to solving the societal problem.</p>

<p>The line between the societal problem and the technical problem is necessarily fuzzy. In general, you should start with a problem recognizable to any member of your audience and take argumentative steps toward a concrete technical problem specific to your discipline.</p>

<p>The societal problem and technical problem together are your strongest motivations for conducting this research. There may be other motivations that you mention incidentally or while discussing how your research relates to prior work, but lead with your strongest argument and ensure that addressing the technical problem clearly addresses the societal problem.</p>

<h3 id="technical-solution">Technical solution</h3>

<p>The <strong>technical solution</strong> is how our research fixes the identified technical problem.</p>

<p>Your solution should correspond clearly to the points you raised in the technical problem. If your technical solution seems to respond to a broader or more narrow problem than your technical problem suggests, that’s a good indicator that you should rework the technical problem or consider presenting less of the work.</p>

<p>For in-progress work, it works fine for the technical solution to be tentative or incomplete; by explicitly characterizing the technical problem you’re trying to solve, you make it obvious where the holes are in your solution.</p>

<p>It can be appropriate for the technical solution to include only a brief teaser of your research work. In longer formats, it is fine to include additional details about the methods you used and your results alongside the technical solution.</p>

<h3 id="how-the-solution-helps-society">How the solution helps society</h3>

<p><strong>How the solution helps society</strong> draws us back from the specifics of your solution to the new state of the societal problem following your research.</p>

<p>If you discussed specific results while describing your technical solution, you should link those results to specific changes for researchers or practitioners who are facing the same societal problem.</p>

<p>Describing the societal benefit can be very brief, but you must do it: remind your reader where you started, or you’ll be left with an incomplete narrative.</p>

<h2 id="arguments-to-avoid">Arguments to avoid</h2>

<p>Choosing an appropriate societal problem and technical problem is important because these communicate your core motivations for conducting the research. Unconvincing or vague motivations will leave your audience confused about the potential value of your research.</p>

<p>Here are two common ‘bad’ arguments to avoid:</p>

<h3 id="x-is-hard">“X is hard”</h3>

<p>Why haven’t we fixed the societal or technical problem yet? Well, because they’re hard problems!</p>

<p>Consider this argument:</p>

<blockquote class="annotated-abstract">
    <span class="cat-society"><span class="label">Societal problem</span> <span class="sentence">Lack of access to food causes human misery. Food distribution networks can provide access, but designing and implementing these networks is challenging.</span></span>
    <span class="cat-techproblem"><span class="label">Technical problem</span> <span class="sentence">Networks need stable supply chain data to distribute food.</span></span>
    <span class="cat-techsolution"><span class="label">Technical solution</span> <span class="sentence">We propose a new method for collecting and structuring supply chain data.</span></span>
    <span class="cat-benefit"><span class="label">How the solution helps society</span> <span class="sentence">Our method enables food distribution networks to operate more efficiently and so increase access to food.</span></span>
</blockquote>

<p>Instead, we should be more specific about <em>why</em> these problems are challenging.</p>

<blockquote class="annotated-abstract">
    <span class="cat-society"><span class="label">Societal problem</span> <span class="sentence">Food distribution networks can provide access to food, but designing effective networks depends on access to stable supply chain data.</span></span>
    <span class="cat-techproblem"><span class="label">Technical problem</span> <span class="sentence">Suppliers provide data in diverse formats or not at all.</span></span>
    <span class="cat-techsolution"><span class="label">Technical solution</span> <span class="sentence">We propose a new method for collecting and structuring supply chain data.</span></span>
    <span class="cat-benefit"><span class="label">How the solution helps society</span> <span class="sentence">Our method enables food distribution networks to operate more efficiently and so increase access to food.</span></span>
</blockquote>

<p>Sharpening the societal problem and specifying a concrete cause forced us to be more specific about the technical problem we actually solved.</p>

<p>When you feel tempted to make an “X is hard” argument, that’s usually a sign that your argument is too high-level. That can be okay for an elevator pitch, but you can make a stronger argument by being specific about <em>what</em> is hard.</p>

<h3 id="research-gaps">Research gaps</h3>

<p>One common piece of advice is to identify a <a href="https://libanswers.snhu.edu/faq/264001">“research gap”</a>: a research question that hasn’t yet been addressed in the published literature.</p>

<p>However, research gaps make for ineffective motivations.</p>

<p>As Casey Fiesler <a href="https://bsky.app/profile/cfiesler.bsky.social/post/3lmursrhukc2b">writes on Bluesky</a>:</p>

<blockquote>
  <p>I would love to never see the phrase “little is known about” in a paper ever again.</p>

  <p>(1) Most of the time the authors are far more confident about that statement than they should be.</p>

  <p>(2) There are lots of things we know little about because no one cares. Just not knowing isn’t a good motivation.</p>
</blockquote>

<p>Fiesler is correct: “there’s a gap” is not a compelling justification for doing research.
It <em>is</em> good to notice when there are gaps, but you should first ask:</p>

<blockquote>
  <p><em>Why</em> is there a gap?</p>
</blockquote>

<p>Use the answer to that question as your technical problem instead.</p>

<h2 id="example-my-first-paper">Example: My first paper</h2>

<p>Let’s look at the abstract from my first paper and see how well I followed this framework.</p>

<p>The paper was called <a href="https://ojs.aaai.org/index.php/ICWSM/article/view/7310">“Bridging Qualitative and Quantitative Methods for User Modeling: Tracing Cancer Patient Behavior in an Online Health Community”</a>, and it proposed a new method for researchers to use when constructing quantitative <a href="https://en.wikipedia.org/wiki/User_modeling">user models</a>.
This is a hard test for this framework, because the contribution – and thus my motivations – were very conceptual.</p>

<p>Here’s the abstract, with annotations added:</p>

<blockquote class="annotated-abstract">
    <span class="cat-society"><span class="label">Societal problem</span> <span class="sentence">Researchers construct models of social media users to understand human behavior and deliver improved digital services. Such models use conceptual categories arranged in a taxonomy to classify unstructured user text data.</span></span>
    <span class="cat-techproblem"><span class="label">Technical problem</span> <span class="sentence">In many contexts, useful taxonomies can be defined via the incorporation of qualitative findings, a mixed-methods approach that offers the ability to create qualitatively-informed user models. But operationalizing taxonomies from the themes described in qualitative work is non-trivial and has received little explicit focus.</span></span>
    <span class="cat-techsolution"><span class="label">Technical solution</span> <span class="sentence">We propose a process and explore challenges bridging qualitative themes to user models, for both operationalization of themes to taxonomies and the use of these taxonomies in constructing classification models.</span></span>
    <span class="cat-details"><span class="label">Method details</span><span class="sentence">For classification of new data, we compare common keyword-based approaches to machine learning models. We demonstrate our process through an example in the health domain, constructing two user models tracing cancer patient experience over time in an online health community.</span></span>
    <span class="cat-benefit"><span class="label">How the solution helps society</span> <span class="sentence">We identify patterns in the model outputs for describing the longitudinal experience of cancer patients and reflect on the use of this process in future research.</span></span>
</blockquote>

<p>Overall, I think this abstract is <em>okay</em>.</p>

<ul>
  <li>My societal problem is expressed very <em>implicitly</em>. That can be okay, but I could consider an explicit frame instead: “Models of social media users without valid conceptual categories can produce misunderstandings and lead to ineffective designs.”</li>
  <li>The technical problem uses <em>both</em> of the arguments to avoid: I say the process is hard and that there’s a research gap! Instead, I should be specific: “But operationalizing taxonomies from the themes described in qualitative work requires a process that can bridge from the thematic to the concrete.”</li>
  <li>My description of the technical solution is vague, although I think that’s mostly okay in an abstract.</li>
  <li>My description of how the solution helps society is too vague. I should tie it back to the problems I identified more explicitly: “Our process creates quantitative user models that are both useful and valid.”</li>
</ul>

<p>If I had used this framework more explicitly, I would have produced a stronger abstract! Here’s a revised version:</p>

<blockquote class="annotated-abstract">
    <span class="cat-society"><span class="label">Societal problem</span> <span class="sentence">Quantitative models of social media users without valid conceptual categories can produce misunderstandings and lead to ineffective designs.</span></span>
    <span class="cat-techproblem"><span class="label">Technical problem</span> <span class="sentence">In many contexts, useful category taxonomies can be defined via the incorporation of qualitative findings, a mixed-methods approach that offers the ability to create qualitatively-informed user models. But operationalizing taxonomies from the themes described in qualitative work requires a process that can bridge from the thematic to the concrete.</span></span>
    <span class="cat-techsolution"><span class="label">Technical solution</span> <span class="sentence">We propose a process and explore challenges bridging qualitative themes to user models, for both operationalization of themes to taxonomies and the use of these taxonomies in constructing classification models.</span></span>
    <span class="cat-details"><span class="label">Method details</span><span class="sentence">For classification of new data, we compare common keyword-based approaches to machine learning models. We demonstrate our process through an example in the health domain, constructing two user models tracing cancer patient experience over time in an online health community.</span></span>
    <span class="cat-benefit"><span class="label">How the solution helps society</span> <span class="sentence">Our process produces user models with qualitatively-grounded categories that better capture user behavior and are more useful for researchers.</span></span>
</blockquote>

<h2 id="using-this-framework">Using this framework</h2>

<p>I try to use this framework as early as possible in a research project. I’ll write four high-level bullet points and see if I find the narrative compelling even without detail. Each month, I return to my bullet points, revising them to be more specific and to see how my thinking about the research has changed.</p>

<p>Research abstracts lend themselves well to this framework, since an abstract should usually serve as a brief summary of the research narrative. But abstracts can include other details as well (listings of results, calls to action, important related work), so I like to keep a “clean” version of the narrative for any research project I’m working on.</p>

<p>In longer formats, such as a research talk or an Introduction section, you might choose to include more or less detail in each component of this framework. 
It can be fine to start with a smaller “abstract” version of your research narrative – essentially functioning as a roadmap – or to introduce only the societal problem initially.
Highly conceptual work might require significant exposition before the audience can even understand the technical problem. 
For example, many pure math talks are 80% “technical problem”, with a sliver of higher-level context at the start and a final theorem presented at the end, with details left to a longer-form write-up.</p>

<h2 id="other-frameworks">Other frameworks</h2>

<p>There are many frameworks for structuring research narratives. While writing up this post, I stumbled on Maggie Appleton’s notes on <a href="https://maggieappleton.com/openings">“On Opening Essays, Conference Talks, and Jam Jars”</a>, which uses a very similar narrative framing.</p>

<p><img src="/images/appleton_openings.webp" alt="Flowchart: a problem needs solutions. A status quo is changed, which has consequences for a community of people." />
<em>Maggie Appleton’s beautiful figure adapting Joseph Williams’ writing on problem structure.</em></p>

<p>I was taught to write abstracts by John Carlis’ <a href="https://web.archive.org/web/20260729200901/https://wacclearinghouse.org/docs/proceedings/iwac2014/presentations/8A-carlis-paper.pdf">“Thinking about Abstracts”</a>.
He breaks down the story of a research abstract into a set of “sentence purposes”, and several of his examples strongly resemble the framework above. But Carlis makes an important point: there’s nothing sacred about a particular narrative structure.
The framework above aligns with researcher expectations that are highly applicable to the fields I know well, but other fields can have very different expectations regarding research stories.</p>

<blockquote>
  <p>A good abstract tells a story. Furthermore, the parts of the story appear in a sensible order, one commonly seen by its (limited) audience. It, therefore, meets its readers’ expectations. When writing my own abstracts or helping others, I find that stating the purpose(s) of each sentence helps me see the story’s flow from a reader’s point of view, and readily find flaws in the story. If you think of an abstract as telling a story, then of course only some content and orderings are effective, and you can judge when content and order works or doesn’t.</p>
</blockquote>

<p>When you start to diverge from the structure I present above, ask yourself: does this meet my readers’ expectations? Does my story ‘work’?</p>

<h2 id="other-resources">Other resources</h2>

<ul>
  <li><a href="https://matt.might.net/articles/academic-presentation-tips/">“10 tips for academic talks”</a>. See the section on “Motivation” in particular.</li>
  <li><a href="https://www.biorxiv.org/content/10.1101/088278v5.full">“Ten simple rules for structuring papers”</a></li>
  <li>Eugene Yan’s <a href="https://eugeneyan.com/writing/writing-faq/">“Frequently Asked Questions about My Writing Process”</a> – Includes a nice set of resources related to writing at the bottom of the post</li>
</ul>

<h2 id="related-posts">Related posts</h2>

<ul>
  <li><a href="/2025/03/07/peer-review.html">Peer reviewing</a></li>
  <li><a href="/2024/01/31/academic-writing.html">Common academic writing pitfalls</a></li>
</ul>]]></content><author><name>Zachary Levonian</name></author><category term="research" /><category term="writing" /><summary type="html"><![CDATA[How to structure research narratives for talks, papers, and posters]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://levon003.github.io/images/appleton_openings.webp" /><media:content medium="image" url="https://levon003.github.io/images/appleton_openings.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Taste</title><link href="https://levon003.github.io/2026/03/22/taste.html" rel="alternate" type="text/html" title="Taste" /><published>2026-03-22T00:00:00+00:00</published><updated>2026-03-22T00:00:00+00:00</updated><id>https://levon003.github.io/2026/03/22/taste</id><content type="html" xml:base="https://levon003.github.io/2026/03/22/taste.html"><![CDATA[<p>A quick link round-up about <em>taste</em>:</p>

<ol>
  <li>Pavel Samsonov, <a href="https://productpicnic.beehiiv.com/p/taste-is-a-dead-end">Taste is a dead end; what you should have is a point of view</a>, <em>Product Picnic</em>, April 2025</li>
  <li>Nicole Alexandra Michaelis, <a href="https://uxdesign.cc/designers-we-should-be-killing-it-right-now-c0a0c535f456">Designers, we should be killing it right now</a>, <em>UX Collective</em>, March 2026</li>
  <li>Harrison Wheeler, <a href="https://technicallyspeakinghw.substack.com/p/what-the-hell-is-taste">Do you have good taste? That’s debatable.</a>, <em>technically speaking</em>, April 2025</li>
</ol>

<p>Is taste a useful concept for designers, researchers, and software developers?</p>

<p>Current thoughts: Taste <em>is</em> important, but – building on Michaelis and Samsonov – we should try to be more specific.</p>

<h2 id="further-reading">Further reading</h2>
<ul>
  <li>Michael Spicher, <a href="https://iep.utm.edu/aesthetic-taste/">Aesthetic taste</a>, <em>Internet Encyclopedia of Philosophy</em></li>
  <li>Kyle Chayka, <a href="https://www.newyorker.com/culture/infinite-scroll/why-tech-bros-are-now-obsessed-with-taste">Why Tech Bros Are Now Obsessed with Taste</a>, <em>New Yorker</em>, March 2026</li>
  <li>Zachary Levonian, <a href="/2026/01/19/coding-agent-tipping-point.html">My personal tipping point: LLM agents are good for coding now</a>, <em>this blog</em>, January 2026</li>
</ul>]]></content><author><name>Zachary Levonian</name></author><category term="taste" /><category term="design" /><category term="roundup" /><category term="short" /><summary type="html"><![CDATA[Is taste a useful concept for designers, researchers, and software developers?]]></summary></entry><entry><title type="html">Uncovering a 17-year Wikipedia hoax</title><link href="https://levon003.github.io/2026/02/19/seventeen-year-wikipedia-hoax.html" rel="alternate" type="text/html" title="Uncovering a 17-year Wikipedia hoax" /><published>2026-02-19T00:00:00+00:00</published><updated>2026-02-19T00:00:00+00:00</updated><id>https://levon003.github.io/2026/02/19/seventeen-year-wikipedia-hoax</id><content type="html" xml:base="https://levon003.github.io/2026/02/19/seventeen-year-wikipedia-hoax.html"><![CDATA[<p>Yesterday I stumbled into the coolest thing I’ve ever done as a Wikipedia editor: uncovering a hoax article.</p>

<p>The biography for composer <a href="https://web.archive.org/web/20260131012042/https://en.wikipedia.org/wiki/Jacob_Isaacson">Jacob Isaacson</a> was created on Wikipedia on June 29th, 2008. 17 years and 238 days later, I nominated it for deletion.</p>

<p>The article is about an American composer who supposedly died in 1980:</p>

<blockquote>
  <p>Jacob Isaacson (May 5, 1911 – September 8, 1980) was an American composer and musician. Isaacson was most noted for his own Colortone musical notation and his early works within this system. His association with the Fluxus movement was played down by Isaacson, who held European classical tradition in high regard, although his experimental and minimalist compositions drew inevitable comparison.</p>
</blockquote>

<p>I initially nominated the article for deletion because it had no <a href="https://en.wikipedia.org/wiki/Wikipedia:Reliable_sources">reliable sources</a>. I couldn’t find anything about Jacob Isaacson on Google Search, Google Scholar, or the Internet Archive. In fact, the <em>only</em> mention I could find was in the description for a July 2008 event called FLUMMOXED at the <a href="https://en.wikipedia.org/wiki/Flatpack_Film_Festival">Flatpack Film Festival</a>:</p>

<blockquote>
  <p>And finally, Scott Johnston (<a href="http://www.youtube.com/user/filmficciones70">Film Ficciones</a>) and Matthew Eaton (<a href="http://www.last.fm/music/Micronormous">Micronormous</a>) will be performing American composer Jacob Isaacson’s final ‘Colortone Experiment’. The piece, called Lucent Harmonic Color, fuses Isaacson’s experiments in sound (inspired by his own synaesthesia) with the cut-up cinema of Burroughs and Balch.</p>
</blockquote>

<p>Probably, Isaacson is just some ridiculously obscure composer, with a bio created by a family member or fan.
But the experimental nature of the FLUMMOXED performance and the mention of <a href="https://en.wikipedia.org/wiki/Fluxus">Fluxus</a> in Isaacson’s bio suggested there might be more going on here.
Other participants in the <a href="https://en.wikipedia.org/wiki/Wikipedia:Articles_for_deletion/Jacob_Isaacson">deletion discussion</a> pointed out some other weird inconsistencies in the biography.</p>

<p>So, I investigated further.</p>

<h2 id="investigating-the-hoax">Investigating the hoax</h2>

<p>The creator of the Jacob Isaacson article had <a href="https://en.wikipedia.org/wiki/Special:Contributions/Colemanoconnor">only made edits</a> to one other article: <a href="https://en.wikipedia.org/wiki/Pram_(band)">Pram (band)</a></p>

<p>Reading the Pram article tells us that Pram’s members are Rosie Cuckston, Matt Eaton, Andy Weir, and Samantha Owen.
Further, the band “collaborated with visual artist ‘Film Ficciones’ (filmmaker Scott Johnston)” in 2010.</p>

<p>Wait! Matt Eaton and Scott Johnston are the supposed performers of Isaacson’s work at FLUMMOXED. That’s too strong a connection to be a coincidence.</p>

<p>I wanted to send a message to Pram. Despite being formed in 1988, Pram still has active social media! I sent an email to a hotmail.co.uk address I found listed on their Facebook, asking if they could resolve this mystery.</p>

<p>Shockingly, I got back a reply within the hour from Matt Eaton himself:</p>

<blockquote>
  <p>So finally Jacob Isaacson has met his match !</p>

  <p>I did indeed create Isaacson as part of a one off performance I did with Scott Johnston at Flatpack Film Festival 2008.
The theme of the piece was in part misdirection and misinformation with some good old fashioned Sci-Fi tropes woven in.
The biography of this minor western composer somehow felt very close to home…</p>

  <p>My apologies for putting my fictions on Wikipedia, it’s a resource I love and very occasionally donate to.</p>
</blockquote>

<p>Straight from the horse’s mouth!</p>

<p>Thanks very much to Matt for resolving this mystery. It was fun to notice the connections between Isaacson and Pram and I’m pleased that we are able to clean up Wikipedia and get a good story out of it at the same time.</p>

<p>I told Matt there’s not much harm here: Isaacson’s biography got less than one human pageview per day and no other articles linked to that page.
It also shows how much Wikipedia’s policies have changed: an article like that wouldn’t survive for more than an hour today.</p>

<p>There are still tens of thousands of unsourced Wikipedia articles created more than ten years ago.
A few might be hoaxes like Jacob Isaacson, but most just need attention and care.
If you want to help, <a href="https://en.wikipedia.org/wiki/Wikipedia:WikiProject_Unreferenced_articles">WikiProject Unreferenced articles</a> is running its <a href="https://en.wikipedia.org/wiki/Wikipedia:WikiProject_Unreferenced_articles/Backlog_drives/March_2026">March 2026 drive</a> soon, and it’s a great way to jump in as a new Wikipedia editor and immediately feel like you’re making the world a better place.</p>

<h2 id="further-reading">Further reading</h2>

<ul>
  <li><a href="https://en.wikipedia.org/wiki/Wikipedia:Articles_for_deletion/Jacob_Isaacson">Wikipedia:Articles for deletion/Jacob Isaacson</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Wikipedia:List_of_hoaxes_on_Wikipedia">Wikipedia:List of hoaxes on Wikipedia</a></li>
  <li><a href="/tags/wikipedia">More posts about Wikipedia on this blog</a></li>
</ul>]]></content><author><name>Zachary Levonian</name></author><category term="wikipedia" /><category term="hoax" /><category term="short" /><summary type="html"><![CDATA[The coolest thing I've done on Wikipedia.]]></summary></entry><entry><title type="html">My personal tipping point: LLM agents are good for coding now</title><link href="https://levon003.github.io/2026/01/19/coding-agent-tipping-point.html" rel="alternate" type="text/html" title="My personal tipping point: LLM agents are good for coding now" /><published>2026-01-19T00:00:00+00:00</published><updated>2026-01-19T00:00:00+00:00</updated><id>https://levon003.github.io/2026/01/19/coding-agent-tipping-point</id><content type="html" xml:base="https://levon003.github.io/2026/01/19/coding-agent-tipping-point.html"><![CDATA[<p>The buzz has been building on social media for a while, but I wanted to note my own personal tipping point: last week (January 12-16, 2026) I wrote the majority of my code using software development agents.
Specifically, I used a combination of <a href="https://code.claude.com/docs/en/overview">Claude Code</a> and <a href="https://opencode.ai/">OpenCode</a>, both with Anthropic’s <a href="https://www.anthropic.com/news/claude-opus-4-5">Claude Opus 4.5</a> model.</p>

<p>I think these tools are officially a clear productivity boost for me. 
As echoed by many others, they not only make it easier to do the coding I was already planning to do, but they also make unthinkable refactors or fiddly, one-off tasks feasible – tasks that would otherwise not be worth the time. Just one example: I spent 30 minutes adding an <a href="https://inquirerpy.readthedocs.io/en/latest/">InquirerPy</a> <a href="https://en.wikipedia.org/wiki/Text-based_user_interface">TUI</a> to a non-interactive <a href="https://en.wikipedia.org/wiki/Extract,_transform,_load">ETL</a> tool, just to make managing pipeline metadata <em>slightly</em> easier (and more accessible to non-developers on my team). I had never used InquirerPy before, so it was the perfect kind of task for large language models: labor-intensive for me to do, but easy to describe and easy to verify.</p>

<p>I wanted to mark this moment because overall I feel a strong sense of dread.
I worry about major disruptions to white collar work, I worry about losing my <em>own</em> job, and I worry that even if I’m okay professionally my day-to-day work will become more competitive, less engaging, and more unstable.
While it’s an exciting time, the uncertainty is very unpleasant.</p>

<p>Hopefully, I’ll look back on this post in a few years and laugh at my fears. (LLMs will never capture <a href="https://aeon.co/essays/big-data-ai-and-the-peculiar-dignity-of-tacit-knowledge">tacit knowledge</a>, right?) Until then, I’ll leave you with a few articles reflecting on the automation of intellectual work and the use of LLM coding agents.</p>

<h2 id="further-reading">Further reading</h2>

<h3 id="on-displacement">On displacement</h3>
<ul>
  <li><a href="https://scottaaronson.blog/?p=9030">“ChatGPT and the Meaning of Life”</a> by Harvey Lederman (August 2025)</li>
  <li><a href="https://thepointmag.com/examined-life/a-matter-of-words/">“A Matter of Words”</a> by Megan Fritts (May 2025)</li>
  <li><a href="https://aeon.co/essays/imagine-if-we-didnt-fear-the-machines-of-our-own-making">“AlphaGolem”</a> by John Cornwell (November 2018)</li>
  <li><a href="https://www.lesswrong.com/posts/kAmgdEjq2eYQkB5PP/douglas-hofstadter-changes-his-mind-on-deep-learning-and-ai">“Gödel, Escher, Bach author Doug Hofstadter on the state of AI today”</a> (June 2023)</li>
  <li><a href="https://arstechnica.com/ai/2026/01/developers-say-ai-coding-tools-work-and-thats-precisely-what-worries-them/">“Developers say AI coding tools work—and that’s precisely what worries them”</a> by Benj Edwards (January 2026)</li>
  <li><a href="https://simonwillison.net/2026/Feb/15/deep-blue/">“Deep Blue”</a> by Simon Willison (February 2026)</li>
</ul>

<h3 id="on-agentic-coding">On agentic coding</h3>

<ul>
  <li><a href="https://arstechnica.com/information-technology/2026/01/10-things-i-learned-from-burning-myself-out-with-ai-coding-agents/">“10 things I learned from burning myself out with AI coding agents”</a> by Benj Edwards (January 2026)</li>
  <li><a href="https://timdettmers.com/2026/01/13/use-agents-or-be-left-behind/">“Use Agents or Be Left Behind? A Personal Guide to Automating Your Own Work”</a> by Tim Dettmers (January 2026)</li>
  <li><a href="/2026/01/07/verification-driven-development.html">Verification-driven development</a> (January 2026)</li>
  <li><a href="https://deadsimpletech.com/blog/week_with_opencode">“My week with opencode”</a> by Iris Meredith (January 2026)</li>
  <li><a href="https://www.anthropic.com/research/how-ai-is-transforming-work-at-anthropic">“How AI Is Transforming Work at Anthropic”</a> by Saffron Huang, Bryan Seethor, Esin Durmus, Kunal Handa, Miles McCain, Michael Stern, Deep Ganguli (December 2025)</li>
</ul>]]></content><author><name>Zachary Levonian</name></author><category term="llm" /><category term="code" /><category term="agents" /><category term="dread" /><category term="short" /><summary type="html"><![CDATA[Quick reflections on feelings of dread.]]></summary></entry></feed>